From 845e11d71f75160b0b8493c3af83832b20bde3bf Mon Sep 17 00:00:00 2001 From: Johannes Zweng Date: Sun, 19 Jan 2014 21:41:51 +0100 Subject: [PATCH] =?UTF-8?q?Version=201.2.4=20(as=20published=20in=20Google?= =?UTF-8?q?=20Play)=20=E2=80=A2=20shows=20new=20fields:=20expiration=20dat?= =?UTF-8?q?e,=20activation=20date,=20card=20number=20=E2=80=A2=20bugfix:?= =?UTF-8?q?=20fixed=20IOException=20on=20app=20restart=20=E2=80=A2=20bugfi?= =?UTF-8?q?x:=20fixed=20crash=20on=20"Settings"=20page=20=E2=80=A2=20new?= =?UTF-8?q?=20menu=20entry:=20"Donation"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .settings/org.eclipse.core.resources.prefs | 3 + AndroidManifest.xml | 9 +- doc/google_play_store/changelog_de.txt | 15 +- doc/google_play_store/changelog_en.txt | 15 +- doc/sreenshots/result_tab_infos_256px.png | Bin 21187 -> 22507 bytes .../background_list_views_not_clickable.xml | 6 + res/layout/fragment_donation_dialog.xml | 73 ++++++++ res/layout/fragment_result_infos.xml | 123 -------------- res/layout/fragment_result_tx_list.xml | 3 +- res/layout/list_item_general_info.xml | 43 +++++ .../list_item_transaction_collapsed.xml | 4 +- res/layout/list_item_transaction_expanded.xml | 13 +- res/menu/main_menu.xml | 9 +- res/raw-de/changelog.txt | 7 + res/raw/changelog.txt | 7 + res/values-de/strings.xml | 14 +- res/values/strings.xml | 14 +- res/values/styles.xml | 31 +++- res/xml/pref_general.xml | 2 +- .../bankomatinfos/iso7816emv/EmvUtils.java | 159 ++++++++++++++++-- .../iso7816emv/NfcBankomatCardReader.java | 102 +++++++---- .../bankomatinfos/iso7816emv/TagAndValue.java | 41 +++++ .../zweng/bankomatinfos/model/CardInfo.java | 54 +++++- .../bankomatinfos/model/InfoKeyValuePair.java | 32 ++++ .../ui/DonateDialogFragment.java | 84 +++++++++ .../bankomatinfos/ui/ListAdapterInfos.java | 71 ++++++++ .../ui/ListAdapterTransactions.java | 10 +- .../zweng/bankomatinfos/ui/MainActivity.java | 17 +- .../bankomatinfos/ui/NfcDisabledActivity.java | 4 + .../bankomatinfos/ui/ResultActivity.java | 6 +- .../bankomatinfos/ui/ResultInfosFragment.java | 71 -------- .../ui/ResultInfosListFragment.java | 44 +++++ src/at/zweng/bankomatinfos/util/Utils.java | 28 ++- 33 files changed, 817 insertions(+), 297 deletions(-) create mode 100644 res/color/background_list_views_not_clickable.xml create mode 100644 res/layout/fragment_donation_dialog.xml delete mode 100644 res/layout/fragment_result_infos.xml create mode 100644 res/layout/list_item_general_info.xml create mode 100644 src/at/zweng/bankomatinfos/iso7816emv/TagAndValue.java create mode 100644 src/at/zweng/bankomatinfos/model/InfoKeyValuePair.java create mode 100644 src/at/zweng/bankomatinfos/ui/DonateDialogFragment.java create mode 100644 src/at/zweng/bankomatinfos/ui/ListAdapterInfos.java delete mode 100644 src/at/zweng/bankomatinfos/ui/ResultInfosFragment.java create mode 100644 src/at/zweng/bankomatinfos/ui/ResultInfosListFragment.java diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs index 2bfcd1e..81b1c04 100644 --- a/.settings/org.eclipse.core.resources.prefs +++ b/.settings/org.eclipse.core.resources.prefs @@ -1,4 +1,7 @@ eclipse.preferences.version=1 encoding//res/raw-de/changelog.txt=UTF-8 encoding//res/raw/changelog.txt=UTF-8 +encoding//src/at/zweng/bankomatinfos/iso7816emv/EmvUtils.java=UTF-8 +encoding//src/at/zweng/bankomatinfos/util/Utils.java=UTF-8 encoding/README.md=UTF-8 +encoding/src=UTF-8 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ea98f64..985b374 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="7" + android:versionName="1.2.4" > + android:label="@string/title_activity_settings" + android:parentActivityName="at.zweng.bankomatinfos.ui.MainActivity" > - + \ No newline at end of file diff --git a/doc/google_play_store/changelog_de.txt b/doc/google_play_store/changelog_de.txt index 229b516..6690001 100644 --- a/doc/google_play_store/changelog_de.txt +++ b/doc/google_play_store/changelog_de.txt @@ -1,12 +1,11 @@ +Version 1.2.4 (19.01.2014): +• zeigt nun auch: Ablaufdatum, Austellungsdatum und Kartennummer +• bugfix: IOException bei App-Neustart behoben +• bugfix: Absturz auf "Einstellungen" Seite behoben +• neuer Menüeintrag: "Spenden" + + Version 1.2.3 (12.01.2014): • bessere Aufschlüsselung der Transaktionen • bugfix: Lesefehler bei bestimmten Transaktionstypen behoben • bugfix: falsche Uhrzeit bei manchen Transaktionen behoben - - -Version 1.2.2 (10.01.2014): -• Neues App Logo -• neuer Menüpunkt 'Einstellungen' -• neue Einstellung 'Scanne alle Dateien' -• Auslesen der noch verfügbaren PIN-Eingabeversuche -• bessere Fehlerbehandlung beim Lesen der QUICK Daten \ No newline at end of file diff --git a/doc/google_play_store/changelog_en.txt b/doc/google_play_store/changelog_en.txt index 3e56def..3153f10 100644 --- a/doc/google_play_store/changelog_en.txt +++ b/doc/google_play_store/changelog_en.txt @@ -1,12 +1,11 @@ +Version 1.2.4 (2014-01-19): +• shows new fields: expiration date, activation date, card number +• bugfix: fixed IOException on app restart +• bugfix: fixed crash on "Settings" page +• new menu entry: "Donation" + + Version 1.2.3 (2014-01-12): • better decoding of transactions! • bugfix: corrected parsing error with some specific transactions • bugfix: wrong time displayed in some transaction - - -Version 1.2.2 (2014-01-10): -• New app icon -• added 'Settings' menu -• new setting 'Scan all files' -• added reading the current PIN-retry-counter -• better handling parsing error with QUICK data \ No newline at end of file diff --git a/doc/sreenshots/result_tab_infos_256px.png b/doc/sreenshots/result_tab_infos_256px.png index 2d95fed14b13cfa8127a014319067611b5fb2d23..738e3199e93b39c5ad6906203dc9cbbe24871936 100644 GIT binary patch literal 22507 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 literal 21187 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 + + + + + \ No newline at end of file diff --git a/res/layout/fragment_donation_dialog.xml b/res/layout/fragment_donation_dialog.xml new file mode 100644 index 0000000..8a403e9 --- /dev/null +++ b/res/layout/fragment_donation_dialog.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/fragment_result_infos.xml b/res/layout/fragment_result_infos.xml deleted file mode 100644 index 743b224..0000000 --- a/res/layout/fragment_result_infos.xml +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/fragment_result_tx_list.xml b/res/layout/fragment_result_tx_list.xml index 6d020bb..ba9cb22 100644 --- a/res/layout/fragment_result_tx_list.xml +++ b/res/layout/fragment_result_tx_list.xml @@ -2,8 +2,7 @@ + android:layout_height="match_parent" > + + + + + + + + + \ No newline at end of file diff --git a/res/layout/list_item_transaction_collapsed.xml b/res/layout/list_item_transaction_collapsed.xml index 12cbc86..299c920 100644 --- a/res/layout/list_item_transaction_collapsed.xml +++ b/res/layout/list_item_transaction_collapsed.xml @@ -23,7 +23,7 @@ android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:maxLines="1" - android:text="23.12.2013 07:12:23" + android:text="" android:textAppearance="@style/TransactionListTimestamp" /> + + + + \ No newline at end of file diff --git a/res/menu/main_menu.xml b/res/menu/main_menu.xml index 50b94e1..d4f8421 100644 --- a/res/menu/main_menu.xml +++ b/res/menu/main_menu.xml @@ -5,14 +5,19 @@ android:orderInCategory="99" android:showAsAction="never" android:title="@string/action_settings"/> + +$ 1.2.4 + % Version 1.2.4 + _ 2014-01-19 + * zeigt nun auch: Ablaufdatum, Austellungsdatum, Kartennummer + * bugfix: IOException bei App-Neustart behoben + * bugfix: Absturz auf 'Einstellungen' Seite + * neuer Menüeintrag: Spenden $ 1.2.3 % Version 1.2.3 _ 2014-01-12 diff --git a/res/raw/changelog.txt b/res/raw/changelog.txt index 3e0a225..8f212f1 100644 --- a/res/raw/changelog.txt +++ b/res/raw/changelog.txt @@ -19,6 +19,13 @@ +$ 1.2.4 + % Version 1.2.4 + _ 2014-01-19 + * show new fields: expiration date, activation date, card number + * bugfix: fixed IOException on app restart + * bugfix: crash on 'Settings' page + * new menu entry: donation $ 1.2.3 % Version 1.2.3 _ 2014-01-12 diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 61b5a64..7ddbd57 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -5,6 +5,7 @@ Einstellungen Ãœber Teilen + Spenden Letzte Änderungen Bitte halten Sie Ihre Bankomatkarte an die Rückseite des Geräts… NFC Logo @@ -41,7 +42,11 @@ NEIN QUICK Guthaben auf der Karte: QUICK Währung: - + Verbleibende Versuche zur PIN Eingabe: + Ablaufdatum der Karte: + Ausstellungsdatum der Karte: + Kartennummer: + Keine Transaktionen gefunden @@ -62,6 +67,13 @@ Infos App logo + + Spenden + Bitcoin + Paypal + Euro (Ãœberweisung) + Eins vorweg:

