Compare commits

...

242 Commits
v0.1 ... v0.14

Author SHA1 Message Date
Branden Archer
11e5cb8ec2 Merge pull request #146 from brarcher/pre-v0.14
Update for v0.14
2017-09-26 23:25:48 -04:00
Branden Archer
bdd5c8fbbb Update CHANGELOG 2017-09-26 23:19:06 -04:00
Branden Archer
cd79839748 Update for v0.14 2017-09-26 23:14:47 -04:00
Branden Archer
d6b47914c8 Merge pull request #145 from brarcher/app-shortcuts
Add card shortcuts for most recently used cards
2017-09-26 23:12:00 -04:00
Branden Archer
ce1acb83f0 Add card shortcuts for most recently used cards
This adds app shortcuts for the most recently used cards.
When a new card is accessed, it is added to the shortcuts list.
When the list exceeds its maximum size, the least recently
used shortcut is discarded.

Android limits the maximum number of shortcuts to 5, however
it recommends in its documentation to limit this to 4. This
commit limits this to 3, however, as that is aesthetically pleasing.
2017-09-26 22:52:32 -04:00
Branden Archer
bc360aa06c Merge pull request #142 from brarcher/widget
Add widget for directly opening specific card
2017-09-19 13:49:47 -04:00
Branden Archer
ebe6139a64 Add widget for directly opening specific card 2017-09-19 12:58:48 -04:00
Branden Archer
d354fd1877 Merge pull request #138 from brarcher/transifex
Remove missing translations stubbed to English
2017-08-10 21:49:46 -04:00
Branden Archer
00b612d6c7 Remove missing translations stubbed to English
By request, a project on Transifex was created to better
manage translations. To make things less confusing, instead
of using the MissingTranslations lint error, and thus needing
to add stub translations, the progress of translations is now
handled on Transifex.

transifex.com/na-243/loyalty-card-locker/
2017-08-10 21:25:13 -04:00
Branden Archer
a307511193 Merge pull request #137 from brarcher/readme
README: add translating info
2017-08-10 21:22:41 -04:00
Branden Archer
f7f358c5c3 README: add translating info 2017-08-10 21:15:10 -04:00
Branden Archer
f7b50a72a4 Merge pull request #135 from brarcher/changelog
Update CHANGELOG for v0.11.1, v0.12, v0.13
2017-07-26 18:18:36 -04:00
Branden Archer
68ce1fe9fd Update CHANGELOG for v0.11.1, v0.12, v0.13 2017-07-26 18:12:01 -04:00
Branden Archer
e822ab0b56 Merge pull request #133 from brarcher/pre-v0.13
Update for v0.13
2017-07-25 10:04:44 -04:00
Branden Archer
e33ab682a6 Update for v0.13 2017-07-25 09:58:19 -04:00
Branden Archer
0d50ad6d10 Merge pull request #132 from brarcher/check-for-card
Verify card exists before displaying it
2017-07-23 10:27:06 -04:00
Branden Archer
4143e5c286 Bail if loyalty card could not be found
This case was hit at least once by a user, though the scenario
is not known. If it is hit, post a message and bail gracefully,
until the reason can be determined.
2017-07-23 10:05:05 -04:00
Branden Archer
15425d51aa Add debugging when entering view activity 2017-07-22 23:52:21 -04:00
Branden Archer
3abcb32a75 Merge pull request #130 from brarcher/text-update
Change lock/unlock text to say block/unblock
2017-07-18 22:25:47 -04:00
Branden Archer
93124a88a5 Change lock/unlock text to say block/unblock 2017-07-18 22:18:52 -04:00
Branden Archer
29b00e3b59 Merge pull request #129 from airon90/patch-1
Update strings.xml
2017-07-18 22:06:08 -04:00
Michael Moroni
04174154d6 Update strings.xml
Instead of "Block screen" I translated "Block rotation" because I think it's more intuitive
2017-07-17 12:49:39 +02:00
Branden Archer
bf60976d10 Merge pull request #128 from brarcher/lock-orientation
Add option to lock screen orientation
2017-07-16 21:32:55 -04:00
Branden Archer
fb7e3e12f2 Add menu option to lock screen when viewing card
When passing a phone to a clerk to scan the barcode, if the
phone is rotated and the screen reloads it can be bothersome
or confusion. To avoid this situation, a new option is
added to lock the screen.

A menu icon is now added which defaults as unlocked. When
touched the app sets its orientation to the "natural" orientation
of the device. When touched again the sensor dictates the
orientation of the device.
2017-07-16 21:26:40 -04:00
Branden Archer
641d29a6f8 Remove usage of depreated location of ActivityController 2017-07-16 21:26:40 -04:00
Branden Archer
0f2ee296b4 Remove container class for intro slides
Even with passing a bundle with the layout ID sometimes
some users will encounter a situation where the _layout
variable is null. To avoid further issues, creating a
class for each intro slide, so the layout is encoded
into the class itself.
2017-07-16 21:26:40 -04:00
Branden Archer
ecd2ecd517 Merge pull request #127 from brarcher/pre-v0.12
Update for v0.12
2017-07-12 22:55:31 -04:00
Branden Archer
f3e7d8f5c1 Update for v0.12 2017-07-12 22:47:29 -04:00
Branden Archer
f4522fb17d Merge pull request #126 from brarcher/scale-barcode
Scale barcode height to match reduced width
2017-07-12 22:14:03 -04:00
Branden Archer
824a72b156 Scale height of barcode to match ratio when reducing width
When reducing the pixel size of the width, if the height is not
scaled to match then 1D barcodes end up being squished and are
too narrow to scan. To avoid this, scale the height to match.
Then, when the barcode is loaded into a Bitmap it will scale up
to the correct size.

It was found that on a Galaxy S4 a barcode width of 400 px started
to see some blurriness, but 500 px still looked sharp. Reducing
the maximum width to 500 px.
2017-07-11 23:16:26 -04:00
Branden Archer
116a44fd1c Let BarcodeImageWriterTask's constructor be package private 2017-07-11 23:13:46 -04:00
Branden Archer
b6190d3250 Merge pull request #125 from arno-github/patch-3
French update
2017-07-03 15:55:48 -04:00
arno-github
82c40408f7 French update 2017-07-03 21:10:21 +02:00
Branden Archer
c6c9fb763a Merge pull request #124 from arno-github/patch-2
Update french translation
2017-07-02 22:36:30 -04:00
arno-github
dda5dcc652 Update french translation
Cosmetic changes
2017-07-02 20:00:21 +02:00
Branden Archer
d039ad9fa4 Merge pull request #122 from Entze/master
add & refine german translation
2017-06-29 21:44:02 -04:00
Lukas Grassauer
5c70c98eb9 Merge branch 'master' of github.com:brarcher/loyalty-card-locker 2017-06-29 16:28:17 +02:00
Lukas Grassauer
2face21985 add missing and revise de translations 2017-06-29 16:23:10 +02:00
Branden Archer
b38ec7b753 Merge pull request #121 from brarcher/pre-v0.11.1
Update for v0.11.1
2017-06-29 10:13:07 -04:00
Branden Archer
32d5aec76b Update for v0.11.1 2017-06-28 22:49:44 -04:00
Branden Archer
bdbcaf4b1e Merge pull request #120 from brarcher/handleConfigChanges
handle orientation and resize config change for IntroActivity
2017-06-28 22:48:50 -04:00
Branden Archer
f54905f218 handle orientation and resize config change for IntroActivity
If orientation and resize are not handled if the screen is
rotate or resized the IntroSlide fragment loses its layout,
and when it tries inflates the layout it fails to do so.
2017-06-28 22:42:02 -04:00
Branden Archer
2d3bd4a375 Merge pull request #118 from brarcher/credit
Add credit for save icon & update copyright string format
2017-06-26 22:56:36 -04:00
Branden Archer
de4ab95437 Add credit for save icon in about dialog 2017-06-26 22:10:09 -04:00
Branden Archer
09fba71710 Fix string format for copyright year
The year is an integer, but the format was for a string.
2017-06-26 22:10:09 -04:00
Branden Archer
2b84762b99 Merge pull request #117 from brarcher/changelog
Update CHANGELOG for v0.11
2017-06-26 22:07:38 -04:00
Branden Archer
3a4850d5cf Merge branch 'master' into changelog 2017-06-26 21:59:46 -04:00
Branden Archer
5bc8a915d6 Update CHANGELOG for v0.11 2017-06-26 21:59:36 -04:00
Branden Archer
15cbd5b641 Merge pull request #116 from brarcher/pre-v0.11
Update for v0.11 release
2017-06-26 21:59:07 -04:00
Branden Archer
d980079d76 Update for v0.11 release 2017-06-26 21:44:16 -04:00
Branden Archer
e58cbd972e Merge pull request #112 from brarcher/readme
Add attribution for save icon in README
2017-06-24 21:38:15 -04:00
Branden Archer
1fbecd439b Add attribution for save icon in README 2017-06-22 22:16:42 -04:00
Branden Archer
707306ac81 Merge pull request #111 from brarcher/readme
Update README with new screenshots
2017-06-21 23:42:12 -04:00
Branden Archer
86152586cd Update README with new screenshots 2017-06-21 23:32:09 -04:00
Branden Archer
fa33e4873e Merge pull request #109 from Airon90/patch-1
Update strings.xml
2017-06-20 22:43:40 -04:00
Branden Archer
24e4ef2d9b Merge branch 'master' into patch-1 2017-06-20 22:35:04 -04:00
Branden Archer
eac4142392 Escape ' in strings.xml 2017-06-20 22:23:33 -04:00
Branden Archer
f38a499bd8 Merge pull request #110 from brarcher/french
Update French translations
2017-06-20 22:21:45 -04:00
Branden Archer
0b8f52b87e Update French translations
Provided by GitHub user arno-github.
2017-06-20 22:14:27 -04:00
Michael Moroni
c8255a2648 Update strings.xml
Updated Italian (it) translation
2017-06-20 19:41:13 +02:00
Branden Archer
f28326e363 Merge pull request #108 from brarcher/intro
First run intro wizard
2017-06-18 23:46:37 -04:00
Branden Archer
a9952d021b Update to Robolectric 3.3.2
This is necessary, as Robolectric 3.0 did not work
with the newer Android SDK. Further, Robolectric 3.3.2
did not like running tests at SDK 17, so this was
also bumped.
2017-06-18 23:39:02 -04:00
Branden Archer
9274339be8 Add intro wizard for application 2017-06-18 23:37:51 -04:00
Branden Archer
18caa01089 Move the ic_launcher.xcf file into a separate directory 2017-06-18 23:05:23 -04:00
Branden Archer
ac495e1bf2 Add missing card add menu
There was no save button when creating a new card.
2017-06-18 23:05:22 -04:00
Branden Archer
a8eecb8841 Merge pull request #107 from brarcher/layout-and-colors
Layout and colors
2017-06-18 22:02:20 -04:00
Branden Archer
abdde45669 Update card view layout
This layout uses a TableLayout and shows lines dividing the
different parts of an entry.
2017-06-18 20:47:37 -04:00
Branden Archer
13b2faab39 Use save button in action bar 2017-06-18 15:37:40 -04:00
Branden Archer
433d31c30f Add super call in onCreateOptionsMenu 2017-06-18 15:37:40 -04:00
Branden Archer
fa7dcd253a Change menus names to focus where they are, not what they contain 2017-06-18 15:37:40 -04:00
Branden Archer
ac057f50e2 Change color palette to better match icon
The green primary color was a bit of contrast with the icons grey.
2017-06-18 15:37:40 -04:00
Branden Archer
ef33c7a33f Merge pull request #106 from brarcher/french-strings
Updated French strings.xml
2017-06-18 14:36:38 -04:00
Branden Archer
6028f7d674 Updated French strings.xml
Contributed by arno-github
2017-06-18 14:30:55 -04:00
Branden Archer
da5f999eed Merge pull request #104 from brarcher/edit-card
Update "Enter card" button if card id exists
2017-06-11 15:13:38 -04:00
Branden Archer
0a9124c834 Update "Enter card" button if card id exists
If someone is editing an existing card, or is adding a card
and wants to change the value, update the name of the button
from "Enter card" to "Edit card", to give the impression that
a new card id need not by typed from scratch.
2017-06-11 15:06:10 -04:00
Branden Archer
c173012955 Merge pull request #103 from brarcher/limit-width
Limit width of barcodes
2017-06-11 15:05:27 -04:00
Branden Archer
64db9e593d Limit width of barcodes
To reduce the amount of memory used in generating barcodes, limit
the width of the barcodes to no more than 3x the height.
This should be plenty sufficient to generate a usable barcode.

On devices with a really long width generating a barcode of 200x2560
is not useful and can result in an OutOfMemoryError.
2017-06-11 14:56:17 -04:00
Branden Archer
acde4ef181 Update to zxing 3.3.0 and zxing-android 3.5.0 2017-06-11 14:55:39 -04:00
Branden Archer
807ea25ac9 Merge pull request #102 from brarcher/privacy-policy
Create privacy-policy.md
2017-06-10 15:40:50 -04:00
Branden Archer
ad4476fead Create privacy-policy.md
This is required by Google for apps in the play store
which use certain permissions, such as the camera.
2017-06-10 15:33:25 -04:00
Branden Archer
f2c0e3904f Merge pull request #98 from brarcher/travis
Travis-CI changes
2017-05-20 18:07:47 -04:00
Branden Archer
0eeadbf77f Build only the release variant
Building both debug and release was unnecessary, and only
increased the time to validate a pull request
2017-05-20 13:36:23 -04:00
Branden Archer
c0e95bd4da do not emit xml results
gradle will output the failures, so this is unnecessary
2017-05-20 13:35:57 -04:00
Branden Archer
b7e8b8d36e Merge pull request #96 from brarcher/about
About
2017-03-06 21:49:53 -05:00
Branden Archer
2bca8b738a Change about dialog info to use map instead of String[][] 2017-03-05 14:42:19 -05:00
Branden Archer
2b617ceca5 Remove mention of Freepik icons from README
The main application icon was replaced, and the icon from
Freepik is no longer used.
2017-03-05 14:42:19 -05:00
Branden Archer
29838f4f73 Merge pull request #95 from brarcher/readme
Add Google Play badge
2017-03-04 00:08:04 -05:00
Branden Archer
1403d4a98e Add Google Play badge
Adds a Google Play badge and updated the F-Droid badge to match
2017-03-03 23:56:43 -05:00
Branden Archer
c4b860bb3d Merge pull request #94 from brarcher/pre-populate-enter-card
Pre-populate Barcode Selector
2017-02-12 22:48:35 -05:00
Branden Archer
2768f4c590 Start with all barcode images visible
If the user enters the barcode selector view with a pre-existing
id, the barcodes are immediately drawn. However, if the ImageViews
were invisible their initial size is not known, and as such the
generated barcode images are fuzzy or not as expected. To avoid this,
all images are set to be visible. The expectation is that once the
barcodes are drawn for the first time if they are invalid the images
will be made invisible anyway.
2017-02-12 22:42:25 -05:00
Branden Archer
8660d4e218 Pre-populate card id in Barcode Selector
If a user is attempting to update a card id of an existing card,
before when the Enter Card button was selected the barcode selector
view would be empty. To be more useful, the view will now be
pre-populated with the existing card id.
2017-02-12 22:42:25 -05:00
Branden Archer
596d899449 Merge pull request #93 from brarcher/changelog
Update changelog for v0.10
2017-02-12 15:53:25 -05:00
Branden Archer
0db5a6f803 Update changelog for v0.10 2017-02-12 15:40:20 -05:00
Branden Archer
bbd3782bf2 Merge pull request #92 from brarcher/pre-v0.10
Update for v0.10 release
2017-02-12 15:35:18 -05:00
Branden Archer
b69f2d470b Update for v0.10 release 2017-02-12 15:25:49 -05:00
Branden Archer
ce4dc01c9d Merge pull request #91 from brarcher/view-layout
View layout
2017-02-11 22:43:58 -05:00
Branden Archer
5cea7eba6c Allow text to be selectable in view mode
When text is long clicked it is now selectable, so that one can
copy it to the clipboard.
2017-02-11 22:33:57 -05:00
Branden Archer
0f7d64bed6 Show card data in a TextView when viewing
Earlier when a card was viewed it was shown in an EditView which
was disabled. This was not optimal, as the resulting text was
dimmed. To help the text be easier to read, a TextView is now
used to display the data.

