diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index d0460fb..cf0869b 100755 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/dictionaries/daniele.xml b/.idea/dictionaries/daniele.xml new file mode 100644 index 0000000..bcb10f1 --- /dev/null +++ b/.idea/dictionaries/daniele.xml @@ -0,0 +1,7 @@ + + + + dumplist + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 99202cc..c0f68ed 100755 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,34 +1,34 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 99f0fde..7936434 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,32 +1,34 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 26 + compileSdkVersion 27 defaultConfig { applicationId "org.dslul.ticketreader" minSdkVersion 15 - targetSdkVersion 26 - versionCode 12 - versionName "1.9.1" + targetSdkVersion 27 + versionCode 14 + 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:26.1.0' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' - implementation 'com.android.support:design:26.1.0' + 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.2' + implementation 'com.android.support:design:27.1.1' 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' + 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 8bfabed..e305b20 100755 --- a/app/release/output.json +++ b/app/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":12,"versionName":"1.9.1","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":13,"versionName":"1.9.2","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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 54f14d3..0c8e555 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,32 +1,38 @@ - + + + + + + + android:label="@string/app_name" + android:screenOrientation="portrait" + android:theme="@style/AppTheme.NoActionBar"> + - + + \ 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 index d81d51d..78ddeb7 100755 --- a/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java +++ b/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java @@ -1,153 +1,121 @@ package org.dslul.ticketreader; import android.util.Log; import org.dslul.ticketreader.util.GttDate; +import org.dslul.ticketreader.util.HelperFunctions; +import java.nio.ByteBuffer; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +import java.util.List; import java.util.concurrent.TimeUnit; +import static org.dslul.ticketreader.util.GttDate.addMinutesToDate; +import static org.dslul.ticketreader.util.HelperFunctions.getBytesFromPage; + public class ChipOnPaper { - private String pages; - private String date; + private Date date; private int type; private long remainingMins; + private int remainingRides; + + ChipOnPaper(List dumplist) { + + dumplist = dumplist; + + type = (int)getBytesFromPage(dumplist.get(5), 2, 2); + + long minutes = getBytesFromPage(dumplist.get(10), 0, 3); + + if(type == 9521) + minutes = getBytesFromPage(dumplist.get(12), 0, 3); + + date = addMinutesToDate(minutes, GttDate.getGttEpoch()); + + //calcola minuti rimanenti + Calendar c = Calendar.getInstance(); + long diff = (c.getTime().getTime() - date.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; + } - 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"); + //calcola le corse rimanenti + //TODO: corse in metropolitana (forse bit più significativo pag. 3) + int tickets; + if(type == 300) { //extraurbano + tickets = (int) (~getBytesFromPage(dumplist.get(3), 0, 4)); + } else { + tickets = (int)(~getBytesFromPage(dumplist.get(3), 2, 2)) + & 0xFFFF; } - this.pages = contentstr; - this.date = this.pages.substring(90, 96); - //this.type = (int)getBytesFromPage(5, 0, 1); - this.type = (int)getBytesFromPage(5, 2, 2); + remainingRides = Integer.bitCount(tickets); + } 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"; + case 9521: + return "Sadem Aeroporto Torino"; 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); + .format(date); } - //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); + return remainingRides; } 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 e9a5056..120f24b 100755 --- a/app/src/main/java/org/dslul/ticketreader/MainActivity.java +++ b/app/src/main/java/org/dslul/ticketreader/MainActivity.java @@ -1,360 +1,394 @@ 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 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 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; + 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); - 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 nfcThread = new NfcThread(getBaseContext(), intent, mContentHandler, mToastShortHandler, mToastLongHandler, mShowInfoDialogHandler); nfcThread.start(); - - scanAction = ACTION_READ; } } @SuppressLint("HandlerLeak") - private Handler mTextBufferHandler = new Handler() { + private Handler mContentHandler = new Handler() { public void handleMessage(Message msg) { - pages = (byte[])msg.obj; + List dumplist = (List)msg.obj; + dump = dumplist; 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.getSubscriptionName()); - 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(); - - } - - + try { + //smartcard + if(dumplist.size() == 15) { + SmartCard smartcard = new SmartCard(dumplist, getBaseContext()); + if(smartcard.isSubscription()) { + dataLabel.setText(R.string.expire_date); + tipologia.setText(smartcard.getSubscriptionName()); + 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.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()); } - //chip on paper - else if(pages.length > 48) { - 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; + 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(); - 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 5b8759f..ab19343 100755 --- a/app/src/main/java/org/dslul/ticketreader/NfcThread.java +++ b/app/src/main/java/org/dslul/ticketreader/NfcThread.java @@ -1,344 +1,212 @@ package org.dslul.ticketreader; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; -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; +import static org.dslul.ticketreader.util.HelperFunctions.hexStringToByteArray; -//code from http://www.emutag.com/soft.php + +//parts of 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 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(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}; - - //override - //content = hexStringToByteArray(""); - - //TODO: throw exception - if(info.length < 30 || info[3] == 0) { - showToastLong(context.getString(R.string.invalid_smartcard)); - setTextBuffer(err); - } else { - setTextBuffer(content); + 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"))); + + 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) { - e.printStackTrace(); + showToastLong(context.getString(R.string.read_failure)); } } } 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; + List dumplist = new ArrayList<>(); try { - //Log.i("position", "read data"); - - if (scanAction == ACTION_READ) { - mfu.connect(); - pagesRead = rdNumPages(mfu, 0); // 0 for no limit (until error) - mfu.close(); + mfu.connect(); + pagesRead = rdNumPages(mfu, 16); // 0 for no limit (until error) + mfu.close(); - byte[] content = new byte[pagesRead*4]; - System.arraycopy(readBuffer, 0, content, 0, pagesRead*4); - /* - String content = ""; + for (int i = 0; i < pagesRead*4; i += 4) { 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); - } + 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 setTextBuffer(byte[] content) { + 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 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; - } - */ + + + 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, 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(); + response = tag.transceive(cmd); } - catch (final IOException e) { return 1; } - /* - finally { - try { mfu.close(); } - catch (final Exception e) {} + catch (IOException e) { + return 1; } - */ - - 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; } + if (response.length != 16) + return 1; + + System.arraycopy(response, 0, readBuffer, pageOffset * 4, 4); 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/SmartCard.java b/app/src/main/java/org/dslul/ticketreader/SmartCard.java index 13cb5e6..e014d7f 100644 --- a/app/src/main/java/org/dslul/ticketreader/SmartCard.java +++ b/app/src/main/java/org/dslul/ticketreader/SmartCard.java @@ -1,271 +1,312 @@ 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; +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"); }}; Context context; - private class Item { + private class Contract { 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; + 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[11] << 16 & 0xff0000 | data[12] << 8 & 0xff00 | - data[13] & 0xff) & 0xffffff; + long minutes = ~(data[9] << 16 & 0xff0000 | data[10] << 8 & 0xff00 | + data[11] & 0xff) & 0xffffff; startDate = GttDate.decode(minutes); - minutes = ~(data[14] << 16 & 0xff0000 | data[15] << 8 & 0xff00 | - data[16] & 0xff) & 0xffffff; + 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 isValid() { + 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[] cardinfo = new byte[31]; - private byte[] itemsdata = new byte[31*7]; + private byte[] efEnvironment; + private Date validationDate; private Date creationDate; private Type type; - private List items = new ArrayList<>(); + private List contracts = new ArrayList<>(); private boolean isSubscription = false; - private Item lastItem; + private Contract latestContract; private String subscriptionName; private int ridesLeft = 0; + private long remainingMins; - public SmartCard(byte[] content, Context context) { + SmartCard(List dumplist, Context context) { this.context = context; - System.arraycopy(content, 0, cardinfo, 0, 31); - System.arraycopy(content, 31*2+2, itemsdata, 0, 31*7); + efEnvironment = dumplist.get(1); + byte[] efContractList = dumplist.get(2); byte[] minutes = new byte[3]; - System.arraycopy(cardinfo, 11, minutes, 0, 3); + System.arraycopy(efEnvironment, 9, minutes, 0, 3); creationDate = GttDate.decode(minutes); - if(cardinfo[30] == (byte)0xC0) + if(efEnvironment[28] == (byte)0xC0) type = Type.BIP; - else if(cardinfo[30] == (byte)0xC1) + else if(efEnvironment[28] == (byte)0xC1) type = Type.PYOU; - else if(cardinfo[30] == (byte)0xC2) + else if(efEnvironment[28] == (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.isSubscription()) { - isSubscription = true; - subscriptionName = item.getTypeName(); - if(!isExpired(item.getEndDate())) - break; - } - if(item.isTicket()) { - ridesLeft += 1; - if(isExpired(lastItem.getEndDate())) - isSubscription = false; - //TODO: count tickets in daily 7 carnets - } + Date latestExpireDate = GttDate.getGttEpoch(); + for (int i = 0; i < 8; i++) { + Contract contract = new Contract(dumplist.get(i+3)); + if(contract.isContract()) { + if(latestExpireDate.before(contract.getEndDate())) { + latestExpireDate = contract.getEndDate(); + latestContract = contract; + } + if(contract.isSubscription()) { + isSubscription = true; + subscriptionName = contract.getTypeName(); + if(!isExpired(contract.getEndDate())) + break; + } + if(contract.isTicket()) { + //ridesLeft += 1; + if(isExpired(latestContract.getEndDate())) + isSubscription = false; + //TODO: count tickets in daily 7 carnets } - //isSubscription = item.isSubscription(); - items.add(item); + //isSubscription = contract.isSubscription(); + + contracts.add(contract); } } + //actual tickets count + for (int i = 2; i < 29; i+=3) { + int num = abs(efContractList[i+1]) >> 4; + int used = efContractList[i+2]; + if(num != 0 && num <= 8 && used == 0) + ridesLeft += 1; + } + + + //get last validation time + long mins = getBytesFromPage(dumplist.get(11), 20, 3); + validationDate = GttDate.addMinutesToDate(mins, GttDate.getGttEpoch()); + Calendar c = Calendar.getInstance(); + long diff = (c.getTime().getTime() - validationDate.getTime()) / 60000; + + int num = (int)(getBytesFromPage(dumplist.get(11), 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; + } } public String getName() { - return type + " - " + lastItem.getTypeName(); + return type + " - " + latestContract.getTypeName(); } public String getSubscriptionName() { return type + " - " + subscriptionName; } public String getDate() { return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) - .format(lastItem.getEndDate()); + .format(latestContract.getEndDate()); + } + + public String getValidationDate() { + return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) + .format(validationDate); } public boolean isExpired() { Calendar c = Calendar.getInstance(); - return c.getTime().after(lastItem.getEndDate()); + return c.getTime().after(latestContract.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; + } public boolean isSubscription() { return isSubscription; } } diff --git a/app/src/main/java/org/dslul/ticketreader/util/HelperFunctions.java b/app/src/main/java/org/dslul/ticketreader/util/HelperFunctions.java index c9e4972..5ac0743 100644 --- a/app/src/main/java/org/dslul/ticketreader/util/HelperFunctions.java +++ b/app/src/main/java/org/dslul/ticketreader/util/HelperFunctions.java @@ -1,4 +1,70 @@ package org.dslul.ticketreader.util; -public class HelperFunctions { +import android.content.Context; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public final class HelperFunctions { + + public static 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; + } + + public 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; + } + + public static long getBytesFromPage(byte[] page, int offset, int bytesnum) { + byte[] bytes = new byte[bytesnum]; + System.arraycopy(page, offset, bytes, 0, bytesnum); + + long value = 0; + for (byte tmp : bytes) + value = (value << 8) + (tmp & 0xff); + + return value; + + } + + public static List loadFromFile(String filename, Context context) { + List array = new ArrayList<>(); + + BufferedReader reader; + + try{ + InputStream file = context.getAssets().open(filename); + reader = new BufferedReader(new InputStreamReader(file)); + String line = reader.readLine(); + while(line != null){ + line = reader.readLine(); + array.add(hexStringToByteArray(line)); + } + } catch(IOException ioe){ + ioe.printStackTrace(); + } + + return array; + } + } diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_36dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_36dp.png new file mode 100644 index 0000000..2dd40bd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_info_outline_white_36dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_star_border_white_36dp.png b/app/src/main/res/drawable-hdpi/ic_star_border_white_36dp.png new file mode 100644 index 0000000..c9ab328 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_star_border_white_36dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_white_36dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_white_36dp.png new file mode 100644 index 0000000..c7b1113 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_info_outline_white_36dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_star_border_white_36dp.png b/app/src/main/res/drawable-mdpi/ic_star_border_white_36dp.png new file mode 100644 index 0000000..e302ef6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_star_border_white_36dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_info_outline_white_36dp.png b/app/src/main/res/drawable-xhdpi/ic_info_outline_white_36dp.png new file mode 100644 index 0000000..c41a5fc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_info_outline_white_36dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_star_border_white_36dp.png b/app/src/main/res/drawable-xhdpi/ic_star_border_white_36dp.png new file mode 100644 index 0000000..7e41906 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_star_border_white_36dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_36dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_36dp.png new file mode 100644 index 0000000..2e500d8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_36dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star_border_white_36dp.png b/app/src/main/res/drawable-xxhdpi/ic_star_border_white_36dp.png new file mode 100644 index 0000000..25ef834 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_star_border_white_36dp.png differ diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 5885961..674007b 100755 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -1,10 +1,16 @@ + + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b8180f9..a8d8017 100755 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,31 +1,37 @@ 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.\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 + Semplice applicazione opensource per visualizzare le corse rimanenti nei biglietti GTT.
Se riscontri problemi durante l\'utilizzo dell\'app, scrivimi a questo indirizzo email, incollando una copia del contenuto della carta (effettuata dal menu).

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... + Tessere con biglietti non ancora supportate, stay tuned… + Segnala errori + Errore sconosciuto, contatta lo sviluppatore per risolvere questo problema. + Copia contenuto + Contenuto copiato negli appunti + Scansiona un biglietto prima di effettuare la copia del contenuto + Tessera vuota! \ 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 041db00..30ae204 100755 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,9 +1,18 @@ #00529f #00529f #FF4081 #FFA5D6A7 #FF90CAF9 #FFEF9A9A + + #3F51B5 + #009688 + #E64A19 + #388E3C + #455A64 + #D32F2F + #FFFFFF + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 59a0b0c..617af6a 100755 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,3 +1,6 @@ 16dp + + 16dp + 16dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c62659d..eeb5858 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,30 +1,36 @@ GTT Ticket Reader Info 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 This device does not support NFC technology. NFC disabled. Activate NFC and press the back button. Information Close - 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 + Simple opensource application to view the remaining trips in GTT tickets.
If you encounter any problem with this app, feel free to contact me at
this email address describing your problem. Do not forget to paste the content of your ticket (this can be done by the menu).

Project URL

Credits:

Icons: Card Paypass by Viktor Vorobyev from the Noun Project; samsung galaxy by Setyo Ari Wibowo from the Noun Project

]]>
Invalid ticket! 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… + Send feedback + Unknown error, please contact the developer to solve this problem. + Copy content + Content successfully copied to clipboard + No content to copy. Scan a ticket first + This smartcard is still empty!
diff --git a/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java b/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java index 87205d7..d1c1e13 100755 --- a/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java +++ b/app/src/test/java/org/dslul/ticketreader/ChipOnPaperUnitTest.java @@ -1,17 +1,50 @@ package org.dslul.ticketreader; +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.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). * * @see Testing documentation */ -public class ExampleUnitTest { +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 addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); + public void ChipOnPaper_isCorrect() throws Exception { + List ticket1 = new ArrayList<>(); + ticket1.add(HelperFunctions.hexStringToByteArray("057D6292")); + ticket1.add(HelperFunctions.hexStringToByteArray("AD2954E9")); + ticket1.add(HelperFunctions.hexStringToByteArray("3915F203")); + ticket1.add(HelperFunctions.hexStringToByteArray("07FFFFF0")); + ticket1.add(HelperFunctions.hexStringToByteArray("01040000")); + ticket1.add(HelperFunctions.hexStringToByteArray("020102BE")); + ticket1.add(HelperFunctions.hexStringToByteArray("68970000")); + ticket1.add(HelperFunctions.hexStringToByteArray("00AE10A7")); + ticket1.add(HelperFunctions.hexStringToByteArray("0200645C")); + ticket1.add(HelperFunctions.hexStringToByteArray("397D91B4")); + ticket1.add(HelperFunctions.hexStringToByteArray("68A4F900")); + ticket1.add(HelperFunctions.hexStringToByteArray("04F80000")); + ticket1.add(HelperFunctions.hexStringToByteArray("68A4F900")); + ticket1.add(HelperFunctions.hexStringToByteArray("00050004")); + ticket1.add(HelperFunctions.hexStringToByteArray("F8AE1079")); + ticket1.add(HelperFunctions.hexStringToByteArray("9E1291E4")); + ChipOnPaper chip = new ChipOnPaper(ticket1); + assertEquals(4, chip.getRemainingRides()); + assertEquals(0, chip.getRemainingMinutes()); + } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 75385e9..654620b 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.3' + classpath 'com.android.tools.build:gradle:3.1.4' // 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 }