Es ist absolut in Ordnung diese App frei und kostenlos zu verwenden. Ich habe diese App nur aus technischem Interesse und nicht mit Gewinnabsichten gebaut.

Diejenigen die dennoch gerne etwas spenden möchte, können dies wie folgt tun: ]]>
+ Letzte Änderungen Was ist neu? diff --git a/res/values/strings.xml b/res/values/strings.xml index 4fa1b42..121f8ad 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5,6 +5,7 @@ Settings About Share + Donation Changelog Please hold your card close to the back side of your device… NFC Logo @@ -41,6 +42,10 @@ NO QUICK balance: QUICK currency: + Remaining PIN entry retries: + Expiration date: + Activation date: + Card number: No transactions found @@ -62,6 +67,13 @@ About App logo + + Donations + Bitcoin + Paypal + Euro (bank account) + First of all:

It\'s completely okay to use this app for free. I\'ve built this app just out of technical interest and not with the intent to make profit.

For those who want to donate, you can do it the following ways: ]]>
+ Change Log What\'s New @@ -75,5 +87,5 @@ Scan all files Scan all files. This will take longer but *may* find more data. Just scan well-known files. Scanning will be much faster. - + \ No newline at end of file diff --git a/res/values/styles.xml b/res/values/styles.xml index f4fdbed..663d00e 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -2,13 +2,13 @@ @@ -17,35 +17,50 @@ - + + + + + - - - + + + + + + + \ No newline at end of file diff --git a/res/xml/pref_general.xml b/res/xml/pref_general.xml index f6fc031..5199052 100644 --- a/res/xml/pref_general.xml +++ b/res/xml/pref_general.xml @@ -1,7 +1,7 @@ + * date: 0x131231
+ * --> which represents 31. December 2013 + * + * @param date + * @return + * @throws ParseException + */ + public static Date getDateFromBcdBytes(byte[] date) throws ParseException { + if (date == null || date.length != 3) { + throw new IllegalArgumentException( + "getDateFromBcdBytes: date must be exactly 3 bytes long"); + } + DateFormat df = new SimpleDateFormat("yy MM dd", Locale.US); + return df.parse(prettyPrintString(bytesToHex(date), 2)); } /** @@ -690,7 +724,7 @@ public static boolean responsePduLooksLikeTxLogEntry(byte[] responsePdu) { // TODO: read cards FCI for getting locataion and format of log entries // TODO: currently hardcoded to log format of Austrian cards - + // 9F 4F - 1A bytes: Log Format // -------------------------------------- // 9F 27 (01 bytes) -> Cryptogram Information Data @@ -703,7 +737,7 @@ public static boolean responsePduLooksLikeTxLogEntry(byte[] responsePdu) { // DF 3E (01 bytes) -> [UNHANDLED TAG] // 9F 21 (03 bytes) -> Transaction Time (HHMMSS) // 9F 7C (14 bytes) -> Customer Exclusive Data - + if (responsePdu == null) { return false; } @@ -736,7 +770,7 @@ public static boolean responsePduLooksLikeTxLogEntry(byte[] responsePdu) { if (!bytesLookLikeValidTime(time)) { return false; } - + // DATE: // check if time bytes look like a valid date if (!bytesLookLikeValidDate(date)) { @@ -890,9 +924,9 @@ public static String prettyPrintBerTlvAPDUResponse(byte[] data, EmvTag tag = tlv.getTag(); // buf.append(" TAG: "); - buf.append(prettyPrintHexString(bytesToHex(tagBytes))); + buf.append(prettyPrintString(bytesToHex(tagBytes), 2)); buf.append(" - "); - buf.append(prettyPrintHexString(bytesToHex(lengthBytes))); + buf.append(prettyPrintString(bytesToHex(lengthBytes), 2)); buf.append(" bytes: "); buf.append(tag.getName()); @@ -918,7 +952,7 @@ public static String prettyPrintBerTlvAPDUResponse(byte[] data, indentLength + extraIndent)); buf.append(" ("); - buf.append(getTagValueAsString(tag, valueBytes)); + buf.append(getTagValueInfo(tag, valueBytes)); buf.append(")"); } } @@ -926,6 +960,109 @@ public static String prettyPrintBerTlvAPDUResponse(byte[] data, return buf.toString(); } + /** + * Tries to parse a byte array as EMV BER-TLV encoded data and returns a + * list of tags + * + * source: https://code.google.com/p/javaemvreader/ + * + * @param data + * @param indentLength + * @return + * @throws NfcException + */ + public static List getTagsFromBerTlvAPDUResponse(byte[] data) + throws TlvParsingException { + List tagList = new ArrayList(); + ByteArrayInputStream stream = new ByteArrayInputStream(data); + while (stream.available() > 0) { + BERTLV tlv = getNextTLV(stream); + EmvTag tag = tlv.getTag(); + byte[] valueBytes = tlv.getValueBytes(); + + if (tag.isConstructed()) { + // Recursion: + tagList.addAll(getTagsFromBerTlvAPDUResponse(tlv + .getValueBytes())); + } else { + tagList.add(new TagAndValue(tag, valueBytes)); + } + } + return tagList; + } + + /** + * Filters interesting tags to be displayed in the result view + * + * @param tagList + * @return + */ + public static List filterTagsForResult(Context ctx, + List tagList) { + List resultList = new ArrayList(); + String tagBytesHexString; + + for (TagAndValue tagAndValue : tagList) { + tagBytesHexString = bytesToHex(tagAndValue.getTag().getTagBytes()); + + // Expiration date + if ("5F24".equalsIgnoreCase(tagBytesHexString)) { + try { + InfoKeyValuePair expirationDate = new InfoKeyValuePair(ctx + .getResources().getString( + R.string.lbl_expiration_date), + formatDateOnly(getDateFromBcdBytes(tagAndValue + .getValue()))); + resultList.add(expirationDate); + } catch (ParseException e) { + // dont add in case we cannot parse + Log.w(TAG, "cannot parse expiration date!", e); + } + } + // Effective date + else if ("5F25".equalsIgnoreCase(tagBytesHexString)) { + try { + InfoKeyValuePair expirationDate = new InfoKeyValuePair(ctx + .getResources().getString( + R.string.lbl_effective_date), + formatDateOnly(getDateFromBcdBytes(tagAndValue + .getValue()))); + resultList.add(expirationDate); + } catch (ParseException e) { + // dont add in case we cannot parse + Log.w(TAG, "cannot parse effective date!", e); + } + } + // Account Number + else if ("5A".equalsIgnoreCase(tagBytesHexString)) { + if (tagAndValue.getValue() != null + && tagAndValue.getValue().length > 1) { + String primaryAccountNumber = bytesToHex(tagAndValue + .getValue()); + // last character is always F: cut it off: + primaryAccountNumber = primaryAccountNumber.substring(0, + primaryAccountNumber.length() - 1); + resultList.add(new InfoKeyValuePair(ctx.getResources() + .getString(R.string.lbl_primary_account_number), + prettyPrintString(primaryAccountNumber, 4))); + } + } + + // TODO: look for other interesting EMV tags (even if they are not + // present on my card) + + // Log.d(TAG, " name: " + tagAndValue.getTag().getName()); + // Log.d(TAG, " tag: " + // + bytesToHex(tagAndValue.getTag().getTagBytes())); + // Log.d(TAG, + // " val: " + // + getTagValueAsString(tagAndValue.getTag(), + // tagAndValue.getValue())); + + } + return resultList; + } + /** * checks if the given 3 byte long array looks like a valid BCD encoded date * value @@ -1073,7 +1210,7 @@ private static String getFormattedTagAndLength(byte[] data, int indentLength) { EmvTag tag = EMVTags.getNotNull(readTagIdBytes(stream)); int length = readTagLength(stream); - buf.append(prettyPrintHexString(bytesToHex(tag.getTagBytes()))); + buf.append(prettyPrintString(bytesToHex(tag.getTagBytes()), 2)); buf.append(" ("); buf.append(bytesToHex(intToByteArray(length))); buf.append(" bytes) -> "); @@ -1091,7 +1228,7 @@ private static String getFormattedTagAndLength(byte[] data, int indentLength) { * @param value * @return */ - private static String getTagValueAsString(EmvTag tag, byte[] value) { + private static String getTagValueInfo(EmvTag tag, byte[] value) { StringBuilder buf = new StringBuilder(); switch (tag.getTagValueType()) { case TEXT: diff --git a/src/at/zweng/bankomatinfos/iso7816emv/NfcBankomatCardReader.java b/src/at/zweng/bankomatinfos/iso7816emv/NfcBankomatCardReader.java index 3302a57..716ca17 100644 --- a/src/at/zweng/bankomatinfos/iso7816emv/NfcBankomatCardReader.java +++ b/src/at/zweng/bankomatinfos/iso7816emv/NfcBankomatCardReader.java @@ -1,10 +1,47 @@ package at.zweng.bankomatinfos.iso7816emv; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.APPLICATION_ID_EMV_MAESTRO_BANKOMAT; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.APPLICATION_ID_QUICK; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.EMV_COMMAND_GET_DATA_ALL_COMMON_BER_TLV; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.EMV_COMMAND_GET_DATA_ALL_COMMON_SIMPLE_TLV; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.EMV_COMMAND_GET_DATA_APP_TX_COUNTER; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.EMV_COMMAND_GET_DATA_LAST_ONLINE_APP_TX_COUNTER; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.EMV_COMMAND_GET_DATA_LOG_FORMAT; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.EMV_COMMAND_GET_DATA_PIN_RETRY_COUNTER; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.ISO_COMMAND_QUICK_READ_BALANCE; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.ISO_COMMAND_QUICK_READ_CURRENCY; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.createApduVerifyPIN; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.createGetProcessingOptionsApdu; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.createReadRecordApdu; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.createSelectAid; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.createSelectMasterFile; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.createSelectParentDfFile; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.filterTagsForResult; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.getAmountFromBcdBytes; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.getAmountFromBytes; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.getCurrencyAsString; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.getNextTLV; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.getTagsFromBerTlvAPDUResponse; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.getTimeStampFromBcdBytes; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.isStatusSuccess; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.prettyPrintBerTlvAPDUResponse; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.responsePduLooksLikeTxLogEntry; +import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.statusToString; +import static at.zweng.bankomatinfos.util.Utils.TAG; +import static at.zweng.bankomatinfos.util.Utils.byteArrayToInt; +import static at.zweng.bankomatinfos.util.Utils.bytesToHex; +import static at.zweng.bankomatinfos.util.Utils.cutoffLast2Bytes; +import static at.zweng.bankomatinfos.util.Utils.fromHexString; +import static at.zweng.bankomatinfos.util.Utils.getByteArrayPart; +import static at.zweng.bankomatinfos.util.Utils.getLast2Bytes; +import static at.zweng.bankomatinfos.util.Utils.prettyPrintString; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import android.content.Context; import android.nfc.Tag; import android.nfc.tech.IsoDep; import android.util.Log; @@ -13,8 +50,6 @@ import at.zweng.bankomatinfos.exceptions.TlvParsingException; import at.zweng.bankomatinfos.model.CardInfo; import at.zweng.bankomatinfos.model.TransactionLogEntry; -import static at.zweng.bankomatinfos.util.Utils.*; -import static at.zweng.bankomatinfos.iso7816emv.EmvUtils.*; /** * Performs all the reading operations on a card. @@ -25,16 +60,20 @@ public class NfcBankomatCardReader { private Tag _nfcTag; private IsoDep _localIsoDep; private AppController _ctl; + private List _tagList; + private Context _ctx; /** * Constructor * * @param _nfcTag */ - public NfcBankomatCardReader(Tag nfcTag) { + public NfcBankomatCardReader(Tag nfcTag, Context ctx) { super(); this._nfcTag = nfcTag; this._ctl = AppController.getInstance(); + this._tagList = new ArrayList(); + this._ctx = ctx; } /** @@ -71,14 +110,14 @@ public void disconnectIsoDep() throws IOException { */ public CardInfo readAllCardData(boolean performFullFileScan) throws IOException { - CardInfo result = new CardInfo(); + CardInfo result = new CardInfo(_ctx); _ctl.log("Starting to read data from card.."); result.setNfcTagId(_nfcTag.getId()); _ctl.log("NFC Tag ID: " - + prettyPrintHexString(bytesToHex(_nfcTag.getId()))); + + prettyPrintString(bytesToHex(_nfcTag.getId()), 2)); _ctl.log("Historical bytes: " - + prettyPrintHexString(bytesToHex(_localIsoDep - .getHistoricalBytes()))); + + prettyPrintString( + bytesToHex(_localIsoDep.getHistoricalBytes()), 2)); result = readQuickInfos(result); result = readMaestroCardInfos(result, performFullFileScan); _ctl.log("FINISHED! :-)"); @@ -202,6 +241,8 @@ private CardInfo readMaestroEmvData(byte[] selectAidResponse, tryToReadAllCommonBerTlvTags(); // result = tryreadingTests(result); result = searchForFiles(result, fullFileScan, true); + + result.addKeyValuePairs(filterTagsForResult(_ctx, _tagList)); return result; } @@ -338,28 +379,19 @@ private CardInfo tryReadingTests(CardInfo result) throws IOException { // byte[] cmd; byte[] resultPdu; // - // _ctl.log("trying to send command GET CHALLENGE: "); - // resultPdu = _localIsoDep.transceive(EMV_COMMAND_GET_CHALLENGE); - // logResultPdu(resultPdu); + // DANGEROUS!!!!!! + // DANGEROUS!!!!!! + // DANGEROUS!!!!!! // - // _ctl.log("trying to send command GET CHALLENGE: "); - // resultPdu = _localIsoDep.transceive(EMV_COMMAND_GET_CHALLENGE); - // logResultPdu(resultPdu); + // GET CHALLENGE is an active command which may change the state in your + // card!!! Only perform if you know what you do!!! // - // _ctl.log("trying to send command GET CHALLENGE: "); - // resultPdu = _localIsoDep.transceive(EMV_COMMAND_GET_CHALLENGE); - // logResultPdu(resultPdu); - // - // _ctl.log("trying to send command GET CHALLENGE: "); - // resultPdu = _localIsoDep.transceive(EMV_COMMAND_GET_CHALLENGE); - // logResultPdu(resultPdu); // // _ctl.log("trying to send command GET CHALLENGE: "); // resultPdu = _localIsoDep.transceive(EMV_COMMAND_GET_CHALLENGE); // logResultPdu(resultPdu); // // - // // Log.d(TAG, "trying to send SELECT COMMAND 3F 00: "); // cmd = createSelectFile(fromHexString("3F 00")); // Log.d(TAG, "sending: " + bytesToHex(cmd)); @@ -534,7 +566,7 @@ private TransactionLogEntry tryParseTxLogEntryFromByteArray(byte[] rawRecord) { // only continue if record is at least 24(+2 status) bytes long Log.w(TAG, "parseTxLogEntryFromByteArray: byte array is not long enough:\n" - + prettyPrintHexString(bytesToHex(rawRecord))); + + prettyPrintString(bytesToHex(rawRecord), 2)); return null; } @@ -549,8 +581,7 @@ private TransactionLogEntry tryParseTxLogEntryFromByteArray(byte[] rawRecord) { tx.setAmount(getAmountFromBcdBytes(getByteArrayPart(rawRecord, 1, 6))); tx.setUnknownByte(rawRecord[20]); - tx.setApplicationDefaultAction(getByteArrayPart( - rawRecord, 14, 19)); + tx.setApplicationDefaultAction(getByteArrayPart(rawRecord, 14, 19)); // if record has only 24 bytes then there is no cust excl data // as it starts at byte 25 @@ -566,7 +597,7 @@ private TransactionLogEntry tryParseTxLogEntryFromByteArray(byte[] rawRecord) { } catch (Exception e) { String msg = "Exception while trying to parse transaction entry: " + e + "\n" + e.getMessage() + "\nraw byte array:\n" - + prettyPrintHexString(bytesToHex(rawRecord)); + + prettyPrintString(bytesToHex(rawRecord), 2); Log.w(TAG, msg, e); _ctl.log(msg); return null; @@ -682,14 +713,14 @@ private byte[] getQuickCardCurrencyBytes() throws IOException, + ". In hex: " + bytesToHex(resultPdu) + "\nThe complete response was:\n" - + prettyPrintHexString(bytesToHex(resultPdu)); + + prettyPrintString(bytesToHex(resultPdu), 2); Log.w(TAG, msg); throw new TlvParsingException(msg); } byte[] rawCurrency = new byte[2]; System.arraycopy(resultPdu, 0, rawCurrency, 0, 2); _ctl.log("QUICK currency = " - + prettyPrintHexString(bytesToHex(rawCurrency))); + + prettyPrintString(bytesToHex(rawCurrency), 2)); _ctl.log("QUICK currency = " + getCurrencyAsString(rawCurrency)); return rawCurrency; } @@ -720,12 +751,14 @@ private byte[] selectApplicationGetBytes(byte[] appId) throws IOException { */ private void logResultPdu(byte[] resultPdu) { Log.d(TAG, "received: " + bytesToHex(resultPdu)); - Log.d(TAG, "status: " - + prettyPrintHexString(bytesToHex(getLast2Bytes(resultPdu)))); + Log.d(TAG, + "status: " + + prettyPrintString( + bytesToHex(getLast2Bytes(resultPdu)), 2)); Log.d(TAG, "status: " + statusToString(getLast2Bytes(resultPdu))); _ctl.log("received: " + bytesToHex(resultPdu)); _ctl.log("status: " - + prettyPrintHexString(bytesToHex(getLast2Bytes(resultPdu))) + + prettyPrintString(bytesToHex(getLast2Bytes(resultPdu)), 2) + " - " + statusToString(getLast2Bytes(resultPdu))); } @@ -737,15 +770,16 @@ private void logResultPdu(byte[] resultPdu) { private void logBerTlvResponse(byte[] resultPdu) { if (resultPdu.length > 2) { try { + byte[] data = cutoffLast2Bytes(resultPdu); _ctl.log("Trying to decode response as BER-TLV.."); - _ctl.log(prettyPrintBerTlvAPDUResponse( - cutoffLast2Bytes(resultPdu), 0)); + _ctl.log(prettyPrintBerTlvAPDUResponse(data, 0)); + // and add all found tags to list + _tagList.addAll(getTagsFromBerTlvAPDUResponse(data)); } catch (TlvParsingException e) { _ctl.log("decoding error... maybe this data is not BER-TLV encoded?"); Log.w(TAG, "exception while parsing BER-TLV PDU response\n" - + prettyPrintHexString(bytesToHex(resultPdu)), e); + + prettyPrintString(bytesToHex(resultPdu), 2), e); } } } - } \ No newline at end of file diff --git a/src/at/zweng/bankomatinfos/iso7816emv/TagAndValue.java b/src/at/zweng/bankomatinfos/iso7816emv/TagAndValue.java new file mode 100644 index 0000000..0a8ab43 --- /dev/null +++ b/src/at/zweng/bankomatinfos/iso7816emv/TagAndValue.java @@ -0,0 +1,41 @@ +package at.zweng.bankomatinfos.iso7816emv; + +/** + * Represents simple (not constructed) EMV tags and the corresponding value + * bytes read from the card + * + * @author Johannes Zweng + * + */ +public class TagAndValue { + + private final EmvTag _tag; + private final byte[] _value; + + /** + * Constructor + * + * @param _tag + * @param _value + */ + public TagAndValue(EmvTag tag, byte[] value) { + super(); + this._tag = tag; + this._value = value; + } + + /** + * @return the _tag + */ + public EmvTag getTag() { + return _tag; + } + + /** + * @return the _value + */ + public byte[] getValue() { + return _value; + } + +} diff --git a/src/at/zweng/bankomatinfos/model/CardInfo.java b/src/at/zweng/bankomatinfos/model/CardInfo.java index d1eae3a..7843163 100644 --- a/src/at/zweng/bankomatinfos/model/CardInfo.java +++ b/src/at/zweng/bankomatinfos/model/CardInfo.java @@ -4,6 +4,10 @@ import java.util.Arrays; import java.util.List; +import android.content.Context; +import at.zweng.bankomatinfos.R; +import static at.zweng.bankomatinfos.util.Utils.*; + /** * Represents the data read from a bankomat card. * @@ -17,17 +21,21 @@ public class CardInfo { private long _quickBalance; private int _pinRetryCounter; private String _quickCurrency; + private Context _ctx; private List _transactionLog; + private List _infoKeyValuePairs; /** * Constructor */ - public CardInfo() { + public CardInfo(Context ctx) { // create empty list this._transactionLog = new ArrayList(); + this._infoKeyValuePairs = new ArrayList(); this._pinRetryCounter = -1; this._quickCurrency = ""; + this._ctx = ctx; } /** @@ -43,6 +51,9 @@ public byte[] getNfcTagId() { */ public void setNfcTagId(byte[] nfcTagId) { this._nfcTagId = nfcTagId; + this.addKeyValuePair(new InfoKeyValuePair(_ctx.getResources() + .getString(R.string.lbl_nfc_tag_id), "0x" + + bytesToHex(nfcTagId))); } /** @@ -60,6 +71,31 @@ public void setTransactionLog(List transactionLog) { this._transactionLog = transactionLog; } + /** + * @return the _infoKeyValuePairs + */ + public List getInfoKeyValuePairs() { + return _infoKeyValuePairs; + } + + /** + * Add a info key-value pair + * + * @param pair + */ + public void addKeyValuePair(InfoKeyValuePair pair) { + _infoKeyValuePairs.add(pair); + } + + /** + * Add a list of key-value pairs + * + * @param pair + */ + public void addKeyValuePairs(List pairs) { + _infoKeyValuePairs.addAll(pairs); + } + /** * @return the _quickCard */ @@ -73,6 +109,10 @@ public boolean isQuickCard() { */ public void setQuickCard(boolean quickCard) { this._quickCard = quickCard; + this.addKeyValuePair(new InfoKeyValuePair(_ctx.getResources() + .getString(R.string.lbl_is_quick_card), quickCard ? _ctx + .getResources().getString(R.string.yes) : _ctx.getResources() + .getString(R.string.no))); } /** @@ -88,6 +128,10 @@ public boolean isMaestroCard() { */ public void setMaestroCard(boolean maestroCard) { this._maestroCard = maestroCard; + this.addKeyValuePair(new InfoKeyValuePair(_ctx.getResources() + .getString(R.string.lbl_is_maestro_card), maestroCard ? _ctx + .getResources().getString(R.string.yes) : _ctx.getResources() + .getString(R.string.no))); } /** @@ -103,6 +147,9 @@ public long getQuickBalance() { */ public void setQuickBalance(long quickBalance) { this._quickBalance = quickBalance; + this.addKeyValuePair(new InfoKeyValuePair(_ctx.getResources() + .getString(R.string.lbl_quick_balance), + formatBalance(quickBalance))); } /** @@ -118,6 +165,8 @@ public String getQuickCurrency() { */ public void setQuickCurrency(String quickCurrency) { this._quickCurrency = quickCurrency; + this.addKeyValuePair(new InfoKeyValuePair(_ctx.getResources() + .getString(R.string.lbl_quick_currency), quickCurrency)); } /** @@ -133,6 +182,9 @@ public int getPinRetryCounter() { */ public void setPinRetryCounter(int pinRetryCounter) { this._pinRetryCounter = pinRetryCounter; + this.addKeyValuePair(new InfoKeyValuePair(_ctx.getResources() + .getString(R.string.lbl_remaining_pin_retries), Integer + .toString(pinRetryCounter))); } /* diff --git a/src/at/zweng/bankomatinfos/model/InfoKeyValuePair.java b/src/at/zweng/bankomatinfos/model/InfoKeyValuePair.java new file mode 100644 index 0000000..66f2f99 --- /dev/null +++ b/src/at/zweng/bankomatinfos/model/InfoKeyValuePair.java @@ -0,0 +1,32 @@ +package at.zweng.bankomatinfos.model; + +public class InfoKeyValuePair { + private final String _name; + private final String _value; + + /** + * + * @param _name + * @param _value + */ + public InfoKeyValuePair(String name, String value) { + super(); + this._name = name; + this._value = value; + } + + /** + * @return the _name + */ + public String getName() { + return _name; + } + + /** + * @return the _value + */ + public String getValue() { + return _value; + } + +} diff --git a/src/at/zweng/bankomatinfos/ui/DonateDialogFragment.java b/src/at/zweng/bankomatinfos/ui/DonateDialogFragment.java new file mode 100644 index 0000000..915920a --- /dev/null +++ b/src/at/zweng/bankomatinfos/ui/DonateDialogFragment.java @@ -0,0 +1,84 @@ +package at.zweng.bankomatinfos.ui; + +import android.app.DialogFragment; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import at.zweng.bankomatinfos.R; + +/** + * Fragment for about dialog + * + * @author Johannes Zweng + */ +public class DonateDialogFragment extends DialogFragment { + + private final static String DONATE_WEBPAGE_URL_BITCOIN_FALLBACK = "http://johannes.zweng.at/donations.html#bitcoin"; + private final static String DONATE_WEBPAGE_URL_BANK = "http://johannes.zweng.at/donations.html#bank"; + private final static String DONATE_WEBPAGE_URL_PAYPAL = "http://johannes.zweng.at/donations.html#paypal"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_donation_dialog, container, + false); + getDialog().setTitle(R.string.donate_dialog_title); + TextView aboutText = (TextView) v.findViewById(R.id.donate_dialog_text); + aboutText.setTextColor(getResources().getColor( + android.R.color.primary_text_light)); + aboutText.setText(Html.fromHtml(getResources().getString( + R.string.donate_text))); + + // close button + Button bitcoin = (Button) v.findViewById(R.id.btnDonateDialogBitcoin); + bitcoin.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + try { + Intent bitcoinIntent = new Intent(); + bitcoinIntent.setAction(Intent.ACTION_VIEW); + bitcoinIntent.setData(Uri + .parse("bitcoin:19bLDxjsV63oF14P38LhDZmfKUApNeqFi6")); + startActivity(bitcoinIntent); + } catch (ActivityNotFoundException anfe) { + // if no app for handling bitcoin URLs is installed: + // go to webpage instead + Intent donateIntent = new Intent(); + donateIntent.setAction(Intent.ACTION_VIEW); + donateIntent.setData(Uri + .parse(DONATE_WEBPAGE_URL_BITCOIN_FALLBACK)); + startActivity(donateIntent); + } + } + }); + Button paypal = (Button) v.findViewById(R.id.btnDonateDialogPaypal); + paypal.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent donateIntent = new Intent(); + donateIntent.setAction(Intent.ACTION_VIEW); + donateIntent.setData(Uri.parse(DONATE_WEBPAGE_URL_PAYPAL)); + startActivity(donateIntent); + } + }); + Button euro = (Button) v.findViewById(R.id.btnDonateDialogEuros); + euro.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent donateIntent = new Intent(); + donateIntent.setAction(Intent.ACTION_VIEW); + donateIntent.setData(Uri.parse(DONATE_WEBPAGE_URL_BANK)); + startActivity(donateIntent); + } + }); + return v; + } +} diff --git a/src/at/zweng/bankomatinfos/ui/ListAdapterInfos.java b/src/at/zweng/bankomatinfos/ui/ListAdapterInfos.java new file mode 100644 index 0000000..91d1e8a --- /dev/null +++ b/src/at/zweng/bankomatinfos/ui/ListAdapterInfos.java @@ -0,0 +1,71 @@ +/** + * + */ +package at.zweng.bankomatinfos.ui; + +import java.util.List; + +import android.app.Activity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import at.zweng.bankomatinfos.AppController; +import at.zweng.bankomatinfos.R; +import at.zweng.bankomatinfos.model.InfoKeyValuePair; + +/** + * Custom list adapter for the card infos list + * + * @author Johannes Zweng + */ +public class ListAdapterInfos extends BaseAdapter { + + private Context _context; + private List _infoList; + + /** + * Constructor + */ + public ListAdapterInfos(Context ctx) { + this._context = ctx; + this._infoList = AppController.getInstance().getCardInfo() + .getInfoKeyValuePairs(); + } + + @Override + public int getCount() { + return _infoList.size(); + } + + @Override + public Object getItem(int position) { + return _infoList.get(position); + } + + @Override + public long getItemId(int position) { + // we simply use position in list as ID for events + return position; + } + + @Override + public View getView(int position, View v, ViewGroup parent) { + InfoKeyValuePair infoItem; + infoItem = _infoList.get(position); + if (v == null) { + LayoutInflater mInflater = (LayoutInflater) _context + .getSystemService(Activity.LAYOUT_INFLATER_SERVICE); + v = mInflater.inflate(R.layout.list_item_general_info, null); + } + + TextView infoLabel = (TextView) v.findViewById(R.id.infoListItemName); + TextView infoValue = (TextView) v.findViewById(R.id.infoListItemValue); + infoLabel.setText(infoItem.getName()); + infoValue.setText(infoItem.getValue()); + return v; + } + +} diff --git a/src/at/zweng/bankomatinfos/ui/ListAdapterTransactions.java b/src/at/zweng/bankomatinfos/ui/ListAdapterTransactions.java index a29d317..48fa517 100644 --- a/src/at/zweng/bankomatinfos/ui/ListAdapterTransactions.java +++ b/src/at/zweng/bankomatinfos/ui/ListAdapterTransactions.java @@ -103,13 +103,13 @@ public View getView(int position, View v, ViewGroup parent) { cryptogramInformation.setText("0x" + byte2Hex(tx.getCryptogramInformationData())); atc.setText(Integer.toString(tx.getAtc())); - appDefaultAction.setText(prettyPrintHexString(bytesToHex(tx - .getApplicationDefaultAction()))); + appDefaultAction.setText(prettyPrintString(bytesToHex(tx + .getApplicationDefaultAction()),2)); unknownByte.setText(byte2Hex(tx.getUnknownByte())); - customerEsclusive.setText(prettyPrintHexString(bytesToHex(tx - .getCustomerExclusiveData()))); + customerEsclusive.setText(prettyPrintString(bytesToHex(tx + .getCustomerExclusiveData()),2)); - rawData.setText(prettyPrintHexString(bytesToHex(tx.getRawEntry()))); + rawData.setText(prettyPrintString(bytesToHex(tx.getRawEntry()),2)); } return v; } diff --git a/src/at/zweng/bankomatinfos/ui/MainActivity.java b/src/at/zweng/bankomatinfos/ui/MainActivity.java index 3a2dad9..c66f52e 100644 --- a/src/at/zweng/bankomatinfos/ui/MainActivity.java +++ b/src/at/zweng/bankomatinfos/ui/MainActivity.java @@ -40,6 +40,7 @@ public class MainActivity extends Activity { private IntentFilter[] _filters; private String[][] _techLists; private NfcAdapter _nfcAdapter; + // View elements private View _viewNfcLogo; private View _viewTextViewShowCard; @@ -86,14 +87,6 @@ protected void onResume() { _nfcAdapter.enableForegroundDispatch(this, _pendingIntent, _filters, _techLists); } - Intent intent = getIntent(); - if (intent != null - && NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) { - Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); - if (tag != null) { - handleTag(tag); - } - } } @Override @@ -124,6 +117,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_about: showAboutDialog(getFragmentManager()); return true; + case R.id.action_donate: + showDonationDialog(getFragmentManager()); + return true; case R.id.action_changelog: showChangelogDialog(getFragmentManager(), true); return true; @@ -218,13 +214,14 @@ protected Boolean doInBackground(Void... params) { ctl.clearLog(); ctl.log(getResources().getString(R.string.app_name) + " version " + getAppVersion(MainActivity.this)); - NfcBankomatCardReader reader = new NfcBankomatCardReader(nfcTag); + NfcBankomatCardReader reader = new NfcBankomatCardReader( + nfcTag, MainActivity.this); reader.connectIsoDep(); // read setting value SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(MainActivity.this); _cardReadingResults = reader.readAllCardData(prefs.getBoolean( - "perform_full_file_scan", true)); + "perform_full_file_scan", false)); ctl.setCardInfo(_cardReadingResults); reader.disconnectIsoDep(); } catch (NoSmartCardException nsce) { diff --git a/src/at/zweng/bankomatinfos/ui/NfcDisabledActivity.java b/src/at/zweng/bankomatinfos/ui/NfcDisabledActivity.java index 642485c..a796fb5 100644 --- a/src/at/zweng/bankomatinfos/ui/NfcDisabledActivity.java +++ b/src/at/zweng/bankomatinfos/ui/NfcDisabledActivity.java @@ -2,6 +2,7 @@ import static at.zweng.bankomatinfos.util.Utils.showAboutDialog; import static at.zweng.bankomatinfos.util.Utils.showChangelogDialog; +import static at.zweng.bankomatinfos.util.Utils.showDonationDialog; import android.app.Activity; import android.content.ComponentName; import android.content.Intent; @@ -37,6 +38,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_about: showAboutDialog(getFragmentManager()); return true; + case R.id.action_donate: + showDonationDialog(getFragmentManager()); + return true; case R.id.action_changelog: showChangelogDialog(getFragmentManager(), true); return true; diff --git a/src/at/zweng/bankomatinfos/ui/ResultActivity.java b/src/at/zweng/bankomatinfos/ui/ResultActivity.java index 37f06e1..f71f08b 100644 --- a/src/at/zweng/bankomatinfos/ui/ResultActivity.java +++ b/src/at/zweng/bankomatinfos/ui/ResultActivity.java @@ -2,6 +2,7 @@ import static at.zweng.bankomatinfos.util.Utils.showAboutDialog; import static at.zweng.bankomatinfos.util.Utils.showChangelogDialog; +import static at.zweng.bankomatinfos.util.Utils.showDonationDialog; import java.util.Locale; @@ -53,7 +54,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_result); - _fragmentResultInfos = new ResultInfosFragment(); + _fragmentResultInfos = new ResultInfosListFragment(); _fragmentResultTxList = new ResultTxListFragment(); _fragmentResultLog = new ResultLogFragment(); @@ -136,6 +137,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_about: showAboutDialog(getFragmentManager()); return true; + case R.id.action_donate: + showDonationDialog(getFragmentManager()); + return true; case R.id.action_changelog: showChangelogDialog(getFragmentManager(), true); return true; diff --git a/src/at/zweng/bankomatinfos/ui/ResultInfosFragment.java b/src/at/zweng/bankomatinfos/ui/ResultInfosFragment.java deleted file mode 100644 index be4320e..0000000 --- a/src/at/zweng/bankomatinfos/ui/ResultInfosFragment.java +++ /dev/null @@ -1,71 +0,0 @@ -package at.zweng.bankomatinfos.ui; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import at.zweng.bankomatinfos.AppController; -import at.zweng.bankomatinfos.R; -import at.zweng.bankomatinfos.model.CardInfo; -import static at.zweng.bankomatinfos.util.Utils.*; - -/** - * A simple fragment subclass, showing the general result tab. - */ -public class ResultInfosFragment extends Fragment { - - private TextView _tvNfcTagId; - private TextView _tvIsQuickCard; - private TextView _tvQuickBalance; - private TextView _tvQuickCurrency; - private TextView _tvIsMaestroCard; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.fragment_result_infos, container, - false); - _tvNfcTagId = (TextView) v.findViewById(R.id.valueNfcTagId); - _tvIsQuickCard = (TextView) v.findViewById(R.id.valueIsQuickCard); - _tvQuickBalance = (TextView) v.findViewById(R.id.valueQuickBalance); - _tvQuickCurrency = (TextView) v.findViewById(R.id.valueQuickCurrency); - _tvIsMaestroCard = (TextView) v.findViewById(R.id.valueIsMaestroCard); - - loadDataIntoUi(); - return v; - } - - /** - * load values into UI - */ - private void loadDataIntoUi() { - AppController controller = AppController.getInstance(); - CardInfo cardInfo = controller.getCardInfo(); - if (cardInfo == null) { - Log.e(TAG, "card info object is null"); - return; - } - _tvNfcTagId.setText("0x" + bytesToHex(cardInfo.getNfcTagId())); - - if (cardInfo.isQuickCard()) { - _tvIsQuickCard.setText(getResources().getString(R.string.yes)); - _tvQuickBalance.setText(formatBalance(cardInfo.getQuickBalance())); - _tvQuickCurrency.setText(cardInfo.getQuickCurrency()); - } else { - _tvIsQuickCard.setText(getResources().getString(R.string.no)); - _tvQuickBalance.setText("-"); - _tvQuickCurrency.setText("-"); - } - - if (cardInfo.isMaestroCard()) { - _tvIsMaestroCard.setText(getResources().getString(R.string.yes)); - } else { - _tvIsMaestroCard.setText(getResources().getString(R.string.no)); - } - - } - -} diff --git a/src/at/zweng/bankomatinfos/ui/ResultInfosListFragment.java b/src/at/zweng/bankomatinfos/ui/ResultInfosListFragment.java new file mode 100644 index 0000000..e8c37f8 --- /dev/null +++ b/src/at/zweng/bankomatinfos/ui/ResultInfosListFragment.java @@ -0,0 +1,44 @@ +package at.zweng.bankomatinfos.ui; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import android.widget.TextView; +import at.zweng.bankomatinfos.R; + +/** + * A simple Fragment subclass, showing the list of infos. + */ +public class ResultInfosListFragment extends Fragment { + + private ListView _listView; + private TextView _noEntriesText; + private ListAdapterInfos _listAdapter; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_result_tx_list, container, + false); + _listView = (ListView) v.findViewById(R.id.listviewTxList); + _noEntriesText = (TextView) v.findViewById(R.id.lblNoEntriesAvailable); + _listAdapter = new ListAdapterInfos(getActivity()); + _listView.setAdapter(_listAdapter); + showNoResultText(_listAdapter.getCount() == 0); + return v; + } + + /** + * Show or hide the no results text + * + * @param show + */ + private void showNoResultText(boolean show) { + _listView.setVisibility(show ? View.GONE : View.VISIBLE); + _noEntriesText.setVisibility(show ? View.VISIBLE : View.GONE); + } + +} diff --git a/src/at/zweng/bankomatinfos/util/Utils.java b/src/at/zweng/bankomatinfos/util/Utils.java index ed5c10d..d3ce0a0 100644 --- a/src/at/zweng/bankomatinfos/util/Utils.java +++ b/src/at/zweng/bankomatinfos/util/Utils.java @@ -23,6 +23,7 @@ import at.zweng.bankomatinfos.R; import at.zweng.bankomatinfos.ui.AboutDialogFragment; import at.zweng.bankomatinfos.ui.ChangelogDialogFragment; +import at.zweng.bankomatinfos.ui.DonateDialogFragment; /** * Some static helper methods @@ -39,6 +40,9 @@ public class Utils { private static SimpleDateFormat fullTimeWithDateFormat = new SimpleDateFormat( "dd.MM.yyyy HH:mm:ss", Locale.US); + private static SimpleDateFormat dateOnlyDateFormat = new SimpleDateFormat( + "dd.MM.yyyy", Locale.US); + private final static SimpleDateFormat fullTimeMilliseconds = new SimpleDateFormat( "HH:mm:ss.SSS", Locale.US); @@ -91,13 +95,13 @@ public static String byte2Hex(byte b) { * hex string (or any other string) (ex: "0011AAEEFF") * @return string with inserted whitespaces (ex: "00 11 AA EE FF") */ - public static String prettyPrintHexString(String in) { + public static String prettyPrintString(String in, int groupCount) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < in.length(); i++) { char c = in.charAt(i); buf.append(c); int nextPos = i + 1; - if (nextPos % 2 == 0 && nextPos != in.length()) { + if (nextPos % groupCount == 0 && nextPos != in.length()) { buf.append(" "); } } @@ -219,6 +223,16 @@ public static String formatDateWithTime(Date d) { return fullTimeWithDateFormat.format(d); } + /** + * format date + * + * @param d + * @return + */ + public static String formatDateOnly(Date d) { + return dateOnlyDateFormat.format(d); + } + /** * Integer to hex string * @@ -588,6 +602,14 @@ public static void showAboutDialog(FragmentManager fm) { aboutFragment.show(fm, "dialog_about"); } + /** + * show donation dialog + */ + public static void showDonationDialog(FragmentManager fm) { + DialogFragment donateFragment = new DonateDialogFragment(); + donateFragment.show(fm, "dialog_donate"); + } + /** * show changelog dialog * @@ -627,7 +649,7 @@ public static Spanned getAboutDialogText(Context ctx) { sb.append("
Johannes Zweng
"); sb.append("android-dev@zweng.at

"); - sb.append("Be curious! Have fun! :-)"); + sb.append("Be curious! Have fun! :-)"); sb.append("

"); // SOURCECODE