In addition, the layout of the card viewing screen has been adjusted
2017-02-11 22:33:57 -05:00
Branden Archer
330b6ecd54 Merge pull request #87 from brarcher/string-correction
String correction
2017-01-28 19:58:56 -05:00
Branden Archer
3f5bcbb941 Add log message when exporting data 2017-01-28 16:22:14 -05:00
Branden Archer
9c74b2a264 Correct typo in comment 2017-01-28 16:22:14 -05:00
Branden Archer
dfc804fbe5 correct "Use external application" title
The title mistakenly said filesystem.
2017-01-28 16:22:14 -05:00
Branden Archer
fa73a30bf4 Use string const for button
There is a string for the "Use external application" button,
but it was not used.
2017-01-28 16:22:14 -05:00
Branden Archer
449419b647 Merge pull request #85 from brarcher/no-download-extras
Remove downloading extras during travis build
2017-01-17 21:43:35 -05:00
Branden Archer
36916b622b Remove downloading extras during travis build
The application does not use google play and other extra libraries,
so downloading them during the build should be unnecessary.
2017-01-17 21:25:50 -05:00
Branden Archer
03f97374bd Merge pull request #84 from brarcher/export-filename
Change default import/export file name
2017-01-17 21:24:35 -05:00
Branden Archer
8650878d95 Change default import/export file name
The application is now "Loyalty Card Keychain", but "Locker"
still appears in the application's default import/export
file. Updating the file to match the new name of the application.
2017-01-17 21:06:07 -05:00
Branden Archer
defb8793f6 Merge pull request #83 from brarcher/changelog
Update CHANGELOG for v0.9
2017-01-17 20:58:12 -05:00
Branden Archer
c6b7fd6032 Update CHANGELOG for v0.9 2017-01-17 20:47:31 -05:00
Branden Archer
5960f03f9b Merge pull request #82 from brarcher/pre-v0.9
Update for release v0.9
2017-01-16 23:07:37 -05:00
Branden Archer
b1df0c0baf Update for release v0.9 2017-01-16 22:53:21 -05:00
Branden Archer
bfb131c811 Merge pull request #81 from brarcher/keychain
Update the name "Locker" to "Keychain"
2017-01-16 22:50:36 -05:00
Branden Archer
4580ee8017 Update the "Locker" name to "Keychain"
The icon on the application is no longer a locker, so the
Locker part of the name no longer makes sense. Changing it
to Keychain, as it may make more sense.
2017-01-16 22:44:11 -05:00
Branden Archer
d76a6d79d7 Merge pull request #76 from brarcher/select-import-file
Select import file, allow exported file to be sent
2017-01-16 22:30:54 -05:00
Branden Archer
b38e035fb6 Allow sending newly exported data
With this change once data is exported it can be sent to
another application.

Translation contributions:
Clonewayx: cs
PanderMusubi: nl
pbeckmann: de
Airon90: it
arno-github : fr

# Conflicts:
#	app/src/main/res/values-it/strings.xml
2017-01-16 22:23:08 -05:00
Branden Archer
eeb41376c5 Add ability to select file to import
To date the only way to import a file is to hope the exported file
is in the correct location. To remove this requirement, this change
checks if there are any activities available on the system which
can look in the file system. If there are, new options for
importing data will be available.

In addition, the import/export activity's layout was updated,
adding more explanation.

Translation contributions:
Clonewayx: cs
PanderMusubi: nl
pbeckmann: de
Airon90: it
arno-github : fr
2017-01-16 22:23:08 -05:00
Branden Archer
89f649e5ef Correct TAG name 2017-01-16 22:23:08 -05:00
Branden Archer
4bde8f49b1 update gradle and android plugin 2017-01-16 22:23:08 -05:00
Branden Archer
87c1bbdbf4 Merge pull request #80 from arno-github/arno-github-patch-4
New french strings update
2016-12-10 11:17:16 -05:00
arno-github
7029818789 Update strings.xml 2016-12-10 15:10:41 +01:00
arno-github
b64c29bf96 Update french strings 2016-12-10 14:30:13 +01:00
arno-github
24317f4d99 Update strings.xml 2016-12-10 14:26:20 +01:00
arno-github
b9d3699038 Update strings.xml 2016-12-10 14:21:19 +01:00
arno-github
ed3c9ceb50 Update strings.xml 2016-12-10 14:13:03 +01:00
arno-github
0a938628cb Update strings.xml 2016-12-10 11:53:44 +01:00
arno-github
5720da1581 Merge pull request #1 from arno-github/arno-github-patch-1
Create strings.xml
2016-12-10 11:52:03 +01:00
arno-github
5393903872 Create strings.xml 2016-12-10 11:50:20 +01:00
Branden Archer
8e0a4b978d Merge pull request #75 from betsythefc/master
Icon V2
2016-12-06 21:53:07 -05:00
betsythefc
df21c2577d Improved icon with keyring hole 2016-12-05 20:47:52 -08:00
betsythefc
a7d5b6fa0c New icon V2 2016-12-05 11:35:40 -08:00
betsythefc
b49501e66a Merge remote-tracking branch 'brarcher/master' 2016-12-05 11:30:54 -08:00
Branden Archer
65d0f57358 Merge pull request #62 from techwebpd/patch-1
Lithuanian strings.xml
2016-12-03 11:35:32 -05:00
Branden Archer
927ac61912 Merge branch 'master' into patch-1 2016-12-03 11:29:52 -05:00
Branden Archer
fd2b84f318 Remove string that did not need translating. 2016-12-03 11:29:44 -05:00
betsythefc
ed4afed538 New Icon 2016-12-02 11:19:18 -08:00
Branden Archer
007427023b Merge pull request #70 from brarcher/changelog
Create CHANGELOG.md
2016-11-22 22:44:13 -05:00
Branden Archer
b4ff68eddb Create CHANGELOG.md
Per-request from:
https://github.com/brarcher/loyalty-card-locker/issues/48
2016-11-22 22:35:49 -05:00
Branden Archer
151b81daa7 Merge pull request #69 from brarcher/pre-v0.8
Update for release v0.8
2016-11-22 22:21:35 -05:00
Branden Archer
c090af1c7d Update for release v0.8 2016-11-22 22:13:29 -05:00
Branden Archer
54a0f89821 Merge pull request #66 from lgasp/patch-1
Small update to italian translation
2016-11-06 15:07:58 -05:00
lgasp
f01058d5b8 Update strings.xml 2016-11-05 11:19:48 +01:00
lgasp
e01c77681b proposing less confusing trnalsation fo r button
"Scan code" is translated as "salva tessera" that corresponds to English "save card",  that is similar to the button " save" on the same screen.
"Scansione codice"is a more literal, and hopefully more clear, translational
2016-11-04 19:48:54 +01:00
Branden Archer
c49ecdca56 Merge pull request #57 from pbeckmann/master
Add a german translation.
2016-11-02 14:37:18 -04:00
Peer Beckmann
b7672e5247 Merge branch 'master' into master 2016-11-01 09:50:18 +01:00
techwebpd
71777a74fd Lithuanian strings.xml
lithuanian translation
2016-10-28 16:28:19 +03:00
Branden Archer
bebab039d7 Merge pull request #60 from brarcher/screenshots
Simplify screenshots in README.md
2016-09-12 00:10:06 -04:00
Branden Archer
588a573878 Simplify screenshots in README.md 2016-09-11 23:56:32 -04:00
Branden Archer
a9bbf8fb65 Merge pull request #59 from brarcher/screenshots
Add screenshots to README.md
2016-08-28 15:44:22 -04:00
Branden Archer
175135d695 Add screenshots to README.md
Requested in:
https://github.com/brarcher/loyalty-card-locker/issues/56
2016-08-28 15:30:23 -04:00
Branden Archer
9f08d07a6a Merge pull request #58 from Clonewayx/patch-1
Czech translation
2016-08-27 13:49:07 -04:00
Clonewayx
07f50b1544 Update and rename strings.xml to strings.xml
remove untranslatable line
2016-08-27 18:04:00 +02:00
Clonewayx
584064a4a2 Czech translation 2016-08-27 06:58:12 +02:00
Peer Beckmann
484f357a82 Add a german translation. 2016-08-25 19:57:55 +02:00
Branden Archer
e32b40a024 Merge pull request #55 from brarcher/delete-confirmation
Delete confirmation
2016-07-22 08:04:15 -04:00
Branden Archer
010b88d477 Attempt to force travis builds on non-container infrastructure
There is an issue with running the travis builds. The root
cause is unknown. Attempting to use the non-container
version of the infrastructure to see if the issue persists.
2016-07-22 07:56:23 -04:00
Branden Archer
fe0bb7b870 Add a confirmation before deleting card
https://github.com/brarcher/loyalty-card-locker/issues/53
2016-07-22 07:56:22 -04:00
Branden Archer
925f780599 Merge pull request #54 from brarcher/screen-brightness
Adjust brightness to its maximum when displaying a card
2016-07-20 00:04:42 -04:00
Branden Archer
9492d206d9 Adjust brightness to its maximum when displaying a card
Some users have mentioned that a barcode scanner has the best
success when the screen is at its brightest. To enable the best
results, the screen brightness will now be adjusted to its
maximum when displaying a loyalty card.

https://github.com/brarcher/loyalty-card-locker/issues/21
2016-07-19 23:42:36 -04:00
Branden Archer
a32415b4d2 Reduce the number of cards tested in import/export tests
There is no need to have so many cards tested in the test, just
a few will do to prove the point.
2016-07-19 23:41:31 -04:00
Branden Archer
5965b82251 Merge pull request #51 from brarcher/pre-v0.7
Update for release v0.7
2016-07-14 16:00:10 -04:00
Branden Archer
a5650e6382 Update for release v0.7 2016-07-14 15:37:35 -04:00
Branden Archer
6befc99ccc Merge pull request #50 from brarcher/id-to-clipboard
Copy card id to clipboard
2016-07-14 14:56:20 -04:00
Branden Archer
0e124b2550 Allow long-click of card to copy id to clipboard
https://github.com/brarcher/loyalty-card-locker/issues/49
2016-07-14 13:38:42 -04:00
Branden Archer
06a7db0e6d Update Android gradle plugin to 2.1.2 2016-07-14 13:38:42 -04:00
Branden Archer
f2f56b06dd Merge pull request #47 from brarcher/back-button
Let pushing back button in menu end import/export activity
2016-05-26 23:12:22 -04:00
Branden Archer
518f339c7a Let pushing back button in menu end import/export activity 2016-05-24 07:55:04 -04:00
Branden Archer
bbe6ec3741 Merge pull request #46 from brarcher/pre-v0.6
Update for release v0.6
2016-05-23 09:08:06 -04:00
Branden Archer
18310fdd25 Update for release v0.6 2016-05-23 08:52:27 -04:00
Branden Archer
19bd64c48c Merge pull request #44 from brarcher/manual-barcode-entry
Allow manual barcode entry
2016-05-23 08:51:28 -04:00
Branden Archer
c86819fc74 Allow user to enter a barcode manually
If a user is unable to scan a barcode, this commit allows a
user to enter is manually.

If the user selects to Enter Card instead of Capture Card,
the user may enter the card's id. As it may not be known which barcode
format the user expects, and the user may not know what barcode
type is what, all barcode types are generated from the user
input. Those that are valid are displayed to the user. The user
may then select the barcode image which matches what the user wants.

Italian translations provided by Michael Moroni (Airon90)
Dutch translations provided by PanderMusubi
2016-05-23 08:43:52 -04:00
Branden Archer
8edc9ce5fd Set barcode image visibility upon completion
If the barcode generation succeeds set it as visible, otherwise
make it gone.
2016-05-21 22:58:08 -04:00
Branden Archer
a3a5a3a8db Remove GlobalLayoutListener when no longer needed 2016-05-21 22:58:08 -04:00
Branden Archer
793247a48c Remove unused context 2016-05-21 22:58:08 -04:00
Branden Archer
2f86de4c1b Protect against unexpected failures when encoding barcodes
It was observed that some barcode encoders will fail if the
data passed to them is not valid for the format. For example,
the ITF encoder will throw an ArrayIndexOutOfBoundsException
on the input "this is a test".
2016-05-21 22:58:08 -04:00
Branden Archer
0aa1804258 Allow home button to back out to previous activity 2016-05-21 22:58:08 -04:00
Branden Archer
1c8ef34b8a Remove unneeded parentActivtyName attributes 2016-05-21 22:58:08 -04:00
Branden Archer
3fd45af7d9 Add dependency on Guava 2016-05-21 22:58:08 -04:00
Branden Archer
227af54de7 Merge pull request #43 from brarcher/datamatrix-scaling
Datamatrix scaling
2016-05-21 18:46:57 -04:00
Branden Archer
b89c5eb91c Change padding of card list on main screen
The entire space for the ListView was not being used, which
appeared odd.
2016-05-21 18:32:29 -04:00
Branden Archer
cecec15762 Scale barcodes to ImageView's size without filtering
It turns out that the library used to create datamatrix barcodes
returns the smallest image necessary to contain the barcode. That
is, the size passed into the barcode writer. If the ImageView
scales the tiny image itself into the full size it will use
bi-linear filtering, which results in a blurry barcode.

To avoid this, if scaling is needed do so without using filtering.
2016-05-21 18:32:29 -04:00
Branden Archer
dc4a41088c Generate barcode after ImageView size is known
The size of the ImageView may not yet be known when the
barcode generation is needed. If this is the case, wait
until the final layout is complete then start the
barcode generation.
2016-05-21 18:32:29 -04:00
Branden Archer
b91d4c934a Generate barcode in an AsyncTask
This change moves the generation of the barcode into its
own async task. In addition, the size of the ImageView is
used to determine the barcode size to use.

There will be cases when the size of the ImageView will not
be known when the barcode generation starts. This will be resolved
in a future commit.
2016-05-21 18:32:29 -04:00
Branden Archer
b8d4eaacad Merge pull request #42 from brarcher/min-sdk-11
Reduce min SDK version to 11
2016-05-17 18:23:11 -04:00
Branden Archer
0a7d5d89cf Reduce min SDK version to 11
The selection of SDK 17 was arbitrarily based on the version
available on my device at the time. As no APIs are being used
at that level, a lower SDK version can be targeted.

According to the current distribution of Android device versions,
99.9% of devices are at SDK 11+. Changing to this for the min SDK
for now.
2016-05-17 14:30:40 -04:00
Branden Archer
7bbcc52ba6 Remove usage of emulator in Travis-CI
The emulator was never used, no reason to download it on every build
2016-05-16 23:26:31 -04:00
Branden Archer
14972c9cc9 Merge pull request #40 from brarcher/pre-v0.5
Update for release v0.5
2016-05-16 21:15:18 -04:00
Branden Archer
213d1f64a6 Update for release v0.5 2016-05-16 21:06:29 -04:00
Branden Archer
f4f22055fc Merge pull request #38 from brarcher/import-export
Add support for backing up cards to CSV file on external storage
2016-05-16 21:00:38 -04:00
Branden Archer
b99704a3d8 Add an About page
This about page layout is borrowed from K-9 Mail:
https://github.com/k9mail

