diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser old mode 100644 new mode 100755 index 71d2f1f..4d8f8c8 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml old mode 100644 new mode 100755 diff --git a/.idea/gradle.xml b/.idea/gradle.xml old mode 100644 new mode 100755 diff --git a/.idea/misc.xml b/.idea/misc.xml old mode 100644 new mode 100755 diff --git a/.idea/modules.xml b/.idea/modules.xml old mode 100644 new mode 100755 diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml old mode 100644 new mode 100755 diff --git a/.idea/vcs.xml b/.idea/vcs.xml old mode 100644 new mode 100755 diff --git a/app/build.gradle b/app/build.gradle index 13aabde..a0cf765 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,32 +1,32 @@ apply plugin: 'com.android.application' android { compileSdkVersion 26 defaultConfig { applicationId "org.dslul.ticketreader" minSdkVersion 15 targetSdkVersion 26 - versionCode 8 - versionName "1.7" + versionCode 11 + versionName "1.9" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support:design:26.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' implementation 'com.google.android.gms:play-services-ads:12.0.1' implementation 'com.android.support:cardview-v7:26.1.0' } diff --git a/app/release/output.json b/app/release/output.json index 0b8b5d9..6127613 100755 --- a/app/release/output.json +++ b/app/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":7,"versionName":"1.6","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] \ No newline at end of file +[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":11,"versionName":"1.9","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] \ No newline at end of file diff --git a/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java b/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java new file mode 100755 index 0000000..d81d51d --- /dev/null +++ b/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java @@ -0,0 +1,153 @@ +package org.dslul.ticketreader; + +import android.util.Log; + +import org.dslul.ticketreader.util.GttDate; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.TimeUnit; + + +public class ChipOnPaper { + + private String pages; + private String date; + private int type; + private long remainingMins; + + public ChipOnPaper(byte[] content) { + //if(data == null) + //TODO: convert this class to use bytes internally instead of strings + String contentstr = ""; + byte[] page = new byte[4]; + for (int i = 0; i < content.length; i += 4) { + System.arraycopy(content, i, page, 0, 4); + contentstr = contentstr + ByteArrayToHexString(page) + System.getProperty("line.separator"); + } + this.pages = contentstr; + this.date = this.pages.substring(90, 96); + //this.type = (int)getBytesFromPage(5, 0, 1); + this.type = (int)getBytesFromPage(5, 2, 2); + } + + public String getTypeName() { + //http://www.gtt.to.it/cms/biglietti-abbonamenti/biglietti/biglietti-carnet + switch (type) { + case 302: + case 304: + return "City 100"; + case 303: + case 305: + return "Daily"; + case 704: + return "Tour"; + case 301: + return "Multicorsa extraurbano"; + case 702: + case 706: + return "Carnet 5 corse"; + case 701: + case 705: + return "Carnet 15 corse"; + case 300: + return "Extraurbano"; + default: + return "Non riconosciuto"; + } + } + + public String getDate() { + Date finalDate = addMinutesToDate(Long.parseLong(this.date, 16), GttDate.getGttEpoch()); + + //calcola minuti rimanenti + Calendar c = Calendar.getInstance(); + long diff = (c.getTime().getTime() - finalDate.getTime()) / 60000; + long maxtime = 90; + //city 100 + if(type == 302 || type == 304) { + maxtime = 100; + } + //daily + if(type == 303 || type == 305) { + maxtime = GttDate.getMinutesUntilMidnight(); + } + //Tour TODO: make a distinction between the two types + if(type == 704) { + maxtime = 2*24*60; + } + if(diff >= maxtime) { + remainingMins = 0; + } else { + remainingMins = maxtime - diff; + } + + + return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) + .format(finalDate); + } + + + //TODO: corse in metropolitana (forse bit più significativo pag. 3) + public int getRemainingRides() { + int tickets; + if(type == 300) { //extraurbano + tickets = (int) (~getBytesFromPage(3, 0, 4)); + } else { + tickets = (int)(~getBytesFromPage(3, 2, 2)) + & 0xFFFF; + } + return Integer.bitCount(tickets); + } + + + public long getRemainingMinutes() { + return remainingMins; + } + + + + private long getBytesFromPage(int page, int offset, int bytesnum) { + return Long.parseLong( + pages.substring(9 * page + offset * 2, 9 * page + offset * 2 + bytesnum * 2), 16); + } + + + + private static Date addMinutesToDate(long minutes, Date beforeTime){ + final long ONE_MINUTE_IN_MILLIS = 60000; + + long curTimeInMs = beforeTime.getTime(); + Date afterAddingMins = new Date(curTimeInMs + (minutes * ONE_MINUTE_IN_MILLIS)); + return afterAddingMins; + } + + + private static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + private String ByteArrayToHexString(byte[] inarray) { + int i, j, in; + String [] hex = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"}; + String out= ""; + for(j = 0 ; j < inarray.length ; ++j) { + in = (int) inarray[j] & 0xff; + i = (in >> 4) & 0x0f; + out += hex[i]; + i = in & 0x0f; + out += hex[i]; + } + return out; + } + +} diff --git a/app/src/main/java/org/dslul/ticketreader/MainActivity.java b/app/src/main/java/org/dslul/ticketreader/MainActivity.java index 42377b8..6e355c0 100755 --- a/app/src/main/java/org/dslul/ticketreader/MainActivity.java +++ b/app/src/main/java/org/dslul/ticketreader/MainActivity.java @@ -1,302 +1,360 @@ package org.dslul.ticketreader; +import android.annotation.SuppressLint; +import android.nfc.tech.IsoDep; import android.os.Bundle; import android.os.CountDownTimer; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.CardView; import android.support.v7.widget.Toolbar; -import android.util.Log; import android.view.View; import android.view.Menu; import android.view.MenuItem; -import android.widget.Button; import android.widget.ImageView; -import android.widget.RelativeLayout; import android.widget.TableLayout; import android.widget.TextView; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.Calendar; -import java.util.Date; import java.util.concurrent.TimeUnit; -import android.content.ClipboardManager; -import android.content.ClipData; -import android.content.Context; import android.nfc.NfcAdapter; import android.nfc.tech.NfcA; import android.widget.Toast; import android.content.Intent; import android.content.IntentFilter; import android.app.PendingIntent; import android.os.Handler; import android.os.Message; import android.app.AlertDialog; import android.content.DialogInterface; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.AdView; import com.google.android.gms.ads.MobileAds; public class MainActivity extends AppCompatActivity { private NfcAdapter mNfcAdapter; private IntentFilter tech; private IntentFilter[] intentFiltersArray; private PendingIntent pendingIntent; private Intent intent; private AlertDialog alertDialog; private Toast currentToast; - private String pages = "ERROR"; + private byte[] pages = {(byte)0xFF}; private AdView adview; private ImageView imageNfc; private CardView ticketCard; private CardView statusCard; private ImageView statusImg; private TextView statoBiglietto; private TextView infoLabel; private TableLayout infoTable; - private TextView dataObliterazione; + private TextView tipologia; + private TextView dataLabel; + private TextView dataObliterazione; private TextView corseRimanenti; private CountDownTimer timer; private static final int ACTION_NONE = 0; private static final int ACTION_READ = 1; private int scanAction; // list of NFC technologies detected: private final String[][] techListsArray = new String[][] { - new String[] { - //MifareUltralight.class.getName(), - NfcA.class.getName() - } + new String[] { + //MifareUltralight.class.getName(), + NfcA.class.getName() + }, + new String[] { + IsoDep.class.getName() + } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); adview = (AdView) findViewById(R.id.adView); imageNfc = (ImageView) findViewById(R.id.imagenfcView); ticketCard = (CardView) findViewById(R.id.ticketCardView); statusCard = (CardView) findViewById(R.id.statusCardView); statusImg = (ImageView) findViewById(R.id.statusImg); statoBiglietto = (TextView) findViewById(R.id.stato_biglietto); infoLabel = (TextView) findViewById(R.id.infolabel); infoTable = (TableLayout) findViewById(R.id.info_table); + tipologia = (TextView) findViewById(R.id.tipologia); + dataLabel = (TextView) findViewById(R.id.validation_or_expire); dataObliterazione = (TextView) findViewById(R.id.data_obliterazione); corseRimanenti = (TextView) findViewById(R.id.corse_rimaste); - MobileAds.initialize(this, "ca-app-pub-3940256099942544/6300978111"); + MobileAds.initialize(this, "ca-app-pub-2102716674867426~1964394961"); AdRequest adRequest = new AdRequest.Builder().build(); adview.loadAd(adRequest); mNfcAdapter = NfcAdapter.getDefaultAdapter(this); if (mNfcAdapter == null) { Toast.makeText(this, R.string.nfc_not_supported, Toast.LENGTH_LONG).show(); finish(); return; } if (!mNfcAdapter.isEnabled()) { Toast.makeText(this, R.string.nfc_disabled, Toast.LENGTH_LONG).show(); startActivity(new Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS)); } tech = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED); intentFiltersArray = new IntentFilter[] {tech}; intent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); //FLAG_ACTIVITY_REORDER_TO_FRONT FLAG_RECEIVER_REPLACE_PENDING pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); scanAction = ACTION_READ; onNewIntent(getIntent()); } @Override protected void onResume() { super.onResume(); mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, this.techListsArray); } @Override protected void onPause() { // disabling foreground dispatch: //NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); mNfcAdapter.disableForegroundDispatch(this); super.onPause(); } @Override protected void onNewIntent(Intent intent) { if (intent.getAction().equals(NfcAdapter.ACTION_TECH_DISCOVERED)) { String mTextBufferText = "aa"; NfcThread nfcThread = new NfcThread(getBaseContext(), intent, scanAction, mTextBufferText, mTextBufferHandler, mToastShortHandler, mToastLongHandler, mShowInfoDialogHandler); nfcThread.start(); scanAction = ACTION_READ; } } + @SuppressLint("HandlerLeak") private Handler mTextBufferHandler = new Handler() { public void handleMessage(Message msg) { - pages = (String)msg.obj; - if(timer != null) - timer.cancel(); - if(pages != "ERROR") { - Parser parser = new Parser(pages); - dataObliterazione.setText(parser.getDate()); - corseRimanenti.setText(Integer.toString(parser.getRemainingRides())); - - if(parser.getRemainingMinutes() != 0) { - statoBiglietto.setText(R.string.in_corso); - statusImg.setImageResource(R.drawable.ic_restore_grey_800_36dp); - statusCard.setCardBackgroundColor(0xFF90CAF9); - Calendar calendar = Calendar.getInstance(); - int sec = calendar.get(Calendar.SECOND); - timer = new CountDownTimer((parser.getRemainingMinutes()*60 - sec)*1000, 1000) { - - public void onTick(long millis) { - statoBiglietto.setText(String.format(getResources().getString(R.string.in_corso), - TimeUnit.MILLISECONDS.toMinutes(millis), - TimeUnit.MILLISECONDS.toSeconds(millis) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis)))); - } - - public void onFinish() { - statoBiglietto.setText(R.string.corse_esaurite); - statusImg.setImageResource(R.drawable.ic_error_grey_800_36dp); - statusCard.setCardBackgroundColor(0xFFEF9A9A); - if(timer != null) - timer.cancel(); - } - - }.start(); - } else if(parser.getRemainingRides() == 0 && parser.getRemainingMinutes() == 0) { - statoBiglietto.setText(R.string.corse_esaurite); - statusImg.setImageResource(R.drawable.ic_error_grey_800_36dp); - statusCard.setCardBackgroundColor(0xFFEF9A9A); - } else if(parser.getRemainingRides() != 0 && parser.getRemainingMinutes() == 0) { - statoBiglietto.setText(String.format(getResources().getString(R.string.corse_disponibili), parser.getRemainingRides())); - statusImg.setImageResource(R.drawable.ic_check_circle_grey_800_36dp); - statusCard.setCardBackgroundColor(0xFFA5D6A7); - } - - statusCard.setVisibility(View.VISIBLE); - ticketCard.setVisibility(View.VISIBLE); - infoLabel.setText(R.string.read_another_ticket); - imageNfc.setVisibility(View.GONE); + pages = (byte[])msg.obj; + + if(timer != null) + timer.cancel(); + + //smartcard + if(pages.length > 200) { + /* + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("content", pages); + clipboard.setPrimaryClip(clip); + */ + SmartCard smartcard = new SmartCard(pages, getBaseContext()); + if(smartcard.isSubscription()) { + dataLabel.setText(R.string.expire_date); + tipologia.setText(smartcard.getName()); + dataObliterazione.setText(smartcard.getDate()); + + if(smartcard.isExpired()) { + corseRimanenti.setText("0"); + statoBiglietto.setText(R.string.expired); + statusImg.setImageResource(R.drawable.ic_error_grey_800_36dp); + statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorRed)); + } else { + corseRimanenti.setText(R.string.unlimited); + statoBiglietto.setText(R.string.valid); + statusImg.setImageResource(R.drawable.ic_check_circle_grey_800_36dp); + statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorGreen)); + } + + statusCard.setVisibility(View.VISIBLE); + ticketCard.setVisibility(View.VISIBLE); + infoLabel.setText(R.string.read_another_ticket); + imageNfc.setVisibility(View.GONE); + } else { + //createTicketInterface(smartcard.getName(),smartcard.getDate(), + // smartcard.getRemainingRides(), 0); + Toast.makeText(getBaseContext(), R.string.smartcard_tickets_not_supported_yet, Toast.LENGTH_LONG).show(); + + } + + + } + //chip on paper + else if(pages.length > 2) { + + ChipOnPaper chipOnPaper = new ChipOnPaper(pages); + createTicketInterface(chipOnPaper.getTypeName(),chipOnPaper.getDate(), + chipOnPaper.getRemainingRides(), chipOnPaper.getRemainingMinutes()); } else { statusCard.setVisibility(View.GONE); ticketCard.setVisibility(View.GONE); infoLabel.setText(R.string.info_instructions); imageNfc.setVisibility(View.VISIBLE); } } }; + + + private void createTicketInterface(String name, String date, int remainingRides, long remainingMinutes) { + dataLabel.setText(R.string.data_obliterazione); + tipologia.setText(name); + dataObliterazione.setText(date); + corseRimanenti.setText(Integer.toString(remainingRides)); + + if(remainingMinutes != 0) { + statoBiglietto.setText(R.string.in_corso); + statusImg.setImageResource(R.drawable.ic_restore_grey_800_36dp); + statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorBlue)); + Calendar calendar = Calendar.getInstance(); + int sec = calendar.get(Calendar.SECOND); + timer = new CountDownTimer((remainingMinutes*60 - sec)*1000, 1000) { + + public void onTick(long millis) { + statoBiglietto.setText(String.format(getResources().getString(R.string.in_corso), + TimeUnit.MILLISECONDS.toMinutes(millis), + TimeUnit.MILLISECONDS.toSeconds(millis) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis)))); + } + + public void onFinish() { + statoBiglietto.setText(R.string.corse_esaurite); + statusImg.setImageResource(R.drawable.ic_error_grey_800_36dp); + statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorRed)); + if(timer != null) + timer.cancel(); + } + + }.start(); + } else if(remainingRides == 0 && remainingMinutes == 0) { + statoBiglietto.setText(R.string.corse_esaurite); + statusImg.setImageResource(R.drawable.ic_error_grey_800_36dp); + statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorRed)); + } else if(remainingRides != 0 && remainingMinutes == 0) { + statoBiglietto.setText(String.format(getResources().getString(R.string.corse_disponibili), remainingRides)); + statusImg.setImageResource(R.drawable.ic_check_circle_grey_800_36dp); + statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorGreen)); + } + + statusCard.setVisibility(View.VISIBLE); + ticketCard.setVisibility(View.VISIBLE); + infoLabel.setText(R.string.read_another_ticket); + imageNfc.setVisibility(View.GONE); + } + + + + + + + private Handler mToastShortHandler = new Handler() { public void handleMessage(Message msg) { String text = (String)msg.obj; if(currentToast != null) currentToast.cancel(); currentToast = Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT); currentToast.show(); } }; private Handler mToastLongHandler = new Handler() { public void handleMessage(Message msg) { String text = (String)msg.obj; if(currentToast != null) currentToast.cancel(); currentToast = Toast.makeText(MainActivity.this, text, Toast.LENGTH_LONG); currentToast.show(); } }; private Handler mShowInfoDialogHandler = new Handler() { public void handleMessage(Message msg) { String text = (String)msg.obj; //infoDialog = showInfoDialog(text); //infoDialog.show(); } }; @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_info) { alertDialog = showAlertDialog(getString(R.string.info_message)); alertDialog.show(); return true; } return super.onOptionsItemSelected(item); } private AlertDialog showAlertDialog(String message) { DialogInterface.OnClickListener dialogInterfaceListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { alertDialog.cancel(); scanAction = ACTION_READ; } }; alertDialog = new AlertDialog.Builder(this) .setTitle(R.string.information) .setIcon(android.R.drawable.ic_dialog_info) .setMessage(message) .setPositiveButton(R.string.close_dialog, null) .create(); alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { scanAction = ACTION_READ; } }); return alertDialog; } } diff --git a/app/src/main/java/org/dslul/ticketreader/NfcThread.java b/app/src/main/java/org/dslul/ticketreader/NfcThread.java index f8fa4f2..66468ae 100755 --- a/app/src/main/java/org/dslul/ticketreader/NfcThread.java +++ b/app/src/main/java/org/dslul/ticketreader/NfcThread.java @@ -1,273 +1,341 @@ package org.dslul.ticketreader; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.nfc.NfcAdapter; import android.nfc.Tag; +import android.nfc.tech.IsoDep; import android.nfc.tech.NfcA; import android.os.Handler; import android.os.Message; +import android.content.ClipboardManager; +import android.util.Log; //import android.util.Log; //code from http://www.emutag.com/soft.php public class NfcThread extends Thread { private static final int ACTION_NONE = 0; private static final int ACTION_READ = 1; private static final int ACTION_WRITE = 2; private Context context; private Intent intent; private int scanAction; private String mTextBufferText; private Handler mTextBufferHandler, mToastShortHandler, mToastLongHandler, mShowInfoDialogHandler; private byte[] readBuffer = new byte[1024]; // maximum theoretical capacity of MIFARE Ultralight private byte[] toWriteBuffer = new byte[1024]; NfcThread( Context context, Intent intent, int scanAction, String mTextBufferText, Handler mTextBufferHandler, Handler mToastShortHandler, Handler mToastLongHandler, Handler mShowInfoDialogHandler ) { this.context = context; this.intent = intent; this.scanAction = scanAction; this.mTextBufferText = mTextBufferText; this.mTextBufferHandler = mTextBufferHandler; this.mToastShortHandler = mToastShortHandler; this.mToastLongHandler = mToastLongHandler; this.mShowInfoDialogHandler = mShowInfoDialogHandler; } public void run() { final Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); - - if (scanAction == ACTION_NONE) { - showToastLong("Please select READ or WRITE before scanning tag"); - return; - } - - final NfcA mfu = NfcA.get(tagFromIntent); - - if (mfu == null) { - showToastLong(context.getString(R.string.ticket_not_supported)); - return; - } - - byte[] ATQA = mfu.getAtqa(); - - if (mfu.getSak() != 0x00 || ATQA.length != 2 || ATQA[0] != 0x44 || ATQA[1] != 0x00) { - showToastLong(context.getString(R.string.ticket_not_supported)); - return; - } - - int pagesRead; - - try { - //Log.i("position", "read data"); - - if (scanAction == ACTION_READ) { - mfu.connect(); - pagesRead = rdNumPages(mfu, 0); // 0 for no limit (until error) - mfu.close(); - - String content = ""; - byte[] mfuPage = new byte[4]; - for (int i = 0; i < pagesRead * 4; i += 4) { - System.arraycopy(readBuffer, i, mfuPage, 0, 4); - content = content + ByteArrayToHexString(mfuPage) + System.getProperty("line.separator"); - } - if(pagesRead >= 16) { - showToastShort(context.getString(R.string.ticket_correctly_read)); - setTextBuffer(content); - } else { - showToastShort(context.getString(R.string.read_failure)); - setTextBuffer("ERROR"); - } - } - } - catch (Exception e) { - showToastLong(context.getString(R.string.communication_error)); - } + + if(tagFromIntent.getTechList()[0].equals(IsoDep.class.getName())) { + handleIsoDep(tagFromIntent); + } else { + handleNfcA(tagFromIntent); + } } + + + private void handleIsoDep(Tag tagFromIntent) { + IsoDep isoDep = IsoDep.get(tagFromIntent); + if (isoDep != null) { + try { + isoDep.connect(); + + byte[] getinfo = {0x00, (byte)0xa4, 0x00, 0x00, 0x02, 0x20, 0x01}; + isoDep.transceive(getinfo); + byte[] read = {0x00, (byte)0xb2, 0x01, 0x05}; + byte[] info = isoDep.transceive(read); + byte[] gettickets = {0x00, (byte)0xa4, 0x00, 0x00, 0x02, 0x20, 0x20}; + isoDep.transceive(gettickets); + byte[] tickets = isoDep.transceive(read); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream( ); + outputStream.write(info); + outputStream.write(tickets); + byte content[] = outputStream.toByteArray(); + byte err[] = {(byte)0xFF}; + + //TODO: throw exception + if(info.length < 30 || info[3] == 0) { + showToastLong(context.getString(R.string.invalid_smartcard)); + setTextBuffer(err); + } else { + setTextBuffer(content); + showToastLong(context.getString(R.string.smartcard_read_correctly)); + } + + isoDep.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + + private void handleNfcA(Tag tagFromIntent) { + if (scanAction == ACTION_NONE) { + showToastLong("Please select READ or WRITE before scanning tag"); + return; + } + + final NfcA mfu = NfcA.get(tagFromIntent); + + if (mfu == null) { + showToastLong(context.getString(R.string.ticket_not_supported)); + return; + } + + byte[] ATQA = mfu.getAtqa(); + + if (mfu.getSak() != 0x00 || ATQA.length != 2 || ATQA[0] != 0x44 || ATQA[1] != 0x00) { + showToastLong(context.getString(R.string.ticket_not_supported)); + return; + } + + int pagesRead; + + try { + //Log.i("position", "read data"); + + if (scanAction == ACTION_READ) { + mfu.connect(); + pagesRead = rdNumPages(mfu, 0); // 0 for no limit (until error) + mfu.close(); + + byte[] content = new byte[pagesRead*4]; + System.arraycopy(readBuffer, 0, content, 0, pagesRead*4); + /* + String content = ""; + byte[] mfuPage = new byte[4]; + for (int i = 0; i < pagesRead*4; i += 4) { + System.arraycopy(readBuffer, i, mfuPage, 0, 4); + content = content + ByteArrayToHexString(mfuPage) + System.getProperty("line.separator"); + }*/ + if(pagesRead >= 16 && content.length >= 16*4) { + showToastShort(context.getString(R.string.ticket_correctly_read)); + setTextBuffer(content); + } else { + showToastShort(context.getString(R.string.read_failure)); + //TODO: throw error instead + byte[] error = {(byte)0xFF}; + setTextBuffer(error); + } + } + } + catch (Exception e) { + showToastLong(context.getString(R.string.communication_error)); + } + } - private void setTextBuffer(String text) { + private void setTextBuffer(byte[] content) { Message msg = new Message(); - msg.obj = text; + msg.obj = content; mTextBufferHandler.sendMessage(msg); } private void showToastShort(String text) { Message msg = new Message(); msg.obj = text; mToastShortHandler.sendMessage(msg); } private void showToastLong(String text) { Message msg = new Message(); msg.obj = text; mToastLongHandler.sendMessage(msg); } private void showInfoDialog(String text) { Message msg = new Message(); msg.obj = text; mShowInfoDialogHandler.sendMessage(msg); } /* private int rdAllPages(NfcA mfu) { int pagesRead = 0; while (rdPages(mfu, pagesRead) == 0) { pagesRead += 4; if (pagesRead == 256) break; } return pagesRead; } */ private int rdNumPages(NfcA mfu, int num) { int pagesRead = 0; while (rdPages(mfu, pagesRead) == 0) { pagesRead++; if (pagesRead == num || pagesRead == 256) break; } return pagesRead; } /* private int rdNumPages(NfcA mfu, int num) { int pagesRead = 0; // align number of pages to a multiple of 4 num += 3; num >>>= 2; // unsigned shift num <<= 2; while (rdPages(mfu, pagesRead) == 0) { pagesRead += 4; if (pagesRead == num || pagesRead == 256) break; } return pagesRead; } */ // first failure (NAK) causes response 0x00 (or possibly other 1-byte values) // second failure (NAK) causes transceive() to throw IOException private byte rdPages(NfcA tag, int pageOffset) { byte[] cmd = {0x30, (byte)pageOffset}; byte[] response = new byte[16]; try { response = tag.transceive(cmd); } catch (IOException e) { return 1; } if (response.length != 16) return 1; //System.arraycopy(response, 0, readBuffer, pageOffset * 4, 16); System.arraycopy(response, 0, readBuffer, pageOffset * 4, 4); return 0; } // first failure (NAK) causes transceive() to throw IOException /* private byte wrPage(NfcA tag, int pageOffset) { byte[] cmd = {(byte)0xA2, (byte)pageOffset, 0x00, 0x00, 0x00, 0x00}; System.arraycopy(toWriteBuffer, pageOffset * 4, cmd, 2, 4); try { Log.i("TRANS START", Integer.toString(pageOffset)); tag.transceive(cmd); Log.i("TRANS END", Integer.toString(pageOffset)); } catch (IOException e) { return 1; } return 0; } */ private byte wrPage(NfcA mfu, int pageOffset) { byte[] cmd = {(byte)0xA2, (byte)pageOffset, 0x00, 0x00, 0x00, 0x00}; System.arraycopy(toWriteBuffer, pageOffset * 4, cmd, 2, 4); //byte[] data = {0x00, 0x00, 0x00, 0x00}; //System.arraycopy(toWriteBuffer, pageOffset * 4, data, 0, 4); //byte errors = 0; //final MifareUltralight mfu = MifareUltralight.get(tag); try { //mfu.connect(); //Log.i("TRANS START", Integer.toString(pageOffset)); //mfu.writePage(pageOffset, data); mfu.transceive(cmd); //Log.i("TRANS END", Integer.toString(pageOffset)); //mfu.close(); } catch (final IOException e) { return 1; } /* finally { try { mfu.close(); } catch (final Exception e) {} } */ return 0; } // first failure (NAK) causes transceive() to throw IOException private byte wrPageCompat(NfcA tag, int pageOffset) { byte[] cmd1 = {(byte)0xA0, (byte)pageOffset}; byte[] cmd2 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; System.arraycopy(toWriteBuffer, pageOffset * 4, cmd2, 0, 4); try { tag.transceive(cmd1); tag.transceive(cmd2); } catch (IOException e) { return 1; } return 0; } private String ByteArrayToHexString(byte[] inarray) { int i, j, in; String [] hex = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"}; String out= ""; for(j = 0 ; j < inarray.length ; ++j) { in = (int) inarray[j] & 0xff; i = (in >> 4) & 0x0f; out += hex[i]; i = in & 0x0f; out += hex[i]; } return out; } private int HexStringToByteArray(String instring, byte[] outarray, int outoffset) { int errors = 0; byte[] nibbles = new byte[2]; for (int i = 0; i < instring.length(); i += 2) { nibbles[0] = (byte)instring.charAt(i+0); nibbles[1] = (byte)instring.charAt(i+1); if (notHex(nibbles[0])) errors = 1; if (notHex(nibbles[1])) errors = 1; outarray[outoffset] = (byte)((hex2bin(nibbles[0]) << 4) | hex2bin(nibbles[1])); outoffset++; } return errors; } + + private byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + private boolean notHex(byte inchar) { if (inchar >= '0' && inchar <= '9') return false; if (inchar >= 'a' && inchar <= 'f') return false; if (inchar >= 'A' && inchar <= 'F') return false; return true; } private byte hex2bin(byte inchar) { if (inchar > 'Z') inchar -= ' '; if (inchar > '9') inchar -= 7; inchar &= 0x0f; return inchar; } } diff --git a/app/src/main/java/org/dslul/ticketreader/Parser.java b/app/src/main/java/org/dslul/ticketreader/Parser.java deleted file mode 100755 index 9ea323e..0000000 --- a/app/src/main/java/org/dslul/ticketreader/Parser.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.dslul.ticketreader; - -import android.util.Log; - -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.concurrent.TimeUnit; - - -public class Parser { - - private String pages; - private String date; - private int remainingMins; - - public Parser(String data) { - //if(data == null) - - this.pages = data; - this.date = this.pages.substring(90, 96); - } - - public String getDate() { - String startingDate = "05/01/01 00:00:00"; - SimpleDateFormat format = new SimpleDateFormat("yy/MM/dd HH:mm:ss"); - Date date = null; - try { - date = format.parse(startingDate); - } catch (ParseException e) { - e.printStackTrace(); - } - - Date finalDate = addMinutesToDate(Long.parseLong(this.date, 16), date); - - //calcola minuti rimanenti - Calendar c = Calendar.getInstance(); - long diff = (c.getTime().getTime() - finalDate.getTime()) / 60000; - if(diff >= 90) { - remainingMins = 0; - } else { - remainingMins = (int)(90 - diff); - } - - - return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) - .format(finalDate); - } - - - //TODO: corse in metropolitana (forse bit più significativo pag. 3) - public int getRemainingRides() { - int tickettype = (int)getBytesFromPage(5, 0, 1); - int tickets; - if(tickettype == 3) { //extraurbano - tickets = (int) (~getBytesFromPage(3, 0, 4)); - } else { - tickets = (int)(~getBytesFromPage(3, 2, 2)) - & 0xFFFF; - } - return Integer.bitCount(tickets); - } - - - public int getRemainingMinutes() { - return remainingMins; - } - - - - private long getBytesFromPage(int page, int offset, int bytesnum) { - return Long.parseLong( - pages.substring(9 * page + offset * 2, 9 * page + offset * 2 + bytesnum * 2), 16); - } - - - - private static Date addMinutesToDate(long minutes, Date beforeTime){ - final long ONE_MINUTE_IN_MILLIS = 60000; - - long curTimeInMs = beforeTime.getTime(); - Date afterAddingMins = new Date(curTimeInMs + (minutes * ONE_MINUTE_IN_MILLIS)); - return afterAddingMins; - } - - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; -} - -} diff --git a/app/src/main/java/org/dslul/ticketreader/SmartCard.java b/app/src/main/java/org/dslul/ticketreader/SmartCard.java new file mode 100644 index 0000000..43f243c --- /dev/null +++ b/app/src/main/java/org/dslul/ticketreader/SmartCard.java @@ -0,0 +1,230 @@ +package org.dslul.ticketreader; + +import android.content.Context; +import android.util.Log; + +import org.dslul.ticketreader.util.GttDate; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class SmartCard { + + public enum Type { + BIP, + PYOU, + EDISU + } + + static final Map subscriptionCodes = new HashMap() {{ + put(72, "Mensile Studenti Rete Urbana"); + + put(101, "Settimanale Formula 1"); + put(102, "Settimanale Formula 2"); + put(103, "Settimanale Formula 3"); + put(104, "Settimanale Formula 4"); + put(105, "Settimanale Formula 5"); + put(106, "Settimanale Formula 6"); + put(107, "Settimanale Formula 7"); + put(108, "Settimanale Intera Area Formula"); + put(109, "Settimanale Personale Rete Urbana"); + + put(199, "Mensile Personale Rete Urbana (Formula U)"); + + put(201, "Mensile Formula 1"); + put(202, "Mensile Formula 2"); + put(203, "Mensile Formula 3"); + put(204, "Mensile Formula 4"); + put(205, "Mensile Formula 5"); + put(206, "Mensile Formula 6"); + put(207, "Mensile Formula 7"); + put(208, "Mensile Intera Area Formula"); + + put(261, "Mensile Studenti Urbano+Suburbano"); + + put(290, "Mensile 65+ Urbano Orario Ridotto"); + put(291, "Mensile 65+ Urbano"); + + put(911, "10 Mesi Studenti"); + put(912, "Annuale Studenti"); + }}; + + static final Map ticketCodes = new HashMap() {{ + put(712, "Ordinario Urbano"); + put(714, "City 100"); + put(715, "Daily"); + put(716, "Multidaily"); + }}; + + Context context; + + private class Item { + private int code; + private boolean isValid; + private boolean isTicket; + private boolean isSubscription; + private Date startDate; + private Date endDate; + + public Item(byte[] data) { + //get item type + code = ((data[6] & 0xff) << 8) | data[7] & 0xff; + if(code == 0) { + isValid = false; + } else { + isValid = true; + } + + if(ticketCodes.containsKey(code)) { + isTicket = true; + isSubscription = false; + } else if(subscriptionCodes.containsKey(code)) { + isTicket = false; + isSubscription = true; + } else { + isTicket = false; + isSubscription = false; + } + + long minutes = ~(data[11] << 16 & 0xff0000 | data[12] << 8 & 0xff00 | + data[13] & 0xff) & 0xffffff; + startDate = GttDate.decode(minutes); + + minutes = ~(data[14] << 16 & 0xff0000 | data[15] << 8 & 0xff00 | + data[16] & 0xff) & 0xffffff; + endDate = GttDate.decode(minutes); + + + } + + public int getCode() { + return code; + } + + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public boolean isValid() { + return isValid; + } + + public boolean isSubscription() { + if(isSubscription) + return true; + else + return false; + } + + public boolean isTicket() { + if(isTicket) + return true; + else + return false; + } + + public String getTypeName() { + if(isTicket) + return ticketCodes.get(code); + else if(isSubscription) + return subscriptionCodes.get(code); + else + return "Sconosciuto"; + } + + } + + + + + private byte[] cardinfo = new byte[31]; + private byte[] itemsdata = new byte[31*7]; + + private Date creationDate; + private Type type; + private List items = new ArrayList<>(); + private boolean isSubscription; + private Item lastItem; + private int ridesLeft = 0; + + + public SmartCard(byte[] content, Context context) { + this.context = context; + + System.arraycopy(content, 0, cardinfo, 0, 31); + System.arraycopy(content, 31*2+2, itemsdata, 0, 31*7); + + byte[] minutes = new byte[3]; + System.arraycopy(cardinfo, 11, minutes, 0, 3); + creationDate = GttDate.decode(minutes); + + if(cardinfo[30] == (byte)0xC0) + type = Type.BIP; + else if(cardinfo[30] == (byte)0xC1) + type = Type.PYOU; + else if(cardinfo[30] == (byte)0xC2) + type = Type.EDISU; + + Date lastExpireDate = GttDate.getGttEpoch(); + for (int i = 0; i < 7; i++) { + byte[] itemdata = new byte[31]; + System.arraycopy(itemsdata, 31*i, itemdata, 0, 31); + Item item = new Item(itemdata); + + if(item.isValid()) { + if(lastExpireDate.before(item.getEndDate())) { + lastExpireDate = item.getEndDate(); + lastItem = item; + if(item.isTicket()) { + ridesLeft += 1; + //TODO: count tickets in daily 7 carnets + } + + } + isSubscription = item.isSubscription(); + + items.add(item); + } + } + + + + } + + + public String getName() { + return type + " - " + lastItem.getTypeName(); + } + + public String getDate() { + return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) + .format(lastItem.getEndDate()); + } + + public boolean isExpired() { + Calendar c = Calendar.getInstance(); + return c.getTime().after(lastItem.getEndDate()); + } + + public int getRemainingRides() { + return ridesLeft; + } + + + public boolean isSubscription() { + return isSubscription; + } + + +} diff --git a/app/src/main/java/org/dslul/ticketreader/util/GttDate.java b/app/src/main/java/org/dslul/ticketreader/util/GttDate.java new file mode 100644 index 0000000..252fc08 --- /dev/null +++ b/app/src/main/java/org/dslul/ticketreader/util/GttDate.java @@ -0,0 +1,59 @@ +package org.dslul.ticketreader.util; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public final class GttDate { + + + public static Date decode(byte[] minutes) { + return addMinutesToDate(byteArrayToLong(minutes), getGttEpoch()); + + } + + public static Date decode(long minutes) { + return addMinutesToDate(minutes, getGttEpoch()); + + } + + public static Date getGttEpoch() { + String startingDate = "05/01/01 00:00:00"; + SimpleDateFormat format = new SimpleDateFormat("yy/MM/dd HH:mm:ss"); + Date date = null; + try { + date = format.parse(startingDate); + } catch (ParseException e) { + e.printStackTrace(); + } + return date; + } + + public static long getMinutesUntilMidnight() { + Calendar c = Calendar.getInstance(); + c.add(Calendar.DAY_OF_MONTH, 1); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return (int)(c.getTimeInMillis()-System.currentTimeMillis()/60000); + + } + + public static Date addMinutesToDate(long minutes, Date beforeTime) { + final long ONE_MINUTE_IN_MILLIS = 60000; + + long curTimeInMs = beforeTime.getTime(); + return new Date(curTimeInMs + (minutes * ONE_MINUTE_IN_MILLIS)); + } + + private static long byteArrayToLong(byte[] bytes) { + long value = 0; + for (int i = 0; i < bytes.length; i++) { + value = (value << 8) + (bytes[i] & 0xff); + } + return value; + } + +} diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 413cc24..144b3f7 100755 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -1,199 +1,223 @@ + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml old mode 100644 new mode 100755 index 43c66e4..b8180f9 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,24 +1,31 @@ Lettore Biglietti GTT Info Data obliterazione: Minuti rimanenti: Corse rimaste: Avvicina un biglietto alla parte posteriore del dispositivo per effettuare la scansione Immagine biglietto nfc Per leggere un altro biglietto, avvicinalo nuovamente al sensore NFC %d corse disponibili In corso %d:%d Corse esaurite Dettagli biglietto Questo dispositivo non supporta la tecnologia NFC. NFC disabilitato. Attiva l\'NFC e premi il tasto indietro. Informazioni Chiudi - Semplice applicazione opensource per visualizzare le corse rimanenti nei biglietti GTT. https://github.com/dslul/ticketreader Icone: Card Paypass by Viktor Vorobyev from the Noun Project; samsung galaxy by Setyo Ari Wibowo from the Noun Project + Semplice applicazione opensource per visualizzare le corse rimanenti nei biglietti GTT.\n\nhttps://github.com/dslul/ticketreader\n\nIcone: Card Paypass by Viktor Vorobyev from the Noun Project; samsung galaxy by Setyo Ari Wibowo from the Noun Project Biglietto non valido! Biglietto letto correttamente. Lettura fallita, riprovare Errore di comunicazione. Riprova + Data di scadenza: + Tessera non valida + Tessera letta correttamente. + Scaduto + Illimitato + Valido + Tessere con biglietti non ancora supportate, stay tuned... \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9fd135c..041db00 100755 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,9 @@ #00529f #00529f #FF4081 + #FFA5D6A7 + #FF90CAF9 + #FFEF9A9A diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ecba8f..a471526 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,23 +1,30 @@ GTT Ticket Reader Info - Data obliterazione: - Minuti rimanenti: - Corse rimaste: - Avvicina un biglietto alla parte posteriore del dispositivo per effettuare la scansione - Immagine biglietto nfc - Per leggere un altro biglietto, avvicinalo nuovamente al sensore NFC - %d corse disponibili - In corso %d:%d - Corse esaurite + Time of validation: + Minutes remaining: + Trips remaining: + Place a ticket near the back of the device to scan + NFC ticket image + To read another ticket, move it close to the NFC sensor again + %d trips available + In progress %d:%d + Trips exhausted Ticket info - Questo dispositivo non supporta la tecnologia NFC. - NFC disabilitato. Attiva l\'NFC e premi il tasto indietro. + This device does not support NFC technology. + NFC disabled. Activate NFC and press the back button. Information Close - Semplice applicazione opensource per visualizzare le corse rimanenti nei biglietti GTT. https://github.com/dslul/ticketreader Icone: Card Paypass by Viktor Vorobyev from the Noun Project; samsung galaxy by Setyo Ari Wibowo from the Noun Project + Simple opensource application to view the remaining trips in GTT tickets.\n\nhttps://github.com/dslul/ticketreader\n\nIcons: Card Paypass by Viktor Vorobyev from the Noun Project; samsung galaxy by Setyo Ari Wibowo from the Noun Project Invalid ticket! - Biglietto letto correttamente. - Lettura fallita, riprovare - Errore di comunicazione. Riprova + Ticket read correctly. + Reading failed, try again + Communication error. Try again + Expire Date: + Invalid smartcard + Smartcard read correctly. + Expired + Unlimited + Valid + Smartcards with tickets still not supported, stay tuned... diff --git a/app/src/main/res/xml/filter_nfc.xml b/app/src/main/res/xml/filter_nfc.xml index d3260d6..7e96898 100755 --- a/app/src/main/res/xml/filter_nfc.xml +++ b/app/src/main/res/xml/filter_nfc.xml @@ -1,6 +1,9 @@ android.nfc.tech.NfcA + + android.nfc.tech.IsoDep + diff --git a/build.gradle b/build.gradle index 206d303..75385e9 100755 --- a/build.gradle +++ b/build.gradle @@ -1,30 +1,30 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.1' + classpath 'com.android.tools.build:gradle:3.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() maven { url "https://maven.google.com" } } } task clean(type: Delete) { delete rootProject.buildDir }