├── res
├── drawable
│ ├── btn_open.png
│ ├── background.png
│ └── btn_write.png
├── mipmap-hdpi
│ └── ic_launcher.png
├── mipmap-mdpi
│ └── ic_launcher.png
├── mipmap-xhdpi
│ └── ic_launcher.png
├── mipmap-xxhdpi
│ └── ic_launcher.png
├── mipmap-xxxhdpi
│ └── ic_launcher.png
├── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── xml
│ └── nfc_tech_filter.xml
├── values-v21
│ └── styles.xml
├── values-w820dp
│ └── dimens.xml
└── layout
│ ├── dump_list_item.xml
│ ├── content_main.xml
│ ├── activity_dump_list.xml
│ └── activity_main.xml
├── .gitignore
├── supported_phones_list.txt
├── java
└── cc
│ └── troikadumper
│ ├── utils
│ └── HexUtils.java
│ ├── DumpListAdapter.java
│ ├── DumpListActivity.java
│ ├── Dump.java
│ └── MainActivity.java
├── AndroidManifest.xml
└── README.md
/res/drawable/btn_open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/drawable/btn_open.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .DS_Store?
3 | ._*
4 | .Spotlight-V100
5 | .Trashes
6 | ehthumbs.db
7 | Thumbs.db
8 |
--------------------------------------------------------------------------------
/res/drawable/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/drawable/background.png
--------------------------------------------------------------------------------
/res/drawable/btn_write.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/drawable/btn_write.png
--------------------------------------------------------------------------------
/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gshevtsov/TroikaDumper/HEAD/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #24768A
4 | #1e5571
5 | #58d2ff
6 |
7 |
--------------------------------------------------------------------------------
/res/xml/nfc_tech_filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | android.nfc.tech.NfcA
5 | android.nfc.tech.MifareClassic
6 |
7 |
--------------------------------------------------------------------------------
/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 | >
2 |
8 |
9 |
--------------------------------------------------------------------------------
/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TroikaDumper
3 | View dumps
4 | Diff tool
5 | Write dump
6 | No NFC adapter on your device
7 | Enable NFC in settings and restart this app
8 | All your dumps
9 | are belong to us
10 |
11 |
--------------------------------------------------------------------------------
/res/layout/dump_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
--------------------------------------------------------------------------------
/res/layout/activity_dump_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
18 |
19 |
20 |
21 |
27 |
28 |
--------------------------------------------------------------------------------
/supported_phones_list.txt:
--------------------------------------------------------------------------------
1 | ALCATEL ONETOUCH POP S9
2 | ALCATEL ONETOUCH IDOL2 Mini S
3 | Acer Liquid Express
4 | Acer Liquid Glow
5 | Asus Padfone 2
6 | Asus Padfone S
7 | HTC One
8 | HTC Desire 610
9 | HTC One X
10 | HTC One M8
11 | HTC One M9
12 | HTC One Max
13 | HUAWEI ASCENT P7
14 | Google Nexus 7 (2012 c NXP PN65)
15 | Lenovo Sisley S90
16 | Lenovo VIBE Z2
17 | Lenovo P780
18 | Lenovo Vibe Z2 Pro (K920)
19 | LG Optimus 3D Max
20 | LG Optimus G
21 | LG Optimus G Pro
22 | LG Optimus 4X HD
23 | LG Optimus L5
24 | LG Optimus L7
25 | LG Optimus LTE
26 | LG Optimus Vu
27 | LG Prada 3.0
28 | LG Optimus Vu 2
29 | LG G3
30 | LG G4
31 | Motorola Droid Razr
32 | Motorola Droid Razr HD
33 | Motorola Droid Razr Maxx HD
34 | Motorola Moto X
35 | Samsung Galaxy Nexus I9250
36 | Samsung Galaxy Note II
37 | Samsung Galaxy SIII
38 | Samsung Galaxy SIII Neo
39 | Samsung Core, Samsung Core DUOS
40 | Samsung Galaxy A7
41 | Samsung Galaxy S5 G900F
42 | Sony Xperia ZR
43 | Sony Xperia Z
44 | Sony Xperia М1
45 | Sony LТ25i/Xperia V
46 | Sony Xperia Z2
47 | Sony Xperia C3
48 | Sony Xperia C3 Dual
49 | Sony Xperia Z3
50 | Sony Xperia Z3 Dual
51 | Sony Xperia Z3 Compact
52 | Sony Xperia Z3 Tablet Compact
53 | Sony Xperia Z3+
54 | Sony Xperia Z3+ Dual
55 | Panasonic ELUGA
56 | Philips Xenium W336
57 | ZTE Grand S
58 | ZTE PF200
59 | ZTE Grand X
60 | ZTE Render
61 | ZTE Kis
62 | ZTE Sprint Flash
63 | Lumia 640
64 | Lumia 730
65 | Lumia 735
66 | Lumia 810
67 | Lumia 830
68 | Lumia 950XL
69 |
--------------------------------------------------------------------------------
/java/cc/troikadumper/utils/HexUtils.java:
--------------------------------------------------------------------------------
1 | package cc.troikadumper.utils;
2 |
3 | public class HexUtils {
4 |
5 | private static final byte[] HEX_CHAR_TABLE = { (byte) '0', (byte) '1',
6 | (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6',
7 | (byte) '7', (byte) '8', (byte) '9', (byte) 'A', (byte) 'B',
8 | (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F' };
9 |
10 | public static String toString(byte[] raw) {
11 | int len = raw.length;
12 | byte[] hex = new byte[2 * len];
13 | int index = 0;
14 | int pos = 0;
15 |
16 | for (byte b : raw) {
17 | if (pos >= len)
18 | break;
19 |
20 | pos++;
21 | int v = b & 0xFF;
22 | hex[index++] = HEX_CHAR_TABLE[v >>> 4];
23 | hex[index++] = HEX_CHAR_TABLE[v & 0xF];
24 | }
25 |
26 | return new String(hex);
27 | }
28 |
29 | public static byte[] fromString(String hex) {
30 | int len = hex.length();
31 | if (len % 2 == 1) {
32 | throw new IllegalArgumentException("hex length is not even");
33 | }
34 | len = len / 2; // actual
35 |
36 | byte[] bytes = new byte[len];
37 | for (int i = 0; i < len; i++) {
38 | bytes[i] = (byte) (Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16) & 0xFF);
39 | }
40 | return bytes;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
28 |
29 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | TroikaDumper
2 | =======
3 |
4 | Приложение TroikaDumper позволяет читать, сохранять и восстанавливать записанное состояние памяти карты Тройка.
5 | Для использования необходим телефон с **версией Android ≥ 4.4** и NFC чипом, **поддерживающим карты Mifare.**
6 | NFC чип должен быть произодства NXP [wikipedia.org/wiki/List_of_NFC-enabled_mobile_devices](https://en.wikipedia.org/wiki/List_of_NFC-enabled_mobile_devices)
7 | Чипы производства Broadcom и другие работать не будут.
8 |
9 | Неполный список поддерживаемых телефонов [supported_phones_list.txt](https://github.com/gshevtsov/TroikaDumper/blob/master/supported_phones_list.txt)
10 |
11 | ### Инструкция
12 |
13 | 1. Установите приложение скачав его по ссылке [TroikaDumper.apk](https://github.com/gshevtsov/TroikaDumper/releases/download/0.1/TroikaDumper-0.1.apk)
14 |
15 | 2. Запустите приложение и поднести карту Тройка.
16 | Должно отобразится состояние баланса, время последнего прохода и т. д.
17 | При считывании карты состояние памяти автоматически сохраняется и доступно в архиве (нижняя правая кнопка в виде папки)
18 |
19 | 3. Для записи дампа памяти на карту выберите нужный дамп из архива и нажмите кнопку запись.
20 | Кнопка записи находится левее кнопки архива.
21 |
22 | ### Как избежать блокировки карты
23 |
24 | 1. Не оперируйте суммами баланса более 100 рублей
25 |
26 | 2. Никогда не проходите в метро два раза с одинаковым временем последнего прохода. После записи дампа обновите текущее время на карте используя валидатор в наземном транспорте.
27 | То есть, перед каждым проходом в метро нужно выполнить списание через желтый валидатор в автобусе или трамвае.
28 |
29 |
30 | ### Интерфейс программы
31 |
32 | 
33 |
34 |
35 |
--------------------------------------------------------------------------------
/java/cc/troikadumper/DumpListAdapter.java:
--------------------------------------------------------------------------------
1 | package cc.troikadumper;
2 |
3 | import android.content.Context;
4 | import android.widget.ArrayAdapter;
5 |
6 | import java.text.DateFormat;
7 | import java.util.Arrays;
8 | import java.util.Collections;
9 | import java.util.Date;
10 | import java.util.List;
11 | import java.util.regex.Matcher;
12 | import java.util.regex.Pattern;
13 |
14 | public class DumpListAdapter extends ArrayAdapter {
15 | protected static Pattern pattern;
16 |
17 | protected Pattern getPattern() {
18 | if (pattern == null) {
19 | pattern = Pattern.compile(Dump.FILENAME_REGEXP);
20 | }
21 | return pattern;
22 | }
23 |
24 | public class DumpListFilename {
25 |
26 | protected String filename;
27 |
28 | public DumpListFilename(String filename) {
29 | this.filename = filename;
30 | }
31 |
32 | public String getFilename() {
33 | return filename;
34 | }
35 |
36 | public String toString() {
37 | Matcher m = getPattern().matcher(filename);
38 | if (m.matches()) {
39 | String info = "";
40 | Date d = new Date(
41 | Integer.parseInt(m.group(1)) - 1900,
42 | Integer.parseInt(m.group(2)) - 1,
43 | Integer.parseInt(m.group(3)),
44 | Integer.parseInt(m.group(4).substring(0, 2)),
45 | Integer.parseInt(m.group(4).substring(2, 4)),
46 | Integer.parseInt(m.group(4).substring(4, 6))
47 | );
48 | info += DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(d);
49 | info += "\nCard: " + Dump.formatCardNumber(Integer.parseInt(m.group(5))) + " - RUB: " + m.group(6);
50 | return info;
51 | } else {
52 | return "error parsing filename";
53 | }
54 | }
55 | }
56 |
57 | public DumpListAdapter(Context context, String[] filenames) {
58 | super(context, R.layout.dump_list_item, R.id.dump_list_item_label);
59 |
60 | Arrays.sort(filenames);
61 | List filenamesList = Arrays.asList(filenames);
62 | Collections.reverse(filenamesList);
63 |
64 | for (String filename : filenamesList) {
65 | add(new DumpListFilename(filename));
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/java/cc/troikadumper/DumpListActivity.java:
--------------------------------------------------------------------------------
1 | package cc.troikadumper;
2 |
3 |
4 | import android.content.Intent;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.os.Bundle;
7 | import android.support.v7.widget.Toolbar;
8 | import android.view.View;
9 | import android.widget.AbsListView;
10 | import android.widget.AdapterView;
11 | import android.widget.ArrayAdapter;
12 | import android.widget.ListView;
13 | import android.widget.Toast;
14 |
15 | import java.io.File;
16 | import java.io.FilenameFilter;
17 | import java.util.ArrayList;
18 | import java.util.Arrays;
19 | import java.util.Collections;
20 | import java.util.List;
21 | import java.util.Scanner;
22 |
23 | public class DumpListActivity extends AppCompatActivity {
24 |
25 | protected Toolbar toolbar;
26 | protected ListView dumpListView;
27 | protected ArrayAdapter dumpListAdapter;
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | setContentView(R.layout.activity_dump_list);
33 |
34 | File dumpsDir = getApplicationContext().getExternalFilesDir(null);
35 | String[] filenames = dumpsDir.list(new FilenameFilter() {
36 | @Override
37 | public boolean accept(File dir, String filename) {
38 | return filename.matches(Dump.FILENAME_REGEXP);
39 | }
40 | });
41 |
42 | // setup toolbar
43 | toolbar = (Toolbar)findViewById(R.id.toolbar);
44 | if (toolbar != null) {
45 | toolbar.setTitle(R.string.dumplist_title);
46 | toolbar.setSubtitle(R.string.dumplist_subtitle);
47 | setSupportActionBar(toolbar);
48 | if (getSupportActionBar() != null) {
49 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
50 | }
51 | }
52 |
53 | // setup list
54 | dumpListView = (ListView) findViewById(R.id.dumpListView);
55 | dumpListAdapter = new DumpListAdapter(getApplicationContext(), filenames);
56 |
57 | dumpListView.setAdapter(dumpListAdapter);
58 | dumpListView.setClickable(true);
59 | dumpListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
60 | @Override
61 | public void onItemClick(AdapterView> parent, View view, int position, long id) {
62 | DumpListAdapter.DumpListFilename filename = (DumpListAdapter.DumpListFilename) dumpListView.getItemAtPosition(position);
63 | String selectedFilename = filename.getFilename();
64 | Intent intent = new Intent(MainActivity.INTENT_READ_DUMP);
65 | intent.putExtra("filename", selectedFilename);
66 | setResult(RESULT_OK, intent);
67 | finish();
68 | }
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/java/cc/troikadumper/Dump.java:
--------------------------------------------------------------------------------
1 | package cc.troikadumper;
2 |
3 | import android.nfc.Tag;
4 | import android.nfc.tech.MifareClassic;
5 | import android.os.Environment;
6 |
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 | import java.io.OutputStreamWriter;
12 | import java.text.DateFormat;
13 | import java.util.Arrays;
14 | import java.util.Calendar;
15 | import java.util.Date;
16 | import java.util.Scanner;
17 | import java.util.TimeZone;
18 |
19 | import cc.troikadumper.utils.HexUtils;
20 |
21 | public class Dump {
22 | public static final String FILENAME_FORMAT = "%04d-%02d-%02d_%02d%02d%02d_%d_%dRUB.txt";
23 | public static final String FILENAME_REGEXP = "([0-9]{4})-([0-9]{2})-([0-9]{2})_([0-9]{6})_([0-9]+)_([0-9]+)RUB.txt";
24 |
25 | public static final int BLOCK_COUNT = 4;
26 | public static final int BLOCK_SIZE = MifareClassic.BLOCK_SIZE;
27 | public static final int SECTOR_INDEX = 8;
28 |
29 | public static final byte[] KEY_B =
30 | {(byte)0xE3,(byte)0x51,(byte)0x73,(byte)0x49, (byte)0x4A,(byte)0x81};
31 |
32 | public static final byte[] KEY_A =
33 | {(byte)0xA7,(byte)0x3F,(byte)0x5D,(byte)0xC1, (byte)0xD3,(byte)0x33};
34 |
35 | public static final byte[] KEY_0 =
36 | {(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00, (byte)0x00,(byte)0x00};
37 |
38 | // raw
39 | protected byte[] uid;
40 | protected byte[][] data;
41 |
42 | // parsed
43 | protected int cardNumber;
44 | protected int balance;
45 | protected Date lastUsageDate;
46 | protected int lastValidatorId;
47 |
48 | public Dump(byte[] uid, byte[][] sector8) {
49 | this.uid = uid;
50 | this.data = sector8;
51 | parse();
52 | }
53 |
54 | public static Dump fromTag(Tag tag) throws IOException {
55 | MifareClassic mfc = getMifareClassic(tag);
56 |
57 | int blockCount = mfc.getBlockCountInSector(SECTOR_INDEX);
58 | if (blockCount < BLOCK_COUNT) {
59 | throw new IOException("Wtf? Not enough blocks on this card");
60 | }
61 |
62 | byte[][] data = new byte[BLOCK_COUNT][BLOCK_SIZE];
63 |
64 | for (int i = 0; i < BLOCK_COUNT; i++) {
65 | data[i] = mfc.readBlock(mfc.sectorToBlock(SECTOR_INDEX) + i);
66 | }
67 |
68 | return new Dump(tag.getId(), data);
69 | }
70 |
71 | public static Dump fromFile(File file) throws IOException {
72 | FileInputStream fs = new FileInputStream(file);
73 | Scanner scanner = new Scanner(fs, "US-ASCII");
74 | byte[] uid = HexUtils.fromString(scanner.nextLine());
75 |
76 | byte[][] data = new byte[BLOCK_COUNT][BLOCK_SIZE];
77 | for (int i = 0; i < BLOCK_COUNT; i++) {
78 | data[i] = HexUtils.fromString(scanner.nextLine());
79 | }
80 |
81 | return new Dump(uid, data);
82 | }
83 |
84 | protected static MifareClassic getMifareClassic(Tag tag) throws IOException {
85 | MifareClassic mfc = MifareClassic.get(tag);
86 | mfc.connect();
87 |
88 | // fucked up card
89 | if (mfc.authenticateSectorWithKeyA(SECTOR_INDEX, KEY_0) && mfc.authenticateSectorWithKeyB(SECTOR_INDEX, KEY_0)) {
90 | return mfc;
91 | }
92 |
93 | // good card
94 | if (mfc.authenticateSectorWithKeyA(SECTOR_INDEX, KEY_A) && mfc.authenticateSectorWithKeyB(SECTOR_INDEX, KEY_B)
95 | ) {
96 | return mfc;
97 | }
98 |
99 | throw new IOException("No permissions");
100 | }
101 |
102 | protected void parse() {
103 | // block#0 bytes#3-6
104 | cardNumber = intval(data[0][3], data[0][4], data[0][5], data[0][6]) >> 4;
105 |
106 | // block#1 bytes#0-1
107 | lastValidatorId = intval(data[1][0], data[1][1]);
108 |
109 | // block#1 bytes#2-4½
110 | int lastUsageDay = intval(data[1][2], data[1][3]);
111 | if (lastUsageDay > 0) {
112 | double lastUsageTime = (double) intval(
113 | (byte) (data[1][4] >> 4 & 0x0F),
114 | (byte) (data[1][5] >> 4 & 0x0F | data[1][4] << 4 & 0xF0)
115 | );
116 | lastUsageTime = lastUsageTime / 120.0;
117 | int lastUsageHour = (int)Math.floor(lastUsageTime);
118 | int lastUsageMinute = (int)Math.round((lastUsageTime % 1) * 60);
119 |
120 | Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT+3"));
121 | c.set(1992, 0, 1, lastUsageHour, lastUsageMinute);
122 | c.add(Calendar.DATE, lastUsageDay - 1);
123 | lastUsageDate = c.getTime();
124 | } else {
125 | lastUsageDate = null;
126 | }
127 |
128 | // block#1 bytes#8.5-10.5 (??)
129 | balance = intval(
130 | (byte)(data[1][8] & 0b00001111),
131 | (byte) data[1][9], // 87654321
132 | (byte)(data[1][10] & 0b11111000)
133 | ) / 200;
134 | }
135 |
136 | public void write(Tag tag) throws IOException {
137 | MifareClassic mfc = getMifareClassic(tag);
138 |
139 | if (!Arrays.equals(tag.getId(), this.getUid())) {
140 | throw new IOException("Card UID mismatch: \n"
141 | + HexUtils.toString(tag.getId()) + " (card) != "
142 | + HexUtils.toString(getUid()) + " (dump)");
143 | }
144 |
145 | int numBlocksToWrite = BLOCK_COUNT - 1; // do not overwrite last block (keys)
146 | int startBlockIndex = mfc.sectorToBlock(SECTOR_INDEX);
147 | for (int i = 0; i < numBlocksToWrite; i++) {
148 | mfc.writeBlock(startBlockIndex + i, data[i]);
149 | }
150 | }
151 |
152 | public File save(File dir) throws IOException {
153 | String state = Environment.getExternalStorageState();
154 | if (!Environment.MEDIA_MOUNTED.equals(state)) {
155 | throw new IOException("Can not write to external storage");
156 | }
157 |
158 | if (!dir.isDirectory()) {
159 | throw new IOException("Not a dir");
160 | }
161 |
162 | if (!dir.exists() && !dir.mkdirs()) {
163 | throw new IOException("Can not make save dir");
164 | }
165 |
166 | File file = new File(dir, makeFilename());
167 | FileOutputStream stream = new FileOutputStream(file);
168 | OutputStreamWriter out = new OutputStreamWriter(stream);
169 | out.write(getUidAsString() + "\r\n");
170 | for (String block : getDataAsStrings()) {
171 | out.write(block + "\r\n");
172 | }
173 | out.close();
174 |
175 | return file;
176 | }
177 |
178 | protected String makeFilename() {
179 | Date now = new Date();
180 | return String.format(
181 | FILENAME_FORMAT,
182 | now.getYear() + 1900, now.getMonth() + 1, now.getDate(),
183 | now.getHours(), now.getMinutes(), now.getSeconds(),
184 | getCardNumber(), getBalance()
185 | );
186 | }
187 |
188 | public byte[] getUid() {
189 | return uid;
190 | }
191 |
192 | public String getUidAsString() {
193 | return HexUtils.toString(getUid());
194 | }
195 |
196 | public byte[][] getData() {
197 | return data;
198 | }
199 |
200 | public String[] getDataAsStrings() {
201 | String blocks[] = new String[data.length];
202 | for (int i = 0; i < data.length; i++) {
203 | blocks[i] = HexUtils.toString(data[i]);
204 | }
205 | return blocks;
206 | }
207 |
208 | public Date getLastUsageDate() {
209 | return lastUsageDate;
210 | }
211 |
212 | public String getLastUsageDateAsString() {
213 | if (lastUsageDate == null) {
214 | return "";
215 | }
216 | return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(lastUsageDate);
217 | }
218 |
219 | public int getLastValidatorId() {
220 | return lastValidatorId;
221 | }
222 |
223 | public String getLastValidatorIdAsString() {
224 | return "ID# " + getLastValidatorId();
225 | }
226 |
227 | public int getBalance() {
228 | return balance;
229 | }
230 |
231 | public String getBalanceAsString() {
232 | return "" + getBalance() + " RUB";
233 | }
234 |
235 | public int getCardNumber() {
236 | return cardNumber;
237 | }
238 |
239 | public String getCardNumberAsString() {
240 | return formatCardNumber(cardNumber);
241 | }
242 |
243 | public static String formatCardNumber(int cardNumber) {
244 | int cardNum3 = cardNumber % 1000;
245 | int cardNum2 = (int)Math.floor(cardNumber / 1000) % 1000;
246 | int cardNum1 = (int)Math.floor(cardNumber / 1000000) % 1000;
247 | return String.format("%04d %03d %03d", cardNum1, cardNum2, cardNum3);
248 | }
249 |
250 | public String toString() {
251 | return "[Card UID=" + getUidAsString() + " " + getBalanceAsString() + "RUR]";
252 | }
253 |
254 | protected static int intval(byte... bytes) {
255 | int value = 0;
256 | for (int i = 0; i < bytes.length; i++) {
257 | int x = (int)bytes[bytes.length - i - 1];
258 | while (x < 0) x = 256 + x;
259 | value += x * Math.pow(0x100, i);
260 | }
261 | return value;
262 | }
263 |
264 | }
265 |
--------------------------------------------------------------------------------
/java/cc/troikadumper/MainActivity.java:
--------------------------------------------------------------------------------
1 | package cc.troikadumper;
2 |
3 | import android.app.Activity;
4 | import android.app.PendingIntent;
5 | import android.app.ProgressDialog;
6 | import android.content.Context;
7 | import android.content.DialogInterface;
8 | import android.content.Intent;
9 | import android.content.IntentFilter;
10 | import android.nfc.NfcAdapter;
11 | import android.nfc.NfcManager;
12 | import android.nfc.Tag;
13 | import android.nfc.tech.MifareClassic;
14 | import android.os.Bundle;
15 | import android.support.design.widget.FloatingActionButton;
16 | import android.support.v7.app.AppCompatActivity;
17 | import android.support.v7.widget.Toolbar;
18 | import android.view.View;
19 | import android.view.Menu;
20 | import android.view.MenuItem;
21 | import android.widget.TextView;
22 |
23 | import java.io.File;
24 | import java.io.IOException;
25 |
26 | public class MainActivity extends AppCompatActivity {
27 | final static int REQUEST_OPEN_DUMP = 1;
28 | final static String INTENT_READ_DUMP = "cc.troikadumper.INTENT_READ_DUMP";
29 |
30 | protected FloatingActionButton btnLoad;
31 | protected FloatingActionButton btnWrite;
32 | protected TextView info;
33 |
34 | protected NfcAdapter nfcAdapter;
35 | protected Dump dump;
36 | protected boolean writeMode = false;
37 | protected ProgressDialog pendingWriteDialog;
38 |
39 | @Override
40 | protected void onCreate(Bundle savedInstanceState) {
41 | super.onCreate(savedInstanceState);
42 |
43 | setContentView(R.layout.activity_main);
44 | info = (TextView) findViewById(R.id.textView);
45 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
46 | setSupportActionBar(toolbar);
47 |
48 | NfcManager nfcManager = (NfcManager)getSystemService(Context.NFC_SERVICE);
49 | nfcAdapter = nfcManager.getDefaultAdapter();
50 | if (nfcAdapter == null) {
51 | info.setText(R.string.error_no_nfc);
52 | }
53 |
54 | if (nfcAdapter != null && !nfcAdapter.isEnabled()) {
55 | info.setText(R.string.error_nfc_is_disabled);
56 | }
57 |
58 | pendingWriteDialog = new ProgressDialog(MainActivity.this);
59 | pendingWriteDialog.setIndeterminate(true);
60 | pendingWriteDialog.setMessage("Waiting for card...");
61 | pendingWriteDialog.setCancelable(true);
62 | pendingWriteDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
63 | @Override
64 | public void onCancel(DialogInterface dialog) {
65 | writeMode = false;
66 | }
67 | });
68 |
69 | btnWrite = (FloatingActionButton) findViewById(R.id.btn_write);
70 | btnWrite.setOnClickListener(new View.OnClickListener() {
71 | @Override
72 | public void onClick(View v) {
73 | writeMode = true;
74 | pendingWriteDialog.show();
75 | }
76 | });
77 | btnLoad = (FloatingActionButton) findViewById(R.id.btn_load);
78 | btnLoad.setOnClickListener(new View.OnClickListener() {
79 | @Override
80 | public void onClick(View view) {
81 | Intent intent = new Intent(getApplicationContext(), DumpListActivity.class);
82 | startActivityForResult(intent, REQUEST_OPEN_DUMP);
83 | }
84 | });
85 |
86 | Intent startIntent = getIntent();
87 | if (startIntent != null && startIntent.getAction().equals(NfcAdapter.ACTION_TECH_DISCOVERED)) {
88 | handleIntent(startIntent);
89 | }
90 | }
91 |
92 | @Override
93 | public boolean onCreateOptionsMenu(Menu menu) {
94 | // Inflate the menu; this adds items to the action bar if it is present.
95 | //getMenuInflater().inflate(R.menu.menu_main, menu);
96 | return true;
97 | }
98 |
99 | @Override
100 | public boolean onOptionsItemSelected(MenuItem item) {
101 | return super.onOptionsItemSelected(item);
102 | }
103 |
104 |
105 | @Override
106 | protected void onResume() {
107 | super.onResume();
108 |
109 | /**
110 | * It's important, that the activity is in the foreground (resumed). Otherwise
111 | * an IllegalStateException is thrown.
112 | */
113 | if (nfcAdapter != null) {
114 | setupForegroundDispatch((Activity) this, nfcAdapter);
115 | }
116 | }
117 |
118 | @Override
119 | protected void onPause() {
120 | /**
121 | * Call this before onPause, otherwise an IllegalArgumentException is thrown as well.
122 | */
123 | if (nfcAdapter != null) {
124 | stopForegroundDispatch(this, nfcAdapter);
125 | }
126 |
127 | super.onPause();
128 | }
129 |
130 | @Override
131 | protected void onNewIntent(Intent intent) {
132 | /**
133 | * This method gets called, when a new Intent gets associated with the current activity instance.
134 | * Instead of creating a new activity, onNewIntent will be called. For more information have a look
135 | * at the documentation.
136 | *
137 | * In our case this method gets called, when the user attaches a Tag to the device.
138 | */
139 | handleIntent(intent);
140 | }
141 |
142 | @Override
143 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
144 | super.onActivityResult(requestCode, resultCode, data);
145 |
146 | if (requestCode == REQUEST_OPEN_DUMP && resultCode == RESULT_OK) {
147 | handleIntent(data);
148 | }
149 | }
150 |
151 | private void handleIntent(Intent intent) {
152 | info.setText("");
153 | File dumpsDir = getApplicationContext().getExternalFilesDir(null);
154 | String action = intent.getAction();
155 | boolean shouldSave = false;
156 | try {
157 | if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {
158 | Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
159 | if (writeMode && dump != null) {
160 | pendingWriteDialog.hide();
161 | info.append("Writing to card...");
162 | dump.write(tag);
163 | } else {
164 | info.append("Reading from card...");
165 | dump = Dump.fromTag(tag);
166 | shouldSave = true;
167 | }
168 | } else if (INTENT_READ_DUMP.equals(action)) {
169 | File file = new File(dumpsDir, intent.getStringExtra("filename"));
170 | info.append("Reading from file...");
171 | dump = Dump.fromFile(file);
172 | }
173 |
174 | info.append("\nCard UID: " + dump.getUidAsString());
175 | info.append("\n\n --- Sector #8: ---\n");
176 | String[] blocks = dump.getDataAsStrings();
177 | for (int i = 0; i < blocks.length; i++) {
178 | info.append("\n" + i + "] " + blocks[i]);
179 | }
180 | info.append("\n\n --- Extracted data: ---\n");
181 | info.append("\nCard number: " + dump.getCardNumberAsString());
182 | info.append("\nCurrent balance: " + dump.getBalanceAsString());
183 | info.append("\nLast usage date: " + dump.getLastUsageDateAsString());
184 | info.append("\nLast validator: " + dump.getLastValidatorIdAsString());
185 |
186 | if (shouldSave) {
187 | info.append("\n\n Saving dump ... ");
188 | File save = dump.save(dumpsDir);
189 | info.append("\n " + save.getCanonicalPath());
190 | }
191 | if (writeMode) {
192 | info.append("\n\n Successfully wrote this dump!");
193 | }
194 | } catch (IOException e) {
195 | info.append("\nError: \n" + e.toString());
196 | dump = null;
197 | } finally {
198 | if (writeMode) {
199 | writeMode = false;
200 | }
201 | }
202 |
203 | btnWrite.setVisibility( (dump == null) ? View.GONE : View.VISIBLE );
204 | }
205 |
206 | /**
207 | * @param activity The corresponding {@link Activity} requesting the foreground dispatch.
208 | * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
209 | */
210 | public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) {
211 | final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass());
212 | intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
213 |
214 | final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0);
215 |
216 | IntentFilter[] filters = new IntentFilter[1];
217 | String[][] techList = new String[][]{
218 | new String[] {MifareClassic.class.getName()}
219 | };
220 |
221 | // Notice that this is the same filter as in our manifest.
222 | filters[0] = new IntentFilter();
223 | filters[0].addAction(NfcAdapter.ACTION_TECH_DISCOVERED);
224 | filters[0].addCategory(Intent.CATEGORY_DEFAULT);
225 | try {
226 | filters[0].addDataType("*/*");
227 | } catch (IntentFilter.MalformedMimeTypeException e) {
228 | throw new RuntimeException("Check your mime type.");
229 | }
230 |
231 | adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList);
232 | }
233 |
234 | /**
235 | * @param activity The corresponding {@link Activity} requesting to stop the foreground dispatch.
236 | * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
237 | */
238 | public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) {
239 | adapter.disableForegroundDispatch(activity);
240 | }
241 |
242 |
243 | }
244 |
--------------------------------------------------------------------------------