Italian translations provided by Airon90.
Dutch translations provided by PanderMusubi.
2016-05-16 20:51:59 -04:00
Branden Archer
715d726ea3 Update Gradle to 2.10 2016-05-16 20:51:59 -04:00
Branden Archer
8b887a2ee9 Update Gradle Android plugin to 2.0.0 2016-05-16 20:51:59 -04:00
Branden Archer
0248df532a Use gradle wrapper script on Travis-CI 2016-05-16 20:51:59 -04:00
Branden Archer
8c83b85cea Request external storage permission if necessary
Starting on Android 6.0 permissions may be requested at
runtime. To allow import/exporting on 6.0+, request permission
to external storage.
2016-05-16 20:51:59 -04:00
Branden Archer
b8f3d891ea Allow user to import/export cards to/from CSV on external storage
Italian translations provided by Airon90.
Dutch translations provided by PanderMusubi
2016-05-16 20:51:59 -04:00
Branden Archer
20338eb09b Add task for importing/exporting 2016-05-16 20:51:59 -04:00
Branden Archer
ce272fe7f1 Add ability to import/export cards to/from CSV 2016-05-16 20:51:59 -04:00
Branden Archer
fb34fb7451 Add call for writing card entry to specific id
This will be necessary when importing card entries later
2016-05-16 20:51:59 -04:00
Branden Archer
7b2d022e92 Merge pull request #35 from brarcher/gradle-wrappers
Add wrapper scripts and instructions for building
2016-04-22 09:27:49 -04:00
Branden Archer
283858a6e3 Add wrapper scripts and instructions for building 2016-04-22 08:58:38 -04:00
Branden Archer
d46b3c0810 Merge pull request #31 from brarcher/note-translation
Update Italian translation for note
2016-04-12 09:04:54 -04:00
Branden Archer
a2ff1a9e05 Update Italian translation for note
Airon90 mentioned that although "Nota" is Italian for "Note", it would be better to use the plural
form "Note" here.
2016-04-12 08:58:05 -04:00
Branden Archer
b2e333c379 Merge pull request #30 from brarcher/pre-v0.4
Update for release v0.4
2016-04-09 22:26:30 -04:00
Branden Archer
2b058ee766 Update for release v0.4 2016-04-09 22:21:23 -04:00
Branden Archer
fc3302219e Merge pull request #29 from brarcher/note-field
Add a note field
2016-04-09 22:18:35 -04:00
Branden Archer
e0a77e9628 display store and note on card summary view
Dutch translation provided by PanderMusubi

Related issues:
https://github.com/brarcher/loyalty-card-locker/issues/23
https://github.com/brarcher/loyalty-card-locker/issues/24
2016-04-09 22:11:20 -04:00
Branden Archer
f6f749de1c Use multi exception catch for idential exceptions 2016-03-20 18:05:51 -04:00
Branden Archer
61dec10e74 Do not overwrite store field when capturing barcode 2016-03-20 18:05:51 -04:00
Branden Archer
9306e1a3d9 Add ability to store a note with loyalty card 2016-03-20 18:05:51 -04:00
Branden Archer
62e4fa402c split parts of test into separate methods
in the future additional tests will be added, and this will
allow for more reuse
2016-03-20 18:05:51 -04:00
Branden Archer
d12edd06a4 Merge pull request #28 from brarcher/dutch
Add Dutch translations
2016-03-16 23:02:28 -04:00
Branden Archer
4a8bc4744d Add Dutch translations
These are provided by PanderMusubi

https://github.com/brarcher/loyalty-card-locker/issues/27
2016-03-16 22:54:31 -04:00
Branden Archer
34faf18deb Merge pull request #26 from brarcher/edit-name
Edit name
2016-03-12 21:26:18 -05:00
Branden Archer
8c371751a7 Allow store name to be editable after creation 2016-03-11 21:53:38 -05:00
Branden Archer
c21c81f2c7 Correct names of unit tests
Some of the tests mentioned "Gift" cards, but should mention
"Loyalty" cards instead. Additionally, made one unit test's
intention more clear.
2016-03-11 21:53:38 -05:00
Branden Archer
77afaf1a3d Merge pull request #22 from brarcher/findbugs
enable findbugs in travis-ci build
2016-02-20 16:29:24 -05:00
Branden Archer
eb61f81b52 cat unit test failure report if failures found 2016-02-20 13:47:30 -05:00
Branden Archer
0289720c09 enable findbugs during travis-ci build 2016-02-20 13:46:53 -05:00
Branden Archer
5f2e3a5e1d Change inner class to be static 2016-02-20 13:31:34 -05:00
Branden Archer
5bc992f7f8 Merge pull request #15 from brarcher/readme
mention barcode types in README.md
2016-02-11 06:32:17 -05:00
Branden Archer
a2ab434d01 Merge branch 'master' into readme 2016-02-11 01:36:03 -05:00
Branden Archer
9593b94a2b Merge pull request #16 from brarcher/pre-v0.3
update version for next release
2016-02-11 01:35:56 -05:00
Branden Archer
075a440e4f update version for next release 2016-02-11 01:30:12 -05:00
Branden Archer
c6967f6a3f mention barcode types in README.md 2016-02-11 01:29:30 -05:00
Branden Archer
a8ca5470c7 Merge pull request #14 from brarcher/update-zxing-android-embedded
update zxing-android-embedded to latest release
2016-02-11 01:22:41 -05:00
Branden Archer
4d277eff51 update zxing-android-embedded to latest release
there was an issue with requesting camera permission
for Android 6 which was resolved in release 3.2.0.
2016-02-11 01:00:02 -05:00
Branden Archer
c51531e367 Merge pull request #13 from brarcher/all-barcodes
All barcodes
2016-02-10 21:49:21 -05:00
Branden Archer
5e57d66daa Do not bring up keyboard in view activity
Typically one will open the view activity to bring up a
barcode, and the keyboard is not expected. Disable the
keyboard until an editable field is selected.
2016-02-10 21:42:57 -05:00
Branden Archer
73aff4aeb8 Support all barcodes zxing can generate 2016-02-10 21:42:57 -05:00
Branden Archer
e6241b5a47 Update to using latest zxing library release, 3.2.1 2016-02-10 21:42:57 -05:00
Branden Archer
dd0bb18234 Merge pull request #9 from brarcher/pre-v0.2
Update version information for release
2016-02-07 22:00:49 -05:00
Branden Archer
35c778eadc Update version information for release 2016-02-07 21:56:02 -05:00
Branden Archer
63e0356ddd Merge pull request #8 from brarcher/camera-permission
Camera permission
2016-02-07 15:51:10 -05:00
Branden Archer
9820bca315 declare usage of the auto focus feature for the camera
The zxing library will attempt to use auto focus on the camera,
if available.
2016-02-07 15:38:07 -05:00
Branden Archer
2e457dc06e Add permission for using camera
The zxing library will need this permission to work
correctly.
2016-02-07 15:37:40 -05:00
Branden Archer
8bfd0f1215 Use multiple lines to declare features and permissions
This is for readability.
2016-02-07 15:37:15 -05:00
Branden Archer
cdbbf9222a Merge pull request #7 from brarcher/1d-barcodes
Support scanning all types of 1D barcodes
2016-02-03 15:16:47 -05:00
Branden Archer
96c665a539 Support scanning all types of 1D barcodes
There are loyalty cards which are not product codes but are 1D
barcodes. Enabling support for all types of 1D barcodes to
enable more types of loyalty cards.
2016-02-03 15:09:54 -05:00
Branden Archer
c0e8a33605 Merge pull request #6 from brarcher/readme
Add F-Droid icon and fix grammer in README.md
2016-02-02 22:51:48 -05:00
Branden Archer
fb96dfc8ac Add F-Droid icon and fix grammer in README.md 2016-02-02 22:27:20 -05:00
Branden Archer
41590706b5 Merge pull request #2 from Airon90/patch-1
Create strings.xml for Italian translation
2016-01-31 14:24:15 -05:00
Michael Moroni
59a7976ad5 Update strings.xml
fix
2016-01-31 14:35:16 +01:00
Michael Moroni
281fc30418 Create strings.xml
Added Italian translation
2016-01-31 14:09:37 +01:00
129 changed files with 5174 additions and 414 deletions

View File

@@ -1,25 +1,16 @@
language: android
android:
components:
# Uncomment the lines below if you want to
# use the latest revision of Android SDK Tools
- platform-tools
- tools
sudo: true
# The BuildTools version used by your project
- build-tools-23.0.2
install:
- echo y | android update sdk -u -a -t tools
- echo y | android update sdk -u -a -t platform-tools
- echo y | android update sdk -u -a -t build-tools-25.0.2
- echo y | android update sdk -u -a -t android-25
- echo y | android update sdk -u -a -t extra-google-m2repository
- echo y | android update sdk -u -a -t extra-android-m2repository
# The SDK version used to compile your project
- android-23
# Additional components
- extra
# Specify at least one system image,
# if you need to run emulator(s) during your tests
- sys-img-x86-android-17
script: gradle build lint test
script: ./gradlew assembleRelease testReleaseUnitTest lintRelease findbugs
after_failure:
- cat app/build/outputs/lint-results.xml
- cat app/build/reports/findbugs/findbugs.html
- cat app/build/reports/tests/debug/index.html

120
CHANGELOG.md Normal file
View File

