diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index ba473a4..d4581c9 100755 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/app/build.gradle b/app/build.gradle index 9604cf9..ae4e792 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,34 +1,34 @@ apply plugin: 'com.android.application' android { compileSdkVersion 27 defaultConfig { applicationId "org.dslul.ticketreader" minSdkVersion 15 targetSdkVersion 27 - versionCode 16 - versionName "2.0beta3" + versionCode 20 + versionName "2.0" 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:27.1.1' implementation 'com.android.support:customtabs:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support:design:27.1.1' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation 'com.google.android.gms:play-services-ads:15.0.1' implementation 'com.android.support:cardview-v7:27.1.1' implementation 'com.yarolegovich:lovely-dialog:1.1.0' } diff --git a/app/release/output.json b/app/release/output.json index 920a5a5..89bf5d6 100755 --- a/app/release/output.json +++ b/app/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":16,"versionName":"2.0beta3","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":20,"versionName":"2.0","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/MainActivity.java b/app/src/main/java/org/dslul/ticketreader/MainActivity.java index 8f3df93..b7ad8d6 100755 --- a/app/src/main/java/org/dslul/ticketreader/MainActivity.java +++ b/app/src/main/java/org/dslul/ticketreader/MainActivity.java @@ -1,404 +1,405 @@ package org.dslul.ticketreader; import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; 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.text.Html; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.View; import android.view.Menu; import android.view.MenuItem; import android.widget.ImageView; import android.widget.TableLayout; import android.widget.TextView; import java.util.Calendar; import java.util.List; import java.util.concurrent.TimeUnit; 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; import com.yarolegovich.lovelydialog.LovelyCustomDialog; import com.yarolegovich.lovelydialog.LovelyInfoDialog; import com.yarolegovich.lovelydialog.LovelyStandardDialog; import org.dslul.ticketreader.util.HelperFunctions; 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 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 tipologia; private TextView dataLabel; private TextView dataObliterazione; private TextView corseRimanenti; private CountDownTimer timer; private List dump; // list of NFC technologies detected: private final String[][] techListsArray = new String[][] { 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-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); onNewIntent(getIntent()); - +/* new LovelyInfoDialog(this) .setTopColorRes(R.color.darkBlueGrey) .setIcon(R.drawable.ic_info_outline_white_36dp) //This will add Don't show again checkbox to the dialog. You can pass any ID as argument .setNotShowAgainOptionEnabled(0) .setNotShowAgainOptionChecked(false) .setTitle("BETA") .setMessage("Questa versione รจ una beta e potrebbe restituire risultati sbagliati. Segnalare per favore ogni incongruenza (insieme ad una copia del contenuto della carta) all'indirizzo email specificato nelle informazioni. Grazie.") .show(); - +*/ } @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)) { NfcThread nfcThread = new NfcThread(getBaseContext(), intent, mContentHandler, mToastShortHandler, mToastLongHandler, mShowInfoDialogHandler); nfcThread.start(); } } @SuppressLint("HandlerLeak") private Handler mContentHandler = new Handler() { public void handleMessage(Message msg) { List dumplist = (List)msg.obj; dump = dumplist; if(timer != null) timer.cancel(); try { //smartcard if(dumplist.size() == 15) { SmartCard smartcard = new SmartCard(dumplist); if(smartcard.hasSubscriptions() && !smartcard.hasTickets()) { dataLabel.setText(R.string.expire_date); tipologia.setText(smartcard.getSubscriptionName()); dataObliterazione.setText(smartcard.getExpireDate()); if(smartcard.isSubscriptionExpired()) { 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 if(smartcard.hasTickets()){ + } else { + createTicketInterface(smartcard.getTicketName(),smartcard.getValidationDate(), smartcard.getRemainingRides(), smartcard.getRemainingMinutes()); //Toast.makeText(getBaseContext(), R.string.smartcard_tickets_not_supported_yet, Toast.LENGTH_LONG).show(); } } //chip on paper else if(dumplist.size() > 15) { ChipOnPaper chipOnPaper = new ChipOnPaper(dumplist); 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); } } catch (Exception ex) { Toast.makeText(getBaseContext(), R.string.unknown_error, Toast.LENGTH_LONG).show(); Log.d("card", ex.getMessage()); ex.printStackTrace(); } } }; 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) { TextView view = new TextView(getBaseContext()); view.setText(Html.fromHtml(getString(R.string.html_info))); view.setMovementMethod(LinkMovementMethod.getInstance()); view.setPadding( 40, 40, 40, 40 ); new LovelyCustomDialog(this) .setTopColorRes(R.color.darkBlueGrey) .setIcon(R.drawable.ic_info_outline_white_36dp) .setTitle("Info") .setView(view) .show(); return true; } if (id == R.id.action_copy_content) { try { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); String content = ""; if (dump != null && dump.size() >= 15) { for (byte[] page : dump) { content = content.concat(HelperFunctions.byteArrayToHexString(page)); } ClipData clip = ClipData.newPlainText("content", content); if (clipboard != null) { clipboard.setPrimaryClip(clip); } Toast.makeText(getBaseContext(), R.string.content_copied, Toast.LENGTH_LONG).show(); } else { Toast.makeText(getBaseContext(), R.string.no_content, Toast.LENGTH_LONG).show(); } } catch (Exception ex) { ex.printStackTrace(); } 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(); } }; 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) { } }); 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 ab19343..5692a9c 100755 --- a/app/src/main/java/org/dslul/ticketreader/NfcThread.java +++ b/app/src/main/java/org/dslul/ticketreader/NfcThread.java @@ -1,212 +1,214 @@ package org.dslul.ticketreader; import java.io.IOException; import java.util.ArrayList; import java.util.List; 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.util.Log; +import static org.dslul.ticketreader.util.HelperFunctions.byteArrayToHexString; import static org.dslul.ticketreader.util.HelperFunctions.hexStringToByteArray; //parts of code from http://www.emutag.com/soft.php public class NfcThread extends Thread { private Context context; private Intent intent; private Handler mTextBufferHandler, mToastShortHandler, mToastLongHandler, mShowInfoDialogHandler; private byte[] readBuffer = new byte[1024]; // maximum theoretical capacity of MIFARE Ultralight NfcThread( Context context, Intent intent, Handler mTextBufferHandler, Handler mToastShortHandler, Handler mToastLongHandler, Handler mShowInfoDialogHandler ) { this.context = context; this.intent = intent; this.mTextBufferHandler = mTextBufferHandler; this.mToastShortHandler = mToastShortHandler; this.mToastLongHandler = mToastLongHandler; this.mShowInfoDialogHandler = mShowInfoDialogHandler; } public void run() { final Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); 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(); List dumplist = new ArrayList<>(); //selectApplication dumplist.add(isoDep.transceive(hexStringToByteArray("00A404000E315449432E494341D38012009101"))); //efEnvironment dumplist.add(isoDep.transceive(hexStringToByteArray("00B2013C1D"))); //efContractList dumplist.add(isoDep.transceive(hexStringToByteArray("00B201F41D"))); //efContract1 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2014C1D"))); //efContract2 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2024C1D"))); //efContract3 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2034C1D"))); //efContract4 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2044C1D"))); //efContract5 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2054C1D"))); //efContract6 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2064C1D"))); //efContract7 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2074C1D"))); //efContract8 dumplist.add(isoDep.transceive(hexStringToByteArray("00B2084C1D"))); //efEventLogs1 dumplist.add(isoDep.transceive(hexStringToByteArray("00B201441D"))); //efEventLogs2 dumplist.add(isoDep.transceive(hexStringToByteArray("00B202441D"))); //efEventLogs3 dumplist.add(isoDep.transceive(hexStringToByteArray("00B203441D"))); - //efCredit - dumplist.add(isoDep.transceive(hexStringToByteArray("007C000721"))); + //efValidation + dumplist.add(isoDep.transceive(hexStringToByteArray("00B201CC1D"))); if(dumplist.size() == 15 && dumplist.get(1)[0] != 0) { if(dumplist.get(2)[1] == 0) { showToastLong(context.getString(R.string.smartcard_empty)); return; } setContentBuffer(dumplist); showToastLong(context.getString(R.string.smartcard_read_correctly)); } else { showToastLong(context.getString(R.string.invalid_smartcard)); } isoDep.close(); } catch (IOException e) { showToastLong(context.getString(R.string.read_failure)); } } } private void handleNfcA(Tag tagFromIntent) { 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; List dumplist = new ArrayList<>(); try { mfu.connect(); pagesRead = rdNumPages(mfu, 16); // 0 for no limit (until error) mfu.close(); for (int i = 0; i < pagesRead*4; i += 4) { byte[] mfuPage = new byte[4]; System.arraycopy(readBuffer, i, mfuPage, 0, 4); dumplist.add(mfuPage); } if(pagesRead >= 16) { showToastShort(context.getString(R.string.ticket_correctly_read)); setContentBuffer(dumplist); } else { throw new RuntimeException(context.getString(R.string.read_failure)); } } catch (RuntimeException e) { showToastLong(context.getString(R.string.read_failure)); } catch (Exception e) { showToastLong(context.getString(R.string.communication_error)); } } private void setContentBuffer(List content) { Message msg = new Message(); 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 rdNumPages(NfcA mfu, int num) { int pagesRead = 0; while (rdPages(mfu, pagesRead) == 0) { pagesRead++; 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, 4); return 0; } } diff --git a/app/src/main/java/org/dslul/ticketreader/SmartCard.java b/app/src/main/java/org/dslul/ticketreader/SmartCard.java index c02e6b0..4fa00b2 100644 --- a/app/src/main/java/org/dslul/ticketreader/SmartCard.java +++ b/app/src/main/java/org/dslul/ticketreader/SmartCard.java @@ -1,334 +1,345 @@ package org.dslul.ticketreader; 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; import static java.lang.Math.abs; import static org.dslul.ticketreader.util.HelperFunctions.getBytesFromPage; public class SmartCard { public enum Type { BIP, PYOU, EDISU } static final Map subscriptionCodes = new HashMap() {{ put(68, "Mensile UNDER 26"); 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(307, "Annuale Ivrea Rete Urbana e Dintorni"); put(308, "Annuale Extraurbano O/D"); put(310, "Plurimensile Studenti Extraurbano O/D"); put(721, "Annuale UNDER 26"); put(722, "Annuale UNDER 26 Fascia A"); put(723, "Annuale UNDER 26 Fascia B"); put(724, "Annuale UNDER 26 Fascia C"); put(730, "Mensile urbano Over 65"); put(761, "Annuale Over A"); put(731, "Annuale Over B"); put(732, "Annuale Over C"); put(733, "Annuale Over D"); put(911, "10 Mesi Studenti"); put(912, "Annuale Studenti"); put(990, "Junior"); put(993, "Annuale Formula U"); put(4003, "Annuale Formula U a Zone"); }}; static final Map ticketCodes = new HashMap() {{ put(712, "Ordinario Urbano"); put(714, "City 100"); put(715, "Daily"); put(716, "Multidaily"); }}; private class Contract { private int code; private boolean isValid; private boolean isTicket; private boolean isSubscription; private Date startDate; private Date endDate; public Contract(byte[] data) { //get contract type code = ((data[4] & 0xff) << 8) | data[5] & 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[9] << 16 & 0xff0000 | data[10] << 8 & 0xff00 | data[11] & 0xff) & 0xffffff; startDate = GttDate.decode(minutes); minutes = ~(data[12] << 16 & 0xff0000 | data[13] << 8 & 0xff00 | data[14] & 0xff) & 0xffffff; endDate = GttDate.decode(minutes); } public int getCode() { return code; } public Date getStartDate() { return startDate; } public Date getEndDate() { return endDate; } public boolean isContract() { 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[] efEnvironment; private Date validationDate; private Date creationDate; private Type type; private List tickets = new ArrayList<>(); private List subscriptions = new ArrayList<>(); private Contract subscription; private int ridesLeft = 0; private long remainingMins; SmartCard(List dumplist) { efEnvironment = dumplist.get(1); byte[] efContractList = dumplist.get(2); byte[] efEventLogs1 = dumplist.get(11); byte[] efEventLogs2 = dumplist.get(12); byte[] efEventLogs3 = dumplist.get(13); + byte[] validations = dumplist.get(14); byte[] minutes = new byte[3]; System.arraycopy(efEnvironment, 9, minutes, 0, 3); creationDate = GttDate.decode(minutes); if(efEnvironment[28] == (byte)0xC0) type = Type.BIP; else if(efEnvironment[28] == (byte)0xC1) type = Type.PYOU; else if(efEnvironment[28] == (byte)0xC2) type = Type.EDISU; for (int i = 0; i < 8; i++) { Contract contract = new Contract(dumplist.get(i+3)); if(contract.isContract()) { if(contract.isSubscription()) { subscriptions.add(contract); } if(contract.isTicket()) { tickets.add(contract); } } } //get a valid subscription, if there's any Date latestExpireDate = GttDate.getGttEpoch(); for (Contract sub : subscriptions) { if (latestExpireDate.before(sub.getEndDate())) { latestExpireDate = sub.getEndDate(); subscription = sub; } } //actual tickets count - ridesLeft = countTickets(efContractList, efEventLogs1, efEventLogs2, efEventLogs3); + ridesLeft = countTickets(validations, efContractList, efEventLogs1, efEventLogs2, efEventLogs3); //get last validation time long mins = getBytesFromPage(efEventLogs1, 20, 3); if(mins == 0) mins = getBytesFromPage(efEventLogs2, 20, 3); if(mins == 0) mins = getBytesFromPage(efEventLogs3, 20, 3); validationDate = GttDate.addMinutesToDate(mins, GttDate.getGttEpoch()); Calendar c = Calendar.getInstance(); long diff = (c.getTime().getTime() - validationDate.getTime()) / 60000; int num = (int)(getBytesFromPage(efEventLogs1, 25, 1) >> 4); int tickettype = (int)getBytesFromPage(dumplist.get(num+2), 4, 2); long maxtime = 90; //city 100 if(tickettype == 714) { maxtime = 100; } //daily if(tickettype == 715 || tickettype == 716) { maxtime = GttDate.getMinutesUntilMidnight(); } if(diff >= maxtime) { remainingMins = 0; } else { remainingMins = maxtime - diff; } } - private int countTickets(byte[] contractsList, byte[] evLogs1, byte[] evLogs2, byte[] evLogs3) { + private int countTickets(byte[] validations, byte[] contractsList, byte[] evLogs1, byte[] evLogs2, byte[] evLogs3) { int count = 0; for (int i = 2; i < 24; i+=3) { - //int pos = abs(contractsList[i+1]) >> 4; - //real pos is given by the position in contractslist + int valpos = abs(contractsList[i+1]) >> 4; + //position in contractslist int pos = i/3 + 1; //check if it's a subscription int sub = abs(contractsList[i]&0xf0) >> 4; if(pos != 0 && pos <= 8 && (contractsList[i]&0x0f) != 0 && sub != 0xA) { - int lognum1 = abs(evLogs1[25]) >> 4; - int lognum2 = abs(evLogs2[25]) >> 4; - int lognum3 = abs(evLogs3[25]) >> 4; - //TODO: find out how to count multi daily 7 tickets - if(pos != lognum1 && pos != lognum2 && pos != lognum3) + /* + int logpos1 = abs(evLogs1[25]) >> 4; + int logpos2 = abs(evLogs2[25]) >> 4; + int logpos3 = abs(evLogs3[25]) >> 4; + //full multidaily + if(sub == 5) { + count += 7; + } + //ignore all logs after a zeroed one + else if(logpos1 == 0 || (pos != logpos1 && pos != logpos2 && pos != logpos3) + || (pos != logpos1 && logpos2 == 0)) { count += 1; + } + */ + int rides = (abs(validations[valpos*3-3] & 0xff) >> 3); + count += rides; } } return count; } public String getTicketName() { if(hasTickets()) return type + " - " + tickets.get(0).getTypeName(); else - return "Invalid"; + return type.toString(); } public String getSubscriptionName() { if(hasSubscriptions()) return type + " - " + subscription.getTypeName(); else - return "Invalid"; + return type.toString(); } public boolean hasTickets() { return ridesLeft != 0; } public boolean hasSubscriptions() { return subscriptions.size() != 0; } public String getExpireDate() { return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) .format(subscription.getEndDate()); } public String getValidationDate() { return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) .format(validationDate); } public boolean isSubscriptionExpired() { Calendar c = Calendar.getInstance(); return c.getTime().after(subscription.getEndDate()); } private boolean isExpired(Date date) { Calendar c = Calendar.getInstance(); return c.getTime().after(date); } public int getRemainingRides() { return ridesLeft; } public long getRemainingMinutes() { return remainingMins; } } diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 465051a..64806f1 100755 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -1,223 +1,222 @@ - diff --git a/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java b/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java index 57d8ada..fbc6a60 100755 --- a/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java +++ b/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java @@ -1,136 +1,145 @@ package org.dslul.ticketreader; import android.test.AndroidTestCase; import android.test.ApplicationTestCase; import android.test.InstrumentationTestCase; import android.util.Log; import org.dslul.ticketreader.util.HelperFunctions; import org.junit.Test; import java.util.ArrayList; import java.util.List; import static org.dslul.ticketreader.util.HelperFunctions.hexStringToByteArray; import static org.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). * * @see Testing documentation */ public class ChipOnPaperUnitTest { List ticket2 = new ArrayList<>(); List ticket3 = new ArrayList<>(); List ticket4 = new ArrayList<>(); List ticket5 = new ArrayList<>(); List ticket6 = new ArrayList<>(); @Test public void ChipOnPaper_isCorrect() throws Exception { List ticket1 = new ArrayList<>(); ticket1.add(hexStringToByteArray("057D6292")); ticket1.add(hexStringToByteArray("AD2954E9")); ticket1.add(hexStringToByteArray("3915F203")); ticket1.add(hexStringToByteArray("07FFFFF0")); ticket1.add(hexStringToByteArray("01040000")); ticket1.add(hexStringToByteArray("020102BE")); ticket1.add(hexStringToByteArray("68970000")); ticket1.add(hexStringToByteArray("00AE10A7")); ticket1.add(hexStringToByteArray("0200645C")); ticket1.add(hexStringToByteArray("397D91B4")); ticket1.add(hexStringToByteArray("68A4F900")); ticket1.add(hexStringToByteArray("04F80000")); ticket1.add(hexStringToByteArray("68A4F900")); ticket1.add(hexStringToByteArray("00050004")); ticket1.add(hexStringToByteArray("F8AE1079")); ticket1.add(hexStringToByteArray("9E1291E4")); ChipOnPaper chip = new ChipOnPaper(ticket1); assertEquals(4, chip.getRemainingRides()); assertEquals(0, chip.getRemainingMinutes()); } @Test public void Smartcard_Count_Ticket_isCorrect() throws Exception { List list = new ArrayList<>(); list.add(hexStringToByteArray("6F208970ABA0B980986C9A09078F098087E0A980DF0101010A0101090E109019011022354345676010019000")); list.add(hexStringToByteArray("050110129845479323849432659823874899264578987A09A9692348799000")); list.add(hexStringToByteArray("05012160014020014030014040014050012110012170012180000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.add(hexStringToByteArray("0501000000216D7F0D0004F800002A00000000006D7F0D00007000BEE39000")); list.add(hexStringToByteArray("0501000000216D00050004F800000400000000006CFFF00000400007779000")); list.add(hexStringToByteArray("0501000000216CFFF00004F80000D800000000006CFFF000004000316A9000")); list.add(hexStringToByteArray("60000234D95D6F5840000000000060004000004000000000234321E56A821000019000")); SmartCard smartcard = new SmartCard(list); assertEquals(3, smartcard.getRemainingRides()); list.set(2, hexStringToByteArray("05012160014020014030014040014050012110012170012180000000009000")); list.set(11, hexStringToByteArray("0501000000216D959E0004F800002A00000000006D959E00001000674E9000")); list.set(12, hexStringToByteArray("0501000000216D7F0D0004F800002A00000000006D7F0D00007000BEE39000")); list.set(13, hexStringToByteArray("0501000000216D00050004F800000400000000006CFFF00000400007779000")); SmartCard s2 = new SmartCard(list); assertEquals(2, s2.getRemainingRides()); list.set(2, hexStringToByteArray("05012060014020014030014040014050012110012170012180000000009000")); list.set(11, hexStringToByteArray("0501000000216D9B2C0004F800002A00000000006D9B2C000060008D809000")); list.set(12, hexStringToByteArray("0501000000216D959E0004F800002A00000000006D959E00001000674E9000")); list.set(13, hexStringToByteArray("0501000000216D7F0D0004F800002A00000000006D7F0D00007000BEE39000")); smartcard = new SmartCard(list); assertEquals(1, smartcard.getRemainingRides()); list.set(2, hexStringToByteArray("05012060014020014030014040014050012110012170012180000000009000")); list.set(11, hexStringToByteArray("0501000000216D9B2C0004F800002A00000000006D9B2C000060008D809000")); list.set(12, hexStringToByteArray("0501000000216D959E0004F800002A00000000006D959E00001000674E9000")); list.set(13, hexStringToByteArray("0501000000216D7F0D0004F800002A00000000006D7F0D00007000BEE39000")); smartcard = new SmartCard(list); assertEquals(1, smartcard.getRemainingRides()); //1 used ticket list.set(2, hexStringToByteArray("05012110000000000000000000000000000000000000000000000000009000")); list.set(11, hexStringToByteArray("0501000000216C9EF30004F800007400000000006C9EB100001000F66B9000")); list.set(12, hexStringToByteArray("0501000000216C9EC80004F80003E700000000006C9EB100001000E9E29000")); list.set(13, hexStringToByteArray("0501000000216C9EB10004F800007400000000006C9EB10000100098509000")); smartcard = new SmartCard(list); assertEquals(0, smartcard.getRemainingRides()); //1 used ticket list.set(2, hexStringToByteArray("05012110000000000000000000000000000000000000000000000000009000")); list.set(11, hexStringToByteArray("0501000000216C9EF30004F800007400000000006C9EB100001000F66B9000")); list.set(12, hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); list.set(13, hexStringToByteArray("00000000000000000000000000000000000000000000000000000000009000")); smartcard = new SmartCard(list); assertEquals(0, smartcard.getRemainingRides()); + //1 full multidaily + list.set(2, hexStringToByteArray("0501A10001511001A100000000000000000000000000000000000000009000")); + list.set(11, hexStringToByteArray("0501030258216C41130004F800007400000000006C355D0000300017D89000")); + list.set(12, hexStringToByteArray("0501030258216C3F8A0004F800000F00000000006C355D000030002EE19000")); + list.set(13, hexStringToByteArray("0501030258216C356A0004F800000A00000000006C355D000030007F249000")); + smartcard = new SmartCard(list); + + assertEquals(7, smartcard.getRemainingRides()); + //expired subscriptions only list.set(2, hexStringToByteArray("0501A10001000001A100000000000000000000000000000000000000009000")); list.set(11, hexStringToByteArray("0501030258216C41130004F800007400000000006C355D0000300017D89000")); list.set(12, hexStringToByteArray("0501030258216C3F8A0004F800000F00000000006C355D000030002EE19000")); list.set(13, hexStringToByteArray("0501030258216C356A0004F800000A00000000006C355D000030007F249000")); smartcard = new SmartCard(list); assertEquals(false, smartcard.hasTickets()); } } \ No newline at end of file