@@ -0,0 +1,120 @@
## v0.14 (2017-10-26)
Changes:
- Add support for app shortcuts (Android 7.1+), where the most recently used cards will appear as shortcuts. (https://github.com/brarcher/loyalty-card-locker/pull/145)
- Add a widget which works like a pinned app shortcut, to support devices which run below Android 7.1. (https://github.com/brarcher/loyalty-card-locker/pull/142)
## v0.13 (2017-07-25)
Changes:
- Add screen rotation lock menu option when displaying a card. If locked, the screen will transition to its "natural" orientation and further screen rotation will be blocked. (https://github.com/brarcher/loyalty-card-locker/pull/128)
- If a card is selected from the main screen but cannot be loaded, the application fails gracefully and posts a message. (https://github.com/brarcher/loyalty-card-locker/pull/132)
- Fix case where layout IDs for intro wizard could not be found. (https://github.com/brarcher/loyalty-card-locker/pull/128)
## v0.12 (2017-07-16)
Changes:
- A change in v0.11 reduced the memory usage of barcode drawing, but affected the barcode dimensions. This is now changed to maintain the barcode dimensions while reducing memory usage. (https://github.com/brarcher/loyalty-card-locker/pull/126)
- Update German and French translations. (https://github.com/brarcher/loyalty-card-locker/pull/122, https://github.com/brarcher/loyalty-card-locker/pull/124, https://github.com/brarcher/loyalty-card-locker/pull/125)
## v0.11.1 (2017-06-29)
Changes:
- Prevent a crash when rotation the screen in the first run intro wizard.
## v0.11 (2017-06-26)
Improvements:
- When editing a card ID, pre-populate the existing ID to start. (https://github.com/brarcher/loyalty-card-locker/pull/94)
- Limit the width of generated barcodes to reduce memory usage and out of memory errors. (https://github.com/brarcher/loyalty-card-locker/pull/103)
- When editing a card, change the "Enter Card" button to say "Edit Card" if a card ID already exists. (https://github.com/brarcher/loyalty-card-locker/pull/104)
- Change the color scheme to be softer and compatible with the app icon, and change the layout when viewing a card to be cleaner. (https://github.com/brarcher/loyalty-card-locker/pull/107)
- Add an intro wizard which launches on the app's first launch. (https://github.com/brarcher/loyalty-card-locker/pull/108)
## v0.10 (2017-02-12)
Improvements:
- Changed the default import/export filename. (https://github.com/brarcher/loyalty-card-locker/pull/84)
- Correct string on the import/export page. (https://github.com/brarcher/loyalty-card-locker/pull/87)
- Improve layout of card view page. The text should be easier to read, and is selectable with a long click. (https://github.com/brarcher/loyalty-card-locker/pull/91)
## v0.9 (2017-01-17)
The "Locker" part of the name was not intuitive. To help remedy this a new application icon was created by betsythefc which better represents the purpose of the application: to store loyalty cards which use barcodes. Along with this new icon the name of the application has been changed to "Loyalty Card Keychain".
Additional features/improvements:
- Importing/Exporting cards was changed to be more flexible. (https://github.com/brarcher/loyalty-card-locker/pull/76)
- Translations for Lithuanian added. (https://github.com/brarcher/loyalty-card-locker/pull/62)
- Translations for French added. (https://github.com/brarcher/loyalty-card-locker/pull/80)
## v0.8 (2016-11-22)
New features/improvements:
- Screen brightness increased to its maximum when displaying a card, to help barcode scanners successfully capture the barcode. (https://github.com/brarcher/loyalty-card-locker/pull/54)
- Add a delete confirmation when deleting a card. (https://github.com/brarcher/loyalty-card-locker/pull/55)
- Add translations for German (https://github.com/brarcher/loyalty-card-locker/pull/57) and Czech (https://github.com/brarcher/loyalty-card-locker/pull/58).
- Clarification change for Italian translation. (https://github.com/brarcher/loyalty-card-locker/pull/66)
## v0.7 (2016-07-14)
New features/improvements:
- Long-click of a card brings up option to copy card ID to the clipboard. (https://github.com/brarcher/loyalty-card-locker/issues/49)
Bug fixes:
- Back button on Input/Export view now works, moving user to main view
## v0.6 (2016-05-23)
New features/improvements:
- Allow user to enter barcode manually. If a user elects to enter a barcode manually, a list of all valid and supported barcode images is displayed. The user then may select the barcode image which matches what the user wants. https://github.com/brarcher/loyalty-card-locker/issues/33, https://github.com/brarcher/loyalty-card-locker/pull/44
Bug fixes:
- Resolve issue where some displayed barcodes were blurry. (https://github.com/brarcher/loyalty-card-locker/issues/37)
## v0.5 (2016-05-16)
New features/improvements:
- An about dialog can be opened from the main screen, which gives details about the application and project on GitHub (https://github.com/brarcher/loyalty-card-locker/issues/19)
- Allow loyalty card information to be imported from/exported to a CSV file in external storage (https://github.com/brarcher/loyalty-card-locker/issues/36 https://github.com/brarcher/loyalty-card-locker/issues/20)
## v0.4 (2016-04-09)
New features/improvements:
- Dutch translation
- Allow name field to be editable after adding loyalty card
- Add an optional note field
Bug fixes:
- Resolve all issues identified by FindBugs and require all FindBugs issues be resolved prior to pull request acceptance
## v0.3 (2016-02-11)
- Now officially supports the following list of 1D and 2D barcodes:
* AZTEC
* CODABAR
* CODE_39
* CODE_128
* DATA_MATRIX
* EAN_8
* EAN_13
* ITF
* PDF_417
* QR_CODE
* UPC_A
- Generated barcodes are larger, easier to scan from a scanning device
## v0.2 (2016-02-07)
- Italian translations
- Support for all 1D barcode types. (Originally only product 1D barcodes were supported)
- Add required camera permission, which was initially missing.
## v0.1 (2016-01-30)
- Ability to create/edit/delete loyalty cards
- Capture barcode of loyalty card using a camera
- Display captured barcode, for scanning at a store

View File

@@ -1,12 +1,58 @@
# Loyalty Card Locker
# Loyalty Card Keychain
[![Build Status](https://travis-ci.org/brarcher/loyalty-card-locker.svg?branch=master)](https://travis-ci.org/brarcher/loyalty-card-locker)
Stores all of your store loyalty cards on your phone, removing the need to carry them around.
<a href="https://f-droid.org/repository/browse/?fdid=protect.card_locker" target="_blank">
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="90"/></a>
<a href="https://play.google.com/store/apps/details?id=protect.card_locker" target="_blank">
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="90"/></a>
Stores all of your store loyalty cards on your phone, removing the need to carry them around. Currently the following barcode types are supported:
- AZTEC
- CODABAR
- CODE_39
- CODE_128
- DATA_MATRIX
- EAN_8
- EAN_13
- ITF
- PDF_417
- QR_CODE
- UPC_A
If there is any interest in improving this project, kindly submit a pull request with
proposed changed.
proposed changes.
# Screenshots
[<img src="https://user-images.githubusercontent.com/5264535/27416124-79b09162-56d9-11e7-967b-8923177dc228.png" width=250>](https://user-images.githubusercontent.com/5264535/27416124-79b09162-56d9-11e7-967b-8923177dc228.png)
[<img src="https://user-images.githubusercontent.com/5264535/27416127-7baea332-56d9-11e7-8a10-5be90bb02225.png" width=250>](https://user-images.githubusercontent.com/5264535/27416127-7baea332-56d9-11e7-8a10-5be90bb02225.png)
[<img src="https://user-images.githubusercontent.com/5264535/27416128-7d50f7b2-56d9-11e7-9833-1dd962f9cf66.png" width=250>](https://user-images.githubusercontent.com/5264535/27416128-7d50f7b2-56d9-11e7-9833-1dd962f9cf66.png)
[<img src="https://user-images.githubusercontent.com/5264535/27416132-7ea6272c-56d9-11e7-9a52-d73424bf902c.png" width=250>](https://user-images.githubusercontent.com/5264535/27416132-7ea6272c-56d9-11e7-9a52-d73424bf902c.png)
[<img src="https://user-images.githubusercontent.com/5264535/27416137-800aee90-56d9-11e7-9cc9-2a7dc63bb4fb.png" width=250>](https://user-images.githubusercontent.com/5264535/27416137-800aee90-56d9-11e7-9cc9-2a7dc63bb4fb.png)
[<img src="https://user-images.githubusercontent.com/5264535/27416140-82d8211a-56d9-11e7-8031-c71d3077bdc6.png" width=250>](https://user-images.githubusercontent.com/5264535/27416140-82d8211a-56d9-11e7-8031-c71d3077bdc6.png)
# Building
To build, use the gradle wrapper scripts provided in the top level directory of the project. The following will
compile the application and run all unit tests:
GNU/Linux, OSX, UNIX:
```
./gradlew build
```
Windows:
```
./gradlew.bat build
```
# Translating
If you are interested in translating this application to another language, create a pull request with changes or find the project listing on [Transifex](https://www.transifex.com/na-243/loyalty-card-locker).
# Thanks
App icon originals by [Freepik](https://www.flaticon.com) and distributed under the [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/) license,
and formatted using [Android Asset Studio](https://romannurik.github.io/AndroidAssetStudio/index.html).
This application uses the following image:
- [Save](https://thenounproject.com/term/save/716011) by [Bernar Novalyi](https://thenounproject.com/bernar.novalyi)

View File

@@ -1,15 +1,21 @@
apply plugin: 'com.android.application'
apply plugin: 'findbugs'
findbugs {
sourceSets = []
ignoreFailures = false
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "protect.card_locker"
minSdkVersion 17
targetSdkVersion 23
versionCode 1
versionName "0.1"
targetSdkVersion 25
versionCode 15
versionName "0.14"
}
buildTypes {
release {
@@ -21,6 +27,7 @@ android {
disable "GoogleAppIndexingWarning"
disable "ButtonStyle"
disable "AlwaysShowAction"
disable "MissingTranslation"
}
// This is for Robolectric support for SDK 23
@@ -29,10 +36,32 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.journeyapps:zxing-android-embedded:3.0.1@aar'
compile 'com.google.zxing:core:3.2.0'
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
compile 'com.google.zxing:core:3.3.0'
compile 'org.apache.commons:commons-csv:1.2'
compile group: 'com.google.guava', name: 'guava', version: '20.0'
compile 'com.github.apl-devs:appintro:v4.2.0'
testCompile 'junit:junit:4.12'
testCompile "org.robolectric:robolectric:3.0"
testCompile "org.robolectric:robolectric:3.3.2"
}
task findbugs(type: FindBugs, dependsOn: assembleDebug) {
description 'Run findbugs'
group 'verification'
classes = fileTree('build/intermediates/classes/debug/')
source = fileTree('src/main/java')
classpath = files()
effort = 'max'
excludeFilter = file("./config/findbugs/exclude.xml")
reports {
xml.enabled = false
html.enabled = true
}
}

View File

@@ -0,0 +1,10 @@
<FindBugsFilter>
<Match>
<Class name="~.*R\$.*"/>
</Match>
<Match>
<Class name="~.*Manifest\$.*"/>
</Match>
</FindBugsFilter>

View File

@@ -2,8 +2,19 @@
<manifest package="protect.card_locker"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:required="true" android:name="android.hardware.camera"/>
<uses-permission android:maxSdkVersion="18" android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.CAMERA"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<application
android:allowBackup="false"
@@ -25,7 +36,37 @@
android:name=".LoyaltyCardViewActivity"
android:theme="@style/AppTheme.NoActionBar"
android:configChanges="orientation|screenSize"
android:parentActivityName="protect.card_locker.MainActivity"/>
android:windowSoftInputMode="stateHidden"/>
<activity
android:name=".BarcodeSelectorActivity"
android:label="@string/selectBarcodeTitle"
android:theme="@style/AppTheme.NoActionBar"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="stateHidden"/>
<activity
android:name=".ImportExportActivity"
android:label="@string/importExport"
android:configChanges="orientation|screenSize"
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".intro.IntroActivity"
android:label=""
android:configChanges="orientation|screenSize"
android:theme="@style/AppTheme.NoActionBar"/>
<receiver android:name=".appwidget.CardAppWidgetProvider">
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
<activity android:name=".appwidget.CardAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,143 @@
package protect.card_locker;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import java.lang.ref.WeakReference;
/**
* This task will generate a barcode and load it into an ImageView.
* Only a weak reference of the ImageView is kept, so this class will not
* prevent the ImageView from being garbage collected.
*/
class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
{
private static final String TAG = "LoyaltyCardLocker";
private static final int MAX_WIDTH = 500;
private final WeakReference<ImageView> imageViewReference;
private final String cardId;
private final BarcodeFormat format;
private final int imageHeight;
private final int imageWidth;
BarcodeImageWriterTask(ImageView imageView, String cardIdString,
BarcodeFormat barcodeFormat)
{
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<>(imageView);
cardId = cardIdString;
format = barcodeFormat;
if(imageView.getWidth() < MAX_WIDTH)
{
imageHeight = imageView.getHeight();
imageWidth = imageView.getWidth();
}
else
{
// Scale down the image to reduce the memory needed to produce it
imageWidth = MAX_WIDTH;
double ratio = (double)MAX_WIDTH / (double)imageView.getWidth();
imageHeight = (int)(imageView.getHeight() * ratio);
}
}
public Bitmap doInBackground(Void... params)
{
MultiFormatWriter writer = new MultiFormatWriter();
BitMatrix bitMatrix;
try
{
try
{
bitMatrix = writer.encode(cardId, format, imageWidth, imageHeight, null);
}
catch(Exception e)
{
// Cast a wider net here and catch any exception, as there are some
// cases where an encoder may fail if the data is invalid for the
// barcode type. If this happens, we want to fail gracefully.
throw new WriterException(e);
}
final int WHITE = 0xFFFFFFFF;
final int BLACK = 0xFF000000;
int bitMatrixWidth = bitMatrix.getWidth();
int bitMatrixHeight = bitMatrix.getHeight();
int[] pixels = new int[bitMatrixWidth * bitMatrixHeight];
for (int y = 0; y < bitMatrixHeight; y++)
{
int offset = y * bitMatrixWidth;
for (int x = 0; x < bitMatrixWidth; x++)
{
int color = bitMatrix.get(x, y) ? BLACK : WHITE;
pixels[offset + x] = color;
}
}
Bitmap bitmap = Bitmap.createBitmap(bitMatrixWidth, bitMatrixHeight,
Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, bitMatrixWidth, 0, 0, bitMatrixWidth, bitMatrixHeight);
// Determine if the image needs to be scaled.
// This is necessary because the datamatrix barcode generator
// ignores the requested size and returns the smallest image necessary
// to represent the barcode. If we let the ImageView scale the image
// it will use bi-linear filtering, which results in a blurry barcode.
// To avoid this, if scaling is needed do so without filtering.
int heightScale = imageHeight / bitMatrixHeight;
int widthScale = imageWidth / bitMatrixHeight;
int scalingFactor = Math.min(heightScale, widthScale);
if(scalingFactor > 1)
{
bitmap = Bitmap.createScaledBitmap(bitmap, bitMatrixWidth * scalingFactor, bitMatrixHeight * scalingFactor, false);
}
return bitmap;
}
catch (WriterException e)
{
Log.e(TAG, "Failed to generate barcode of type " + format + ": " + cardId, e);
}
return null;
}
protected void onPostExecute(Bitmap result)
{
Log.i(TAG, "Finished generating barcode image of type " + format + ": " + cardId);
ImageView imageView = imageViewReference.get();
if(imageView == null)
{
// The ImageView no longer exists, nothing to do
return;
}
imageView.setImageBitmap(result);
if(result != null)
{
Log.i(TAG, "Displaying barcode");
imageView.setVisibility(View.VISIBLE);
}
else
{
Log.i(TAG, "Barcode generation failed, removing image from display");
imageView.setVisibility(View.GONE);
}
}
}

View File

@@ -0,0 +1,209 @@
package protect.card_locker;
import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.EditText;
import android.widget.ImageView;
import com.google.common.collect.ImmutableMap;
import com.google.zxing.BarcodeFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Map;
/**
* This activity is callable and will allow a user to enter
* barcode data and generate all barcodes possible for
* the data. The user may then select any barcode, where its
* data and type will be returned to the caller.
*/
public class BarcodeSelectorActivity extends AppCompatActivity
{
private static final String TAG = "LoyaltyCardLocker";
// Result this activity will return
public static final String BARCODE_CONTENTS = "contents";
public static final String BARCODE_FORMAT = "format";
// These are all the barcode types that the zxing library
// is able to generate a barcode for, and thus should be
// the only barcodes which we should attempt to scan.
public static final Collection<String> SUPPORTED_BARCODE_TYPES = Collections.unmodifiableList(
Arrays.asList(
BarcodeFormat.AZTEC.name(),
BarcodeFormat.CODE_39.name(),
BarcodeFormat.CODE_128.name(),
BarcodeFormat.CODABAR.name(),
BarcodeFormat.DATA_MATRIX.name(),
BarcodeFormat.EAN_8.name(),
BarcodeFormat.EAN_13.name(),
BarcodeFormat.ITF.name(),
BarcodeFormat.PDF_417.name(),
BarcodeFormat.QR_CODE.name(),
BarcodeFormat.UPC_A.name()
));
private Map<String, Integer> barcodeViewMap;
private LinkedList<AsyncTask> barcodeGeneratorTasks = new LinkedList<>();
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.barcode_selector_activity);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if(actionBar != null)
{
actionBar.setDisplayHomeAsUpEnabled(true);
}
barcodeViewMap = ImmutableMap.<String, Integer>builder()
.put(BarcodeFormat.AZTEC.name(), R.id.aztecBarcode)
.put(BarcodeFormat.CODE_39.name(), R.id.code39Barcode)
.put(BarcodeFormat.CODE_128.name(), R.id.code128Barcode)
.put(BarcodeFormat.CODABAR.name(), R.id.codabarBarcode)
.put(BarcodeFormat.DATA_MATRIX.name(), R.id.datamatrixBarcode)
.put(BarcodeFormat.EAN_8.name(), R.id.ean8Barcode)
.put(BarcodeFormat.EAN_13.name(), R.id.ean13Barcode)
.put(BarcodeFormat.ITF.name(), R.id.itfBarcode)
.put(BarcodeFormat.PDF_417.name(), R.id.pdf417Barcode)
.put(BarcodeFormat.QR_CODE.name(), R.id.qrcodeBarcode)
.put(BarcodeFormat.UPC_A.name(), R.id.upcaBarcode)
.build();
EditText cardId = (EditText) findViewById(R.id.cardId);
cardId.addTextChangedListener(new TextWatcher()
{
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after)
{
// Noting to do
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count)
{
Log.d(TAG, "Entered text: " + s);
// Stop any async tasks which may not have been started yet
for(AsyncTask task : barcodeGeneratorTasks)
{
task.cancel(false);
}
barcodeGeneratorTasks.clear();
// Update barcodes
for(String key : barcodeViewMap.keySet())
{
ImageView image = (ImageView)findViewById(barcodeViewMap.get(key));
createBarcodeOption(image, key, s.toString());
}
}
@Override
public void afterTextChanged(Editable s)
{
// Noting to do
}
});
final Bundle b = getIntent().getExtras();
final String initialCardId = b != null ? b.getString("initialCardId") : null;
if(initialCardId != null)
{
cardId.setText(initialCardId);
}
}
private void createBarcodeOption(final ImageView image, final String formatType, final String cardId)
{
final BarcodeFormat format = BarcodeFormat.valueOf(formatType);
if(format == null)
{
Log.w(TAG, "Unsupported barcode format: " + formatType);
return;
}
image.setImageBitmap(null);
image.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Log.d(TAG, "Selected barcode type " + formatType);
Intent result = new Intent();
result.putExtra(BARCODE_FORMAT, formatType);
result.putExtra(BARCODE_CONTENTS, cardId);
BarcodeSelectorActivity.this.setResult(RESULT_OK, result);
finish();
}
});
if(image.getHeight() == 0)
{
// The size of the ImageView is not yet available as it has not
// yet been drawn. Wait for it to be drawn so the size is available.
image.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener()
{
@Override
public void onGlobalLayout()
{
Log.d(TAG, "Global layout finished, type: + " + formatType + ", width: " + image.getWidth());
if (Build.VERSION.SDK_INT < 16)
{
image.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
else
{
image.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
Log.d(TAG, "Generating barcode for type " + formatType);
BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format);
barcodeGeneratorTasks.add(task);
task.execute();
}
});
}
else
{
Log.d(TAG, "Generating barcode for type " + formatType);
BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format);
barcodeGeneratorTasks.add(task);
task.execute();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == android.R.id.home)
{
setResult(Activity.RESULT_CANCELED);
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,50 @@
package protect.card_locker;
import android.database.Cursor;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import java.io.IOException;
import java.io.OutputStreamWriter;
/**
* Class for exporting the database into CSV (Comma Separate Values)
* format.
*/
public class CsvDatabaseExporter implements DatabaseExporter
{
public void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException
{
CSVPrinter printer = new CSVPrinter(output, CSVFormat.RFC4180);
// Print the header
printer.printRecord(DBHelper.LoyaltyCardDbIds.ID,
DBHelper.LoyaltyCardDbIds.STORE,
DBHelper.LoyaltyCardDbIds.NOTE,
DBHelper.LoyaltyCardDbIds.CARD_ID,
DBHelper.LoyaltyCardDbIds.BARCODE_TYPE);
Cursor cursor = db.getLoyaltyCardCursor();
while(cursor.moveToNext())
{
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor);
printer.printRecord(card.id,
card.store,
card.note,
card.cardId,
card.barcodeType);
if(Thread.currentThread().isInterrupted())
{
throw new InterruptedException();
}
}
cursor.close();
printer.close();
}
}

View File

@@ -0,0 +1,135 @@
package protect.card_locker;
import android.database.sqlite.SQLiteDatabase;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Class for importing a database from CSV (Comma Separate Values)
* formatted data.
*
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class CsvDatabaseImporter implements DatabaseImporter
{
public void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException
{
final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.withHeader());
SQLiteDatabase database = db.getWritableDatabase();
database.beginTransaction();
try
{
for (CSVRecord record : parser)
{
importLoyaltyCard(database, db, record);
if(Thread.currentThread().isInterrupted())
{
throw new InterruptedException();
}
}
parser.close();
database.setTransactionSuccessful();
}
catch(IllegalArgumentException e)
{
throw new FormatException("Issue parsing CSV data", e);
}
finally
{
database.endTransaction();
database.close();
}
}
/**
* Extract a string from the items array. The index into the array
* is determined by looking up the index in the fields map using the
* "key" as the key. If no such key exists, defaultValue is returned
* if it is not null. Otherwise, a FormatException is thrown.
*/
private String extractString(String key, CSVRecord record, String defaultValue)
throws FormatException
{
String toReturn = defaultValue;
if(record.isMapped(key))
{
toReturn = record.get(key);
}
else
{
if(defaultValue == null)
{
throw new FormatException("Field not used but expected: " + key);
}
}
return toReturn;
}
/**
* Extract an integer from the items array. The index into the array
* is determined by looking up the index in the fields map using the
* "key" as the key. If no such key exists, or the data is not a valid
* int, a FormatException is thrown.
*/
private int extractInt(String key, CSVRecord record)
throws FormatException
{
if(record.isMapped(key) == false)
{
throw new FormatException("Field not used but expected: " + key);
}
try
{
return Integer.parseInt(record.get(key));
}
catch(NumberFormatException e)
{
throw new FormatException("Failed to parse field: " + key, e);
}
}
/**
* Import a single loyalty card into the database using the given
* session.
*/
private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record)
throws IOException, FormatException
{
int id = extractInt(DBHelper.LoyaltyCardDbIds.ID, record);
String store = extractString(DBHelper.LoyaltyCardDbIds.STORE, record, "");
if(store.isEmpty())
{
throw new FormatException("No store listed, but is required");
}
String note = extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, "");
String cardId = extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, "");
if(cardId.isEmpty())
{
throw new FormatException("No card ID listed, but is required");
}
String barcodeType = extractString(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, record, "");
if(barcodeType.isEmpty())
{
throw new FormatException("No barcode type listed, but is required");
}
helper.insertLoyaltyCard(database, id, store, note, cardId, barcodeType);
}
}

View File

@@ -10,13 +10,15 @@ import android.database.sqlite.SQLiteOpenHelper;
public class DBHelper extends SQLiteOpenHelper
{
public static final String DATABASE_NAME = "LoyaltyCards.db";
public static final int DATABASE_VERSION = 1;
public static final int ORIGINAL_DATABASE_VERSION = 1;
public static final int DATABASE_VERSION = 2;
class LoyaltyCardDbIds
static class LoyaltyCardDbIds
{
public static final String TABLE = "cards";
public static final String ID = "_id";
public static final String STORE = "store";
public static final String NOTE = "note";
public static final String CARD_ID = "cardid";
public static final String BARCODE_TYPE = "barcodetype";
}
@@ -33,6 +35,7 @@ public class DBHelper extends SQLiteOpenHelper
db.execSQL("create table " + LoyaltyCardDbIds.TABLE + "(" +
LoyaltyCardDbIds.ID + " INTEGER primary key autoincrement," +
LoyaltyCardDbIds.STORE + " TEXT not null," +
LoyaltyCardDbIds.NOTE + " TEXT not null," +
LoyaltyCardDbIds.CARD_ID + " TEXT not null," +
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT not null)");
}
@@ -40,16 +43,35 @@ public class DBHelper extends SQLiteOpenHelper
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
// Do not support versioning yet
db.execSQL("DROP TABLE IF EXISTS " + LoyaltyCardDbIds.TABLE);
onCreate(db);
// Upgrade from version 1 to version 2
if(oldVersion < 2 && newVersion >= 2)
{
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
+ " ADD COLUMN " + LoyaltyCardDbIds.NOTE + " TEXT not null default ''");
}
}
public boolean insertLoyaltyCard(final String store, final String cardId, final String barcodeType)
public boolean insertLoyaltyCard(final String store, final String note, final String cardId,
final String barcodeType)
{
SQLiteDatabase db = getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(LoyaltyCardDbIds.STORE, store);
contentValues.put(LoyaltyCardDbIds.NOTE, note);
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType);
final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
return (newId != -1);
}
public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id,
final String store, final String note, final String cardId,
final String barcodeType)
{
ContentValues contentValues = new ContentValues();
contentValues.put(LoyaltyCardDbIds.ID, id);
contentValues.put(LoyaltyCardDbIds.STORE, store);
contentValues.put(LoyaltyCardDbIds.NOTE, note);
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType);
final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
@@ -57,12 +79,13 @@ public class DBHelper extends SQLiteOpenHelper
}
public boolean updateLoyaltyCard(final int id, final String store, final String cardId,
final String barcodeType)
public boolean updateLoyaltyCard(final int id, final String store, final String note,
final String cardId, final String barcodeType)
{
SQLiteDatabase db = getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(LoyaltyCardDbIds.STORE, store);
contentValues.put(LoyaltyCardDbIds.NOTE, note);
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType);
int rowsUpdated = db.update(LoyaltyCardDbIds.TABLE, contentValues,

View File

@@ -0,0 +1,8 @@
package protect.card_locker;
public enum DataFormat
{
CSV,
;
}

View File

@@ -0,0 +1,17 @@
package protect.card_locker;
import java.io.IOException;
import java.io.OutputStreamWriter;
/**
* Interface for a class which can export the contents of the database
* in a given format.
*/
public interface DatabaseExporter
{
/**
* Export the database to the output stream in a given format.
* @throws IOException
*/
void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException;
}

View File

@@ -0,0 +1,19 @@
package protect.card_locker;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Interface for a class which can import the contents of a stream
* into the database.
*/
public interface DatabaseImporter
{
/**
* Import data from the input stream in a given format into
* the database.
* @throws IOException
* @throws FormatException
*/
void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException;
}

View File

@@ -0,0 +1,19 @@
package protect.card_locker;
/**
* Exception thrown when something unexpected is
* encountered with the format of data being
* imported or exported.
*/
public class FormatException extends Exception
{
public FormatException(String message)
{
super(message);
}
public FormatException(String message, Exception rootCause)
{
super(message, rootCause);
}
}

View File

@@ -0,0 +1,377 @@
package protect.card_locker;
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.io.File;
import java.util.List;
public class ImportExportActivity extends AppCompatActivity
{
private static final String TAG = "LoyaltyCardLocker";
private static final int PERMISSIONS_EXTERNAL_STORAGE = 1;
private static final int CHOOSE_EXPORT_FILE = 2;
private ImportExportTask importExporter;
private final File sdcardDir = Environment.getExternalStorageDirectory();
private final File exportFile = new File(sdcardDir, "LoyaltyCardKeychain.csv");
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.import_export_activity);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if(actionBar != null)
{
actionBar.setDisplayHomeAsUpEnabled(true);
}
// If the application does not have permissions to external
// storage, ask for it now
if (ContextCompat.checkSelfPermission(ImportExportActivity.this,
Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(ImportExportActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
{
ActivityCompat.requestPermissions(ImportExportActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_EXTERNAL_STORAGE);
}
Button exportButton = (Button)findViewById(R.id.exportButton);
exportButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
startExport();
}
});
// Check that there is an activity that can bring up a file chooser
final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
intentPickAction.setData(Uri.parse("file://"));
Button importFilesystem = (Button) findViewById(R.id.importOptionFilesystemButton);
importFilesystem.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
chooseFileWithIntent(intentPickAction);
}
});
if(isCallable(getApplicationContext(), intentPickAction) == false)
{
findViewById(R.id.dividerImportFilesystem).setVisibility(View.GONE);
findViewById(R.id.importOptionFilesystemTitle).setVisibility(View.GONE);
findViewById(R.id.importOptionFilesystemExplanation).setVisibility(View.GONE);
importFilesystem.setVisibility(View.GONE);
}
// Check that there is an application that can find content
final Intent intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT);
intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE);
intentGetContentAction.setType("*/*");
Button importApplication = (Button) findViewById(R.id.importOptionApplicationButton);
importApplication.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
chooseFileWithIntent(intentGetContentAction);
}
});
if(isCallable(getApplicationContext(), intentGetContentAction) == false)
{
findViewById(R.id.dividerImportApplication).setVisibility(View.GONE);
findViewById(R.id.importOptionApplicationTitle).setVisibility(View.GONE);
findViewById(R.id.importOptionApplicationExplanation).setVisibility(View.GONE);
importApplication.setVisibility(View.GONE);
}
// This option, to import from the fixed location, should always be present
Button importButton = (Button)findViewById(R.id.importOptionFixedButton);
importButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
startImport(exportFile);
}
});
}
private void startImport(File target)
{
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener()
{
@Override
public void onTaskComplete(boolean success, File file)
{
onImportComplete(success, file);
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
true, DataFormat.CSV, target, listener);
importExporter.execute();
}
private void startExport()
{
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener()
{
@Override
public void onTaskComplete(boolean success, File file)
{
onExportComplete(success, file);
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
false, DataFormat.CSV, exportFile, listener);
importExporter.execute();
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)
{
if(requestCode == PERMISSIONS_EXTERNAL_STORAGE)
{
// If request is cancelled, the result arrays are empty.
boolean success = grantResults.length > 0;
for(int grant : grantResults)
{
if(grant != PackageManager.PERMISSION_GRANTED)
{
success = false;
}
}
if(success == false)
{
// External storage permission rejected, inform user that
// import/export is prevented
Toast.makeText(getApplicationContext(), R.string.noExternalStoragePermissionError,
Toast.LENGTH_LONG).show();
}
}
}
@Override
protected void onDestroy()
{
if(importExporter != null && importExporter.getStatus() != AsyncTask.Status.RUNNING)
{
importExporter.cancel(true);
}
super.onDestroy();
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
int id = item.getItemId();
if(id == android.R.id.home)
{
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void onImportComplete(boolean success, File path)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if(success)
{
builder.setTitle(R.string.importSuccessfulTitle);
}
else
{
builder.setTitle(R.string.importFailedTitle);
}
int messageId = success ? R.string.importedFrom : R.string.importFailed;
final String template = getResources().getString(messageId);
final String message = String.format(template, path.getAbsolutePath());
builder.setMessage(message);
builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
});
builder.create().show();
}
private void onExportComplete(boolean success, final File path)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if(success)
{
builder.setTitle(R.string.exportSuccessfulTitle);
}
else
{
builder.setTitle(R.string.exportFailedTitle);
}
int messageId = success ? R.string.exportedTo : R.string.exportFailed;
final String template = getResources().getString(messageId);
final String message = String.format(template, path.getAbsolutePath());
builder.setMessage(message);
builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
});
if(success)
{
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
builder.setPositiveButton(sendLabel, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
Uri outputUri = Uri.fromFile(path);
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, outputUri);
sendIntent.setType("text/plain");
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
sendLabel));
dialog.dismiss();
}
});
}
builder.create().show();
}
/**
* Determines if there is at least one activity that can perform the given intent
*/
private boolean isCallable(Context context, final Intent intent)
{
PackageManager manager = context.getPackageManager();
if(manager == null)
{
return false;
}
List<ResolveInfo> list = manager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for(ResolveInfo info : list)
{
if(info.activityInfo.exported)
{
// There is one activity which is available to be called
return true;
}
}
return false;
}
private void chooseFileWithIntent(Intent intent)
{
try
{
startActivityForResult(intent, CHOOSE_EXPORT_FILE);
}
catch (ActivityNotFoundException e)
{
Log.e(TAG, "No activity found to handle intent", e);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == CHOOSE_EXPORT_FILE)
{
String path = null;
Uri uri = data.getData();
if(uri != null && uri.toString().startsWith("/"))
{
uri = Uri.parse("file://" + uri.toString());
}
if(uri != null)
{
path = uri.getPath();
}
if(path != null)
{
Log.e(TAG, "Starting file import with: " + uri.toString());
startImport(new File(path));
}
else
{
Log.e(TAG, "Fail to make sense of URI returned from activity: " + (uri != null ? uri.toString() : "null"));
}
}
else
{
Log.w(TAG, "Failed onActivityResult(), result=" + resultCode);
}
}
}

View File

@@ -0,0 +1,136 @@
package protect.card_locker;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
class ImportExportTask extends AsyncTask<Void, Void, Boolean>
{
private static final String TAG = "LoyaltyCardLocker";
private Activity activity;
private boolean doImport;
private DataFormat format;
private File target;
private TaskCompleteListener listener;
private ProgressDialog progress;
public ImportExportTask(Activity activity, boolean doImport, DataFormat format, File target,
TaskCompleteListener listener)
{
super();
this.activity = activity;
this.doImport = doImport;
this.format = format;
this.target = target;
this.listener = listener;
}
private boolean performImport(File importFile, DBHelper db)
{
boolean result = false;
try
{
FileInputStream fileReader = new FileInputStream(importFile);
InputStreamReader reader = new InputStreamReader(fileReader, Charset.forName("UTF-8"));
result = MultiFormatImporter.importData(db, reader, format);
reader.close();
}
catch(IOException e)
{
Log.e(TAG, "Unable to import file", e);
}
Log.i(TAG, "Import of '" + importFile.getAbsolutePath() + "' result: " + result);
return result;
}
private boolean performExport(File exportFile, DBHelper db)
{
boolean result = false;
try
{
FileOutputStream fileWriter = new FileOutputStream(exportFile);
OutputStreamWriter writer = new OutputStreamWriter(fileWriter, Charset.forName("UTF-8"));
result = MultiFormatExporter.exportData(db, writer, format);
writer.close();
}
catch (IOException e)
{
Log.e(TAG, "Unable to export file", e);
}
Log.i(TAG, "Export of '" + exportFile.getAbsolutePath() + "' result: " + result);
return result;
}
protected void onPreExecute()
{
progress = new ProgressDialog(activity);
progress.setTitle(doImport ? R.string.importing : R.string.exporting);
progress.setOnDismissListener(new DialogInterface.OnDismissListener()
{
@Override
public void onDismiss(DialogInterface dialog)
{
ImportExportTask.this.cancel(true);
}
});
progress.show();
}
protected Boolean doInBackground(Void... nothing)
{
final DBHelper db = new DBHelper(activity);
boolean result;
if(doImport)
{
result = performImport(target, db);
}
else
{
result = performExport(target, db);
}
return result;
}
protected void onPostExecute(Boolean result)
{
listener.onTaskComplete(result, target);
progress.dismiss();
Log.i(TAG, (doImport ? "Import" : "Export") + " Complete");
}
protected void onCancelled()
{
progress.dismiss();
Log.i(TAG, (doImport ? "Import" : "Export") + " Cancelled");
}
interface TaskCompleteListener
{
void onTaskComplete(boolean success, File file);
}
}

View File

@@ -6,13 +6,15 @@ public class LoyaltyCard
{
public final int id;
public final String store;
public final String note;
public final String cardId;
public final String barcodeType;
public LoyaltyCard(final int id, final String store, final String cardId, final String barcodeType)
public LoyaltyCard(final int id, final String store, final String note, final String cardId, final String barcodeType)
{
this.id = id;
this.store = store;
this.note = note;
this.cardId = cardId;
this.barcodeType = barcodeType;
}
@@ -21,9 +23,10 @@ public class LoyaltyCard
{
int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE));
String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE));
String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID));
String barcodeType = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE));
return new LoyaltyCard(id, store, cardId, barcodeType);
return new LoyaltyCard(id, store, note, cardId, barcodeType);
}
}

View File

@@ -8,7 +8,7 @@ import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.TextView;
class LoyaltyCardCursorAdapter extends CursorAdapter
public class LoyaltyCardCursorAdapter extends CursorAdapter
{
public LoyaltyCardCursorAdapter(Context context, Cursor cursor)
{
@@ -36,7 +36,14 @@ class LoyaltyCardCursorAdapter extends CursorAdapter
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(cursor);
// Populate fields with extracted properties
storeField.setText(loyaltyCard.store);
String storeAndNote = loyaltyCard.store;
if(loyaltyCard.note.isEmpty() == false)
{
String storeNameAndNoteFormat = view.getResources().getString(R.string.storeNameAndNoteFormat);
storeAndNote = String.format(storeNameAndNoteFormat, loyaltyCard.store, loyaltyCard.note);
}
storeField.setText(storeAndNote);
String cardIdFormat = view.getResources().getString(R.string.cardIdFormat);
String cardIdLabel = view.getResources().getString(R.string.cardId);

View File

@@ -1,25 +1,33 @@
package protect.card_locker;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
@@ -28,6 +36,31 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
{
private static final String TAG = "CardLocker";
private static final int SELECT_BARCODE_REQUEST = 1;
EditText storeFieldEdit;
TextView storeFieldView;
EditText noteFieldEdit;
TextView noteFieldView;
TextView cardIdFieldView;
View cardIdDivider;
View cardIdTableRow;
TextView barcodeTypeField;
ImageView barcodeImage;
View barcodeImageLayout;
View barcodeCaptureLayout;
Button captureButton;
Button enterButton;
int loyaltyCardId;
boolean updateLoyaltyCard;
boolean viewLoyaltyCard;
boolean rotationEnabled;
DBHelper db;
@Override
protected void onCreate(Bundle savedInstanceState)
{
@@ -41,6 +74,17 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
{
actionBar.setDisplayHomeAsUpEnabled(true);
}
final Bundle b = getIntent().getExtras();
loyaltyCardId = b != null ? b.getInt("id") : 0;
updateLoyaltyCard = b != null && b.getBoolean("update", false);
viewLoyaltyCard = b != null && b.getBoolean("view", false);
Log.d(TAG, "View activity: id=" + loyaltyCardId
+ ", updateLoyaltyCard=" + Boolean.toString(updateLoyaltyCard)
+ ", viewLoyaltyCard=" + Boolean.toString(viewLoyaltyCard));
db = new DBHelper(this);
}
@Override
@@ -48,37 +92,63 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
{
super.onResume();
final Bundle b = getIntent().getExtras();
final int loyaltyCardId = b != null ? b.getInt("id") : 0;
final boolean updateLoyaltyCard = b != null && b.getBoolean("update", false);
final boolean viewLoyaltyCard = b != null && b.getBoolean("view", false);
Log.i(TAG, "To view card: " + loyaltyCardId);
final EditText storeField = (EditText) findViewById(R.id.storeName);
final EditText cardIdField = (EditText) findViewById(R.id.cardId);
final EditText barcodeTypeField = (EditText) findViewById(R.id.barcodeType);
final ImageView barcodeImage = (ImageView) findViewById(R.id.barcode);
final View barcodeIdLayout = findViewById(R.id.barcodeIdLayout);
final View barcodeTypeLayout = findViewById(R.id.barcodeTypeLayout);
final View barcodeImageLayout = findViewById(R.id.barcodeLayout);
final View barcodeCaptureLayout = findViewById(R.id.barcodeCaptureLayout);
if(viewLoyaltyCard)
{
// The brightness value is on a scale from [0, ..., 1], where
// '1' is the brightest. We attempt to maximize the brightness
// to help barcode readers scan the barcode.
Window window = getWindow();
if(window != null)
{
WindowManager.LayoutParams attributes = window.getAttributes();
attributes.screenBrightness = 1F;
window.setAttributes(attributes);
}
}
final Button captureButton = (Button) findViewById(R.id.captureButton);
final Button saveButton = (Button) findViewById(R.id.saveButton);
final Button cancelButton = (Button) findViewById(R.id.cancelButton);
storeFieldEdit = (EditText) findViewById(R.id.storeNameEdit);
storeFieldView = (TextView) findViewById(R.id.storeNameView);
noteFieldEdit = (EditText) findViewById(R.id.noteEdit);
noteFieldView = (TextView) findViewById(R.id.noteView);
cardIdFieldView = (TextView) findViewById(R.id.cardIdView);
cardIdDivider = findViewById(R.id.cardIdDivider);
cardIdTableRow = findViewById(R.id.cardIdTableRow);
barcodeTypeField = (TextView) findViewById(R.id.barcodeType);
barcodeImage = (ImageView) findViewById(R.id.barcode);
barcodeImageLayout = findViewById(R.id.barcodeLayout);
barcodeCaptureLayout = findViewById(R.id.barcodeCaptureLayout);
final DBHelper db = new DBHelper(this);
captureButton = (Button) findViewById(R.id.captureButton);
enterButton = (Button) findViewById(R.id.enterButton);
if(updateLoyaltyCard || viewLoyaltyCard)
{
final LoyaltyCard loyaltyCard = db.getLoyaltyCard(loyaltyCardId);
storeField.setText(loyaltyCard.store);
if(cardIdField.getText().length() == 0)
if(loyaltyCard == null)
{
cardIdField.setText(loyaltyCard.cardId);
Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId);
Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show();
finish();
return;
}
if(storeFieldEdit.getText().length() == 0)
{
storeFieldEdit.setText(loyaltyCard.store);
storeFieldView.setText(loyaltyCard.store);
}
if(noteFieldEdit.getText().length() == 0)
{
noteFieldEdit.setText(loyaltyCard.note);
noteFieldView.setText(loyaltyCard.note);
}
if(cardIdFieldView.getText().length() == 0)
{
cardIdFieldView.setText(loyaltyCard.cardId);
}
if(barcodeTypeField.getText().length() == 0)
@@ -86,82 +156,69 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
barcodeTypeField.setText(loyaltyCard.barcodeType);
}
storeField.setEnabled(false);
if(updateLoyaltyCard)
{
setTitle(R.string.editCardTitle);
storeFieldView.setVisibility(View.GONE);
noteFieldView.setVisibility(View.GONE);
}
else
{
barcodeCaptureLayout.setVisibility(View.GONE);
captureButton.setVisibility(View.GONE);
saveButton.setVisibility(View.GONE);
cancelButton.setVisibility(View.GONE);
setTitle(R.string.viewCardTitle);
storeFieldEdit.setVisibility(View.GONE);
noteFieldEdit.setVisibility(View.GONE);
}
}
else
{
setTitle(R.string.addCardTitle);
storeFieldView.setVisibility(View.GONE);
noteFieldView.setVisibility(View.GONE);
}
if(cardIdField.getText().length() == 0)
if(cardIdFieldView.getText().length() > 0 && barcodeTypeField.getText().length() > 0)
{
barcodeIdLayout.setVisibility(View.GONE);
}
String formatString = barcodeTypeField.getText().toString();
final BarcodeFormat format = BarcodeFormat.valueOf(formatString);
final String cardIdString = cardIdFieldView.getText().toString();
barcodeTypeLayout.setVisibility(View.GONE);
if(cardIdField.getText().length() > 0 && barcodeTypeField.getText().length() > 0)
{
MultiFormatWriter writer = new MultiFormatWriter();
BitMatrix result;
try
if(barcodeImage.getHeight() == 0)
{
String formatString = barcodeTypeField.getText().toString();
BarcodeFormat format = BarcodeFormat.valueOf(formatString);
if(format == null)
{
throw new IllegalArgumentException("Unrecognized barcode format: " + formatString);
}
String cardIdString = cardIdField.getText().toString();
Log.i(TAG, "Card: " + cardIdString);
result = writer.encode(cardIdString, format, 1500, 400, null);
final int WHITE = 0xFFFFFFFF;
final int BLACK = 0xFF000000;
int width = result.getWidth();
int height = result.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++)
{
int offset = y * width;
for (int x = 0; x < width; x++)
Log.d(TAG, "ImageView size is not known known at start, waiting for load");
// The size of the ImageView is not yet available as it has not
// yet been drawn. Wait for it to be drawn so the size is available.
barcodeImage.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener()
{
pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
barcodeImage.setImageBitmap(bitmap);
@Override
public void onGlobalLayout()
{
if (Build.VERSION.SDK_INT < 16)
{
barcodeImage.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
else
{
barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
barcodeIdLayout.setVisibility(View.VISIBLE);
barcodeImageLayout.setVisibility(View.VISIBLE);
Log.d(TAG, "ImageView size now known");
new BarcodeImageWriterTask(barcodeImage, cardIdString, format).execute();
}
});
}
catch (WriterException e)
else
{
Log.e(TAG, "Failed to generate barcode", e);
}
catch(IllegalArgumentException e)
{
Log.e(TAG, "Failed to generate barcode", e);
Log.d(TAG, "ImageView size known known, creating barcode");
new BarcodeImageWriterTask(barcodeImage, cardIdString, format).execute();
}
barcodeImageLayout.setVisibility(View.VISIBLE);
}
View.OnClickListener captureCallback = new View.OnClickListener()
@@ -170,7 +227,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
public void onClick(View v)
{
IntentIntegrator integrator = new IntentIntegrator(LoyaltyCardViewActivity.this);
integrator.setDesiredBarcodeFormats(IntentIntegrator.PRODUCT_CODE_TYPES);
integrator.setDesiredBarcodeFormats(BarcodeSelectorActivity.SUPPORTED_BARCODE_TYPES);
String prompt = getResources().getString(R.string.scanCardBarcode);
integrator.setPrompt(prompt);
@@ -180,49 +237,69 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
captureButton.setOnClickListener(captureCallback);
saveButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View v)
{
String store = storeField.getText().toString();
String cardId = cardIdField.getText().toString();
String barcodeType = barcodeTypeField.getText().toString();
if(store.isEmpty())
{
Snackbar.make(v, R.string.noStoreError, Snackbar.LENGTH_LONG).show();
return;
}
if(cardId.isEmpty() || barcodeType.isEmpty())
{
Snackbar.make(v, R.string.noCardIdError, Snackbar.LENGTH_LONG).show();
return;
}
if(updateLoyaltyCard)
{
db.updateLoyaltyCard(loyaltyCardId, store, cardId, barcodeType);
Log.i(TAG, "Updated " + loyaltyCardId + " to " + cardId);
}
else
{
db.insertLoyaltyCard(store, cardId, barcodeType);
}
finish();
}
});
cancelButton.setOnClickListener(new View.OnClickListener()
enterButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
finish();
Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
String cardId = cardIdFieldView.getText().toString();
if(cardId.length() > 0)
{
final Bundle b = new Bundle();
b.putString("initialCardId", cardId);
i.putExtras(b);
}
startActivityForResult(i, SELECT_BARCODE_REQUEST);
}
});
if(cardIdFieldView.getText().length() > 0)
{
cardIdDivider.setVisibility(View.VISIBLE);
cardIdTableRow.setVisibility(View.VISIBLE);
enterButton.setText(R.string.editCard);
}
else
{
cardIdDivider.setVisibility(View.GONE);
cardIdTableRow.setVisibility(View.GONE);
enterButton.setText(R.string.enterCard);
}
}
private void doSave()
{
String store = storeFieldEdit.getText().toString();
String note = noteFieldEdit.getText().toString();
String cardId = cardIdFieldView.getText().toString();
String barcodeType = barcodeTypeField.getText().toString();
if(store.isEmpty())
{
Snackbar.make(storeFieldEdit, R.string.noStoreError, Snackbar.LENGTH_LONG).show();
return;
}
if(cardId.isEmpty() || barcodeType.isEmpty())
{
Snackbar.make(cardIdFieldView, R.string.noCardIdError, Snackbar.LENGTH_LONG).show();
return;
}
if(updateLoyaltyCard)
{
db.updateLoyaltyCard(loyaltyCardId, store, note, cardId, barcodeType);
Log.i(TAG, "Updated " + loyaltyCardId + " to " + cardId);
}
else
{
db.insertLoyaltyCard(store, note, cardId, barcodeType);
}
finish();
}
@Override
@@ -234,14 +311,20 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
if(viewLoyaltyCard)
{
getMenuInflater().inflate(R.menu.card_edit_menu, menu);
getMenuInflater().inflate(R.menu.card_view_menu, menu);
}
else if(updateLoyaltyCard)
{
getMenuInflater().inflate(R.menu.card_delete_menu, menu);
getMenuInflater().inflate(R.menu.card_update_menu, menu);
}
else
{
getMenuInflater().inflate(R.menu.card_add_menu, menu);
}
return true;
rotationEnabled = true;
return super.onCreateOptionsMenu(menu);
}
@Override
@@ -254,12 +337,41 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
switch(id)
{
case R.id.action_delete:
Log.e(TAG, "Deleting card: " + loyaltyCardId);
DBHelper db = new DBHelper(this);
db.deleteLoyaltyCard(loyaltyCardId);
case android.R.id.home:
finish();
break;
case R.id.action_delete:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.deleteTitle);
builder.setMessage(R.string.deleteConfirmation);
builder.setPositiveButton(R.string.confirm, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
Log.e(TAG, "Deleting card: " + loyaltyCardId);
DBHelper db = new DBHelper(LoyaltyCardViewActivity.this);
db.deleteLoyaltyCard(loyaltyCardId);
ShortcutHelper.removeShortcut(LoyaltyCardViewActivity.this, loyaltyCardId);
finish();
dialog.dismiss();
}
});
builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
});
AlertDialog dialog = builder.create();
dialog.show();
return true;
case R.id.action_edit:
Intent intent = new Intent(getApplicationContext(), LoyaltyCardViewActivity.class);
@@ -270,6 +382,26 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
startActivity(intent);
finish();
return true;
case R.id.action_lock_unlock:
if(rotationEnabled)
{
item.setIcon(R.drawable.ic_lock_outline_white_24dp);
item.setTitle(R.string.unlockScreen);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
}
else
{
item.setIcon(R.drawable.ic_lock_open_white_24dp);
item.setTitle(R.string.lockScreen);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
rotationEnabled = !rotationEnabled;
return true;
case R.id.action_save:
doSave();
return true;
}
return super.onOptionsItemSelected(item);
@@ -278,24 +410,38 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent)
{
String contents = null;
String format = null;
IntentResult result =
IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
if (result != null)
{
String contents = result.getContents();
String format = result.getFormatName();
if(contents != null && contents.isEmpty() == false &&
format != null && format.isEmpty() == false)
{
Log.i(TAG, "Read Contents from scan: " + contents);
Log.i(TAG, "Read Format: " + format);
Log.i(TAG, "Received barcode information from capture");
contents = result.getContents();
format = result.getFormatName();
}
final EditText cardIdField = (EditText) findViewById(R.id.cardId);
cardIdField.setText(contents);
final EditText barcodeTypeField = (EditText) findViewById(R.id.barcodeType);
barcodeTypeField.setText(format);
onResume();
}
if(requestCode == SELECT_BARCODE_REQUEST && resultCode == Activity.RESULT_OK)
{
Log.i(TAG, "Received barcode information from capture");
contents = intent.getStringExtra(BarcodeSelectorActivity.BARCODE_CONTENTS);
format = intent.getStringExtra(BarcodeSelectorActivity.BARCODE_FORMAT);
}
if(contents != null && contents.isEmpty() == false &&
format != null && format.isEmpty() == false)
{
Log.i(TAG, "Read barcode id: " + contents);
Log.i(TAG, "Read format: " + format);
TextView cardIdView = (TextView)findViewById(R.id.cardIdView);
cardIdView.setText(contents);
final TextView barcodeTypeField = (TextView) findViewById(R.id.barcodeType);
barcodeTypeField.setText(format);
onResume();
}
}
}
}

View File

@@ -1,19 +1,44 @@
package protect.card_locker;
import android.content.ClipData;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ClipboardManager;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.collect.ImmutableMap;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import protect.card_locker.intro.IntroActivity;
public class MainActivity extends AppCompatActivity
{
private static final String TAG = "LoyaltyCardLocker";
@Override
protected void onCreate(Bundle savedInstanceState)
{
@@ -23,6 +48,12 @@ public class MainActivity extends AppCompatActivity
setSupportActionBar(toolbar);
updateLoyaltyCardList();
SharedPreferences prefs = getSharedPreferences("protect.card_locker", MODE_PRIVATE);
if (prefs.getBoolean("firstrun", true)) {
startIntro();
prefs.edit().putBoolean("firstrun", false).commit();
}
}
@Override
@@ -55,6 +86,8 @@ public class MainActivity extends AppCompatActivity
final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor);
cardList.setAdapter(adapter);
registerForContextMenu(cardList);
cardList.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
@@ -64,15 +97,52 @@ public class MainActivity extends AppCompatActivity
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
Intent i = new Intent(view.getContext(), LoyaltyCardViewActivity.class);
i.setAction("");
final Bundle b = new Bundle();
b.putInt("id", loyaltyCard.id);
b.putBoolean("view", true);
i.putExtras(b);
ShortcutHelper.updateShortcuts(MainActivity.this, loyaltyCard, i);
startActivity(i);
}
});
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, v, menuInfo);
if (v.getId()==R.id.list)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.card_longclick_menu, menu);
}
}
@Override
public boolean onContextItemSelected(MenuItem item)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
ListView listView = (ListView) findViewById(R.id.list);
Cursor cardCursor = (Cursor)listView.getItemAtPosition(info.position);
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor);
if(card != null && item.getItemId() == R.id.action_clipboard)
{
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(card.store, card.cardId);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.copy_to_clipboard_toast, Toast.LENGTH_LONG).show();
return true;
}
return super.onContextItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
@@ -92,6 +162,116 @@ public class MainActivity extends AppCompatActivity
return true;
}
if(id == R.id.action_import_export)
{
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
startActivity(i);
return true;
}
if(id == R.id.action_intro)
{
startIntro();
return true;
}
if(id == R.id.action_about)
{
displayAboutDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
private void displayAboutDialog()
{
final Map<String, String> USED_LIBRARIES = ImmutableMap.of
(
"Commons CSV", "https://commons.apache.org/proper/commons-csv/",
"Guava", "https://github.com/google/guava",
"ZXing", "https://github.com/zxing/zxing",
"ZXing Android Embedded", "https://github.com/journeyapps/zxing-android-embedded",
"AppIntro", "https://github.com/apl-devs/AppIntro"
);
final Map<String, String> USED_ASSETS = ImmutableMap.of
(
"Save by Bernar Novalyi", "https://thenounproject.com/term/save/716011"
);
StringBuilder libs = new StringBuilder().append("<ul>");
for (Map.Entry<String, String> entry : USED_LIBRARIES.entrySet())
{
libs.append("<li><a href=\"").append(entry.getValue()).append("\">").append(entry.getKey()).append("</a></li>");
}
libs.append("</ul>");
StringBuilder resources = new StringBuilder().append("<ul>");
for (Map.Entry<String, String> entry : USED_ASSETS.entrySet())
{
resources.append("<li><a href=\"").append(entry.getValue()).append("\">").append(entry.getKey()).append("</a></li>");
}
resources.append("</ul>");
String appName = getString(R.string.app_name);
int year = Calendar.getInstance().get(Calendar.YEAR);
String version = "?";
try
{
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
version = pi.versionName;
}
catch (PackageManager.NameNotFoundException e)
{
Log.w(TAG, "Package name not found", e);
}
WebView wv = new WebView(this);
String html =
"<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />" +
"<img src=\"file:///android_res/mipmap/ic_launcher.png\" alt=\"" + appName + "\"/>" +
"<h1>" +
String.format(getString(R.string.about_title_fmt),
"<a href=\"" + getString(R.string.app_webpage_url)) + "\">" +
appName +
"</a>" +
"</h1><p>" +
appName +
" " +
String.format(getString(R.string.debug_version_fmt), version) +
"</p><p>" +
String.format(getString(R.string.app_revision_fmt),
"<a href=\"" + getString(R.string.app_revision_url) + "\">" +
getString(R.string.app_revision_url) +
"</a>") +
"</p><hr/><p>" +
String.format(getString(R.string.app_copyright_fmt), year) +
"</p><hr/><p>" +
getString(R.string.app_license) +
"</p><hr/><p>" +
String.format(getString(R.string.app_libraries), appName, libs.toString()) +
"</p><hr/><p>" +
String.format(getString(R.string.app_resources), appName, resources.toString());
wv.loadDataWithBaseURL("file:///android_res/drawable/", html, "text/html", "utf-8", null);
new AlertDialog.Builder(this)
.setView(wv)
.setCancelable(true)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
})
.show();
}
private void startIntro()
{
Intent intent = new Intent(this, IntroActivity.class);
startActivity(intent);
}
}

View File

@@ -0,0 +1,57 @@
package protect.card_locker;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class MultiFormatExporter
{
private static final String TAG = "LoyaltyCardLocker";
/**
* Attempts to export data to the output stream in the
* given format, if possible.
*
* The output stream is closed on success.
*
* @return true if the database was successfully exported,
* false otherwise. If false, partial data may have been
* written to the output stream, and it should be discarded.
*/
public static boolean exportData(DBHelper db, OutputStreamWriter output, DataFormat format)
{
DatabaseExporter exporter = null;
switch(format)
{
case CSV:
exporter = new CsvDatabaseExporter();
break;
}
if(exporter != null)
{
try
{
exporter.exportData(db, output);
return true;
}
catch(IOException e)
{
Log.e(TAG, "Failed to export data", e);
}
catch(InterruptedException e)
{
Log.e(TAG, "Failed to export data", e);
}
return false;
}
else
{
Log.e(TAG, "Unsupported data format exported: " + format.name());
return false;
}
}
}

View File

@@ -0,0 +1,62 @@
package protect.card_locker;
import android.util.Log;
import java.io.IOException;
import java.io.InputStreamReader;
public class MultiFormatImporter
{
private static final String TAG = "LoyaltyCardLocker";
/**
* Attempts to import data from the input stream of the
* given format into the database.
*
* The input stream is not closed, and doing so is the
* responsibility of the caller.
*
* @return true if the database was successfully imported,
* false otherwise. If false, no data was written to
* the database.
*/
public static boolean importData(DBHelper db, InputStreamReader input, DataFormat format)
{
DatabaseImporter importer = null;
switch(format)
{
case CSV:
importer = new CsvDatabaseImporter();
break;
}
if(importer != null)
{
try
{
importer.importData(db, input);
return true;
}
catch(IOException e)
{
Log.e(TAG, "Failed to input data", e);
}
catch(FormatException e)
{
Log.e(TAG, "Failed to input data", e);
}
catch(InterruptedException e)
{
Log.e(TAG, "Failed to input data", e);
}
return false;
}
else
{
Log.e(TAG, "Unsupported data format imported: " + format.name());
return false;
}
}
}

View File

@@ -0,0 +1,147 @@
package protect.card_locker;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
class ShortcutHelper
{
// Android documentation says that no more than 5 shortcuts
// are supported. However, that may be too many, as not all
// launcher will show all 5. Instead, the number is limited
// to 3 here, so that the most recent shortcut has a good
// chance of being shown.
private static final int MAX_SHORTCUTS = 3;
/**
* Add a card to the app shortcuts, and maintain a list of the most
* recently used cards. If there is already a shortcut for the card,
* the card is marked as the most recently used card. If adding this
* card exceeds the max number of shortcuts, then the least recently
* used card shortcut is discarded.
*/
@TargetApi(25)
static void updateShortcuts(Context context, LoyaltyCard card, Intent intent)
{
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1)
{
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
LinkedList<ShortcutInfo> list = new LinkedList<>(shortcutManager.getDynamicShortcuts());
String shortcutId = Integer.toString(card.id);
// Sort the shortcuts by rank, so working with the relative order will be easier.
// This sorts so that the lowest rank is first.
Collections.sort(list, new Comparator<ShortcutInfo>()
{
@Override
public int compare(ShortcutInfo o1, ShortcutInfo o2)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
{
return o1.getRank() - o2.getRank();
}
else
{
return 0;
}
}
});
Integer foundIndex = null;
for(int index = 0; index < list.size(); index++)
{
if(list.get(index).getId().equals(shortcutId))
{
// Found the item already
foundIndex = index;
break;
}
}
if(foundIndex != null)
{
// If the item is already found, then the list needs to be
// reordered, so that the selected item now has the lowest
// rank, thus letting it survive longer.
ShortcutInfo found = list.remove(foundIndex.intValue());
list.addFirst(found);
}
else
{
// The item is new to the list. First, we need to trim the list
// until it is able to accept a new item, then the item is
// inserted.
while(list.size() >= MAX_SHORTCUTS)
{
list.pollLast();
}
ShortcutInfo shortcut = new ShortcutInfo.Builder(context, Integer.toString(card.id))
.setShortLabel(card.store)
.setLongLabel(card.store)
.setIntent(intent)
.build();
list.addFirst(shortcut);
}
LinkedList<ShortcutInfo> finalList = new LinkedList<>();
// The ranks are now updated; the order in the list is the rank.
for(int index = 0; index < list.size(); index++)
{
ShortcutInfo prevShortcut = list.get(index);
ShortcutInfo updatedShortcut = new ShortcutInfo.Builder(context, prevShortcut.getId())
.setShortLabel(prevShortcut.getShortLabel())
.setLongLabel(prevShortcut.getLongLabel())
.setIntent(prevShortcut.getIntent())
.setIcon(Icon.createWithResource(context, R.drawable.circle))
.setRank(index)
.build();
finalList.addLast(updatedShortcut);
}
shortcutManager.setDynamicShortcuts(finalList);
}
}
/**
* Remove the given card id from the app shortcuts, if such a
* shortcut exists.
*/
@TargetApi(25)
static void removeShortcut(Context context, int cardId)
{
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1)
{
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
List<ShortcutInfo> list = shortcutManager.getDynamicShortcuts();
String shortcutId = Integer.toString(cardId);
for(int index = 0; index < list.size(); index++)
{
if(list.get(index).getId().equals(shortcutId))
{
list.remove(index);
break;
}
}
shortcutManager.setDynamicShortcuts(list);
}
}
}

View File

@@ -0,0 +1,130 @@
package protect.card_locker.appwidget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import protect.card_locker.DBHelper;
import protect.card_locker.LoyaltyCard;
import protect.card_locker.LoyaltyCardCursorAdapter;
import protect.card_locker.R;
/**
* The configuration screen for the CardAppWidgetProvider widget.
*/
public class CardAppWidgetConfigure extends AppCompatActivity
{
static final String TAG = "LoyaltyCardLocker";
private static final String PREFS_NAME
= "protect.card_locker.appwidget.CardAppWidgetProvider";
private static final String PREF_PREFIX_KEY = "prefix_";
int appWidgetId_ = AppWidgetManager.INVALID_APPWIDGET_ID;
@Override
public void onCreate(Bundle bundle)
{
super.onCreate(bundle);
// Set the result to CANCELED. This will cause the widget host to cancel
// out of the widget placement if they press the back button.
setResult(RESULT_CANCELED);
setContentView(R.layout.main_activity);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setVisibility(View.GONE);
setTitle(R.string.selectCardTitle);
// Find the widget id from the intent.
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null)
{
appWidgetId_ = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
}
// If they gave us an intent without the widget id, just bail.
if (appWidgetId_ == AppWidgetManager.INVALID_APPWIDGET_ID)
{
finish();
}
final DBHelper db = new DBHelper(this);
// If there are no cards, bail
if(db.getLoyaltyCardCount() == 0)
{
finish();
}
final ListView cardList = (ListView) findViewById(R.id.list);
cardList.setVisibility(View.VISIBLE);
Cursor cardCursor = db.getLoyaltyCardCursor();
final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor);
cardList.setAdapter(adapter);
cardList.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
Context context = CardAppWidgetConfigure.this;
Cursor selected = (Cursor) parent.getItemAtPosition(position);
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
Log.d(TAG, "Saving card " + loyaltyCard.store + "," + loyaltyCard.id + " at " + appWidgetId_);
// Save the association of the card to the widget
saveIdPref(context, appWidgetId_, loyaltyCard.id);
// Push widget update to surface with newly set association
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
CardAppWidgetProvider.updateAppWidget(context, appWidgetManager, appWidgetId_);
// Make sure we pass back the original appWidgetId
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId_);
setResult(RESULT_OK, resultValue);
finish();
}
});
}
// Write the prefix to the SharedPreferences object for this widget
static void saveIdPref(Context context, int appWidgetId, int id)
{
SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit();
prefs.putInt(PREF_PREFIX_KEY + appWidgetId, id);
prefs.commit();
}
// Read the prefix from the SharedPreferences object for this widget.
// If there is no preference saved, get the default from a resource
static Integer loadIdPref(Context context, int appWidgetId)
{
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
int id = prefs.getInt(PREF_PREFIX_KEY + appWidgetId, -1);
if(id >= 0)
{
return id;
}
else
{
return null;
}
}
}

View File

@@ -0,0 +1,79 @@
package protect.card_locker.appwidget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViews;
import protect.card_locker.DBHelper;
import protect.card_locker.LoyaltyCard;
import protect.card_locker.LoyaltyCardViewActivity;
import protect.card_locker.R;
public class CardAppWidgetProvider extends AppWidgetProvider
{
private static final String TAG = "LoyaltyCardLocker";
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
Log.d(TAG, "CardAppWidgetProvider onUpdate");
// For each widget that needs an update, get the text that we should display:
// - Create a RemoteViews object for it
// - Set the text in the RemoteViews object
// - Tell the AppWidgetManager to show that views object for the widget.
for (int appWidgetId : appWidgetIds)
{
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId)
{
Log.d(TAG, "updateAppWidget appWidgetId=" + appWidgetId);
LoyaltyCard card = null;
DBHelper db = new DBHelper(context);
Integer id = CardAppWidgetConfigure.loadIdPref(context, appWidgetId);
if(id != null)
{
Log.d(TAG, "updateAppWidget Retrieved id " + id);
card = db.getLoyaltyCard(id);
}
if(card != null)
{
Log.d(TAG, "updateAppWidget Updating widget " + appWidgetId + " to load " + card.store);
// Construct the RemoteViews object. It takes the package name (in our case, it's our
// package, but it needs this because on the other side it's the widget host inflating
// the layout from our package).
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider);
views.setTextViewText(R.id.title, card.store);
// Launch the view activity when clicked
Intent intent = new Intent(context, LoyaltyCardViewActivity.class);
Bundle extras = new Bundle();
extras.putInt("id", id);
extras.putBoolean("view", true);
intent.putExtras(extras);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.widget, pendingIntent);
// Tell the widget manager
appWidgetManager.updateAppWidget(appWidgetId, views);
}
else
{
Log.d(TAG, "updateAppWidget, no card ID associated with widget " + appWidgetId
+ ", ignoring update");
}
}
}

View File

@@ -0,0 +1,36 @@
package protect.card_locker.intro;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.v4.app.Fragment;
import com.github.paolorotolo.appintro.AppIntro;
import protect.card_locker.R;
public class IntroActivity extends AppIntro
{
@Override
public void init(Bundle savedInstanceState)
{
addSlide(new IntroSlide1());
addSlide(new IntroSlide2());
addSlide(new IntroSlide3());
addSlide(new IntroSlide4());
addSlide(new IntroSlide5());
addSlide(new IntroSlide6());
}
@Override
public void onSkipPressed(Fragment fragment) {
finish();
}
@Override
public void onDonePressed(Fragment fragment) {
finish();
}
}

View File

@@ -0,0 +1,19 @@
package protect.card_locker.intro;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import protect.card_locker.R;
public class IntroSlide1 extends Fragment
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.intro1_layout, container, false);
return v;
}
}

View File

@@ -0,0 +1,20 @@
package protect.card_locker.intro;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import protect.card_locker.R;
public class IntroSlide2 extends Fragment
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.intro2_layout, container, false);
return v;
}
}

View File

@@ -0,0 +1,19 @@
package protect.card_locker.intro;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import protect.card_locker.R;
public class IntroSlide3 extends Fragment
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.intro3_layout, container, false);
return v;
}
}

View File

@@ -0,0 +1,19 @@
package protect.card_locker.intro;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import protect.card_locker.R;
public class IntroSlide4 extends Fragment
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.intro4_layout, container, false);
return v;
}
}

View File

@@ -0,0 +1,19 @@
package protect.card_locker.intro;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import protect.card_locker.R;
public class IntroSlide5 extends Fragment
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.intro5_layout, container, false);
return v;
}
}

View File

@@ -0,0 +1,19 @@
package protect.card_locker.intro;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import protect.card_locker.R;
public class IntroSlide6 extends Fragment
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.intro6_layout, container, false);
return v;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget"
android:layout_width="60.0dip"
android:layout_height="70.0dip">
<ImageView
android:id="@+id/icon"
android:layout_gravity="start"
android:layout_centerHorizontal="true"
android:layout_width="55.0dip"
android:layout_height="55.0dip"
android:gravity="center_horizontal"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/icon"
android:gravity="center_horizontal"
android:ellipsize="end"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
</RelativeLayout>

View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<ScrollView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/scrollView">
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:textSize="20sp"
android:layout_gravity="center_vertical"
android:paddingStart="20.0dip"
android:paddingEnd="20.0dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enterBarcodeInstructions" />
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/barcodeIdLayout">
<TextView android:textSize="16.0sp"
android:textStyle="bold"
android:layout_gravity="center_vertical"
android:paddingStart="20.0dip"
android:paddingEnd="20.0dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@+id/cardId"
android:text="@string/cardId" />
<EditText android:id="@+id/cardId"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="text"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/barcodesLayout"/>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/aztecBarcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/code39Barcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/code128Barcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/codabarBarcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/datamatrixBarcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/ean8Barcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/ean13Barcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/itfBarcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/pdf417Barcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/qrcodeBarcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/upcaBarcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</android.support.design.widget.CoordinatorLayout>

View File

@@ -5,10 +5,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="protect.card_locker.MainActivity"
tools:showIn="@layout/main_activity">

View File

@@ -0,0 +1,166 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_medium"
android:layout_gravity="center"
android:text="@string/importExportHelp"/>
<View
android:id="@+id/dividerExport"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="?android:attr/listDivider"/>
<TextView
android:id="@+id/exportOptionTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_large"
android:text="@string/exportName"/>
<TextView
android:id="@+id/exportOptionExplanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="@dimen/text_size_medium"
android:text="@string/exportOptionExplanation"/>
<Button
android:id="@+id/exportButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="@string/exportName" />
<View
android:id="@+id/dividerImportFilesystem"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="?android:attr/listDivider"/>
<TextView
android:id="@+id/importOptionFilesystemTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_large"
android:text="@string/importOptionFilesystemTitle"/>
<TextView
android:id="@+id/importOptionFilesystemExplanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="@dimen/text_size_medium"
android:text="@string/importOptionFilesystemExplanation"/>
<Button
android:id="@+id/importOptionFilesystemButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="@string/importOptionFilesystemButton" />
<View
android:id="@+id/dividerImportApplication"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="?android:attr/listDivider"/>
<TextView
android:id="@+id/importOptionApplicationTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_large"
android:text="@string/importOptionApplicationTitle"/>
<TextView
android:id="@+id/importOptionApplicationExplanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="@dimen/text_size_medium"
android:text="@string/importOptionApplicationExplanation"/>
<Button
android:id="@+id/importOptionApplicationButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="@string/importOptionApplicationButton" />
<View
android:id="@+id/dividerImportFixed"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="?android:attr/listDivider"/>
<TextView
android:id="@+id/importOptionFixedTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_large"
android:text="@string/importOptionFixedTitle"/>
<TextView
android:id="@+id/importOptionFixedExplanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="@dimen/text_size_medium"
android:text="@string/importOptionFixedExplanation"/>
<Button
android:id="@+id/importOptionFixedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="@string/importOptionFixedButton" />
</LinearLayout>
</ScrollView>
</android.support.design.widget.CoordinatorLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#222222"
android:layout_weight="10"
android:id="@+id/main">
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:gravity="center"
android:paddingLeft="32dp"
android:layout_weight="3"
android:fontFamily="sans-serif-thin"
android:textColor="#ffffff"
android:paddingRight="32dp"
android:textSize="28sp"
android:text="@string/intro1Title"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:gravity="center"
android:layout_weight="5">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:src="@drawable/app_icon_intro"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="3"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#ffffff"
android:paddingLeft="64dp"
android:paddingRight="64dp"
android:textSize="16sp"
android:text="@string/intro1Description"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="64dp" />
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#222222"
android:layout_weight="10"
android:id="@+id/main">
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:gravity="center"
android:paddingLeft="32dp"
android:layout_weight="3"
android:fontFamily="sans-serif-thin"
android:textColor="#ffffff"
android:paddingRight="32dp"
android:textSize="28sp"
android:text="@string/intro2Title"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:gravity="center"
android:layout_weight="5">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:src="@drawable/intro2_image"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="3"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#ffffff"
android:paddingLeft="64dp"
android:paddingRight="64dp"
android:textSize="16sp"
android:text="@string/intro2Description"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="64dp" />
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#222222"
android:layout_weight="10"
android:id="@+id/main">
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:gravity="center"
android:paddingLeft="32dp"
android:layout_weight="3"
android:fontFamily="sans-serif-thin"
android:textColor="#ffffff"
android:paddingRight="32dp"
android:textSize="28sp"
android:text="@string/intro3Title"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:gravity="center"
android:layout_weight="5">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:src="@drawable/intro3_image"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="3"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#ffffff"
android:paddingLeft="64dp"
android:paddingRight="64dp"
android:textSize="16sp"
android:text="@string/intro3Description"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="64dp" />
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#222222"
android:layout_weight="10"
android:id="@+id/main">
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:gravity="center"
android:paddingLeft="32dp"
android:layout_weight="3"
android:fontFamily="sans-serif-thin"
android:textColor="#ffffff"
android:paddingRight="32dp"
android:textSize="28sp"
android:text="@string/intro4Title"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:gravity="center"
android:layout_weight="5">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:src="@drawable/intro4_image"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="3"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#ffffff"
android:paddingLeft="64dp"
android:paddingRight="64dp"
android:textSize="16sp"
android:text="@string/intro4Description"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="64dp" />
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#222222"
android:layout_weight="10"
android:id="@+id/main">
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:gravity="center"
android:paddingLeft="32dp"
android:layout_weight="3"
android:fontFamily="sans-serif-thin"
android:textColor="#ffffff"
android:paddingRight="32dp"
android:textSize="28sp"
android:text="@string/intro5Title"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:gravity="center"
android:layout_weight="5">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:src="@drawable/intro5_image"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="3"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#ffffff"
android:paddingLeft="64dp"
android:paddingRight="64dp"
android:textSize="16sp"
android:text="@string/intro5Description"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="64dp" />
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#222222"
android:layout_weight="10"
android:id="@+id/main">
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:gravity="center"
android:paddingLeft="32dp"
android:layout_weight="3"
android:fontFamily="sans-serif-thin"
android:textColor="#ffffff"
android:paddingRight="32dp"
android:textSize="28sp"
android:text="@string/intro6Title"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:gravity="center"
android:layout_weight="5">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:src="@drawable/app_icon_intro"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="3"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#ffffff"
android:paddingLeft="64dp"
android:paddingRight="64dp"
android:textSize="16sp"
android:text="@string/intro6Description"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="64dp" />
</LinearLayout>

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:orientation="vertical"
android:padding="5.0dip"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout android:orientation="horizontal"
android:padding="5.0dip"
android:padding="5.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:baselineAligned="true">
@@ -13,11 +13,12 @@
android:id="@+id/store"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"/>
android:layout_weight="1.0"
android:maxLines="1"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="5.0dip"
android:padding="5.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:baselineAligned="true">

View File

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="protect.budgetwatch.AccountsActivity">
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
@@ -21,118 +19,230 @@
</android.support.design.widget.AppBarLayout>
<ScrollView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/scrollView">
<ScrollView android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/inputContrastBackground"
app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TableLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:shrinkColumns="1"
android:background="@color/inputContrastBackground">
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
<!-- Store Name -->
<View
android:layout_height="@dimen/inputBorderThickness"
android:layout_width="match_parent"
android:background="@color/inputBorder" />
<TableRow
android:background="@color/inputBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:gravity="start"
android:layout_height="match_parent"
android:layout_width="@dimen/inputBorderThickness"
android:background="@color/inputBorder" />
<RelativeLayout
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingRight="@dimen/inputPadding"
android:paddingEnd="@dimen/inputPadding">
<TextView
android:id="@+id/storeNameField"
android:text="@string/storeName"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:textSize="@dimen/inputSize"
android:padding="@dimen/inputPadding"
android:layout_alignParentStart="true"/>
<EditText
android:id="@+id/storeNameEdit"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="@dimen/inputPadding"
android:textSize="@dimen/inputSize"
android:layout_toEndOf="@id/storeNameField"/>
<TextView
android:id="@+id/storeNameView"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="@dimen/inputPadding"
android:textSize="@dimen/inputSize"
android:textIsSelectable="true"
android:layout_toEndOf="@id/storeNameField"/>
</RelativeLayout>
<View
android:gravity="end"
android:layout_height="match_parent"
android:layout_width="@dimen/inputBorderThickness"
android:background="@color/inputBorder" />
</TableRow>
<!-- Note -->
<View
android:layout_height="@dimen/inputBorderThickness"
android:layout_width="match_parent"
android:background="@color/inputBorder" />
<TableRow
android:background="@color/inputBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:gravity="start"
android:layout_height="match_parent"
android:layout_width="@dimen/inputBorderThickness"
android:background="@color/inputBorder" />
<RelativeLayout
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingRight="@dimen/inputPadding"
android:paddingEnd="@dimen/inputPadding">
<TextView
android:id="@+id/noteField"
android:text="@string/note"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:textSize="@dimen/inputSize"
android:padding="@dimen/inputPadding"
android:layout_alignParentStart="true"/>
<EditText
android:id="@+id/noteEdit"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="@dimen/inputPadding"
android:textSize="@dimen/inputSize"
android:layout_toEndOf="@id/noteField"/>
<TextView
android:id="@+id/noteView"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="@dimen/inputPadding"
android:textSize="@dimen/inputSize"
android:textIsSelectable="true"
android:layout_toEndOf="@id/noteField"/>
</RelativeLayout>
<View
android:gravity="end"
android:layout_height="match_parent"
android:layout_width="@dimen/inputBorderThickness"
android:background="@color/inputBorder" />
</TableRow>
<!-- Card ID -->
<View
android:id="@+id/cardIdDivider"
android:layout_height="@dimen/inputBorderThickness"
android:layout_width="match_parent"
android:background="@color/inputBorder" />
<TableRow
android:id="@+id/cardIdTableRow"
android:background="@color/inputBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:gravity="start"
android:layout_height="match_parent"
android:layout_width="@dimen/inputBorderThickness"
android:background="@color/inputBorder" />
<RelativeLayout
android:orientation="horizontal"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingRight="@dimen/inputPadding"
android:paddingEnd="@dimen/inputPadding">
<TextView
android:id="@+id/cardIdField"
android:text="@string/cardId"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:textSize="@dimen/inputSize"
android:padding="@dimen/inputPadding"
android:layout_alignParentStart="true"/>
<TextView
android:id="@+id/cardIdView"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="@dimen/inputPadding"
android:textSize="@dimen/inputSize"
android:textIsSelectable="true"
android:layout_toEndOf="@id/cardIdField"/>
</RelativeLayout>
<View
android:gravity="end"
android:layout_height="match_parent"
android:layout_width="@dimen/inputBorderThickness"
android:background="@color/inputBorder" />
</TableRow>
<View
android:layout_height="@dimen/inputBorderThickness"
android:layout_width="match_parent"
android:background="@color/inputBorder" />
<TextView android:id="@+id/barcodeType"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:textSize="16.0sp"
android:textStyle="bold"
android:layout_gravity="center_vertical"
android:paddingStart="20.0dip"
android:paddingEnd="20.0dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@+id/storeName"
android:text="@string/storeName" />
android:visibility="gone"/>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:id="@+id/barcodeLayout">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/barcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText android:id="@+id/storeName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="text"/>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/barcodeCaptureLayout">
<Button android:id="@+id/captureButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/capture"
android:layout_weight="1.0"/>
<Button android:id="@+id/enterButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/enterCard"
android:layout_weight="1.0"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/barcodeIdLayout">
<TextView android:textSize="16.0sp"
android:textStyle="bold"
android:layout_gravity="center_vertical"
android:paddingStart="20.0dip"
android:paddingEnd="20.0dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@+id/cardId"
android:text="@string/cardId" />
<EditText android:id="@+id/cardId"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:inputType="text"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/barcodeTypeLayout">
<TextView android:textSize="16.0sp"
android:textStyle="bold"
android:layout_gravity="center_vertical"
android:paddingStart="20.0dip"
android:paddingEnd="20.0dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@+id/barcodeType"
android:text="@string/barcodeType" />
<EditText android:id="@+id/barcodeType"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:inputType="text"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:id="@+id/barcodeLayout">
<ImageView
android:layout_width="0dp"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/barcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/barcodeCaptureLayout">
<Button android:id="@+id/captureButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/capture"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:padding="10.0dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:baselineAligned="true">
<Button android:id="@+id/cancelButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:layout_weight="1.0" />
<Button android:id="@+id/saveButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/save"
android:layout_weight="1.0" />
</LinearLayout>
</TableLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save"
android:icon="@drawable/save_24dp"
android:title="@string/save"
app:showAsAction="always"/>
</menu>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_clipboard"
android:title="@string/copy_to_clipboard"
app:showAsAction="always"/>
</menu>

View File

@@ -2,9 +2,15 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save"
android:icon="@drawable/save_24dp"
android:title="@string/save"
app:showAsAction="always"/>
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_white_24dp"
android:title="@string/delete"
app:showAsAction="always"/>
app:showAsAction="never"/>
</menu>

View File

@@ -2,6 +2,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_lock_unlock"
android:icon="@drawable/ic_lock_open_white_24dp"
android:title="@string/lockScreen"
app:showAsAction="always"/>
<item
android:id="@+id/action_edit"
android:icon="@drawable/ic_mode_edit_white_24dp"

View File

@@ -7,4 +7,17 @@
android:icon="@drawable/ic_add_white_24dp"
android:title="@string/action_add"
app:showAsAction="always"/>
<item
android:id="@+id/action_import_export"
android:icon="@drawable/ic_import_export_white_24dp"
android:title="@string/importExport"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_intro"
android:title="@string/startIntro"
app:showAsAction="never"/>
<item
android:id="@+id/action_about"
android:title="@string/about"
app:showAsAction="never"/>
</menu>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1012 B

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

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