diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 4d8f8c8..d0460fb 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 a0cf765..99f0fde 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,32 +1,32 @@ apply plugin: 'com.android.application' android { compileSdkVersion 26 defaultConfig { applicationId "org.dslul.ticketreader" minSdkVersion 15 targetSdkVersion 26 - versionCode 11 - versionName "1.9" + versionCode 12 + versionName "1.9.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support:design:26.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' implementation 'com.google.android.gms:play-services-ads:12.0.1' implementation 'com.android.support:cardview-v7:26.1.0' } diff --git a/app/release/output.json b/app/release/output.json index 6127613..8bfabed 100755 --- a/app/release/output.json +++ b/app/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":11,"versionName":"1.9","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] \ No newline at end of file +[{"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 diff --git a/app/src/main/java/org/dslul/ticketreader/MainActivity.java b/app/src/main/java/org/dslul/ticketreader/MainActivity.java index 6e355c0..9791b99 100755 --- a/app/src/main/java/org/dslul/ticketreader/MainActivity.java +++ b/app/src/main/java/org/dslul/ticketreader/MainActivity.java @@ -1,360 +1,360 @@ package org.dslul.ticketreader; import android.annotation.SuppressLint; import android.nfc.tech.IsoDep; import android.os.Bundle; import android.os.CountDownTimer; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.CardView; import android.support.v7.widget.Toolbar; import android.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.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; 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; // 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.start(); scanAction = ACTION_READ; } } @SuppressLint("HandlerLeak") private Handler mTextBufferHandler = new Handler() { public void handleMessage(Message msg) { pages = (byte[])msg.obj; if(timer != null) timer.cancel(); //smartcard if(pages.length > 200) { /* ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("content", pages); clipboard.setPrimaryClip(clip); */ SmartCard smartcard = new SmartCard(pages, getBaseContext()); if(smartcard.isSubscription()) { dataLabel.setText(R.string.expire_date); tipologia.setText(smartcard.getName()); dataObliterazione.setText(smartcard.getDate()); if(smartcard.isExpired()) { corseRimanenti.setText("0"); statoBiglietto.setText(R.string.expired); statusImg.setImageResource(R.drawable.ic_error_grey_800_36dp); statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorRed)); } else { corseRimanenti.setText(R.string.unlimited); statoBiglietto.setText(R.string.valid); statusImg.setImageResource(R.drawable.ic_check_circle_grey_800_36dp); statusCard.setCardBackgroundColor(getResources().getColor(R.color.colorGreen)); } statusCard.setVisibility(View.VISIBLE); ticketCard.setVisibility(View.VISIBLE); infoLabel.setText(R.string.read_another_ticket); imageNfc.setVisibility(View.GONE); } else { - //createTicketInterface(smartcard.getName(),smartcard.getDate(), - // smartcard.getRemainingRides(), 0); - Toast.makeText(getBaseContext(), R.string.smartcard_tickets_not_supported_yet, Toast.LENGTH_LONG).show(); + createTicketInterface(smartcard.getName(),smartcard.getDate(), + smartcard.getRemainingRides(), 0); + //Toast.makeText(getBaseContext(), R.string.smartcard_tickets_not_supported_yet, Toast.LENGTH_LONG).show(); } } //chip on paper - else if(pages.length > 2) { + 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; } 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/SmartCard.java b/app/src/main/java/org/dslul/ticketreader/SmartCard.java index 43f243c..f16ab7c 100644 --- a/app/src/main/java/org/dslul/ticketreader/SmartCard.java +++ b/app/src/main/java/org/dslul/ticketreader/SmartCard.java @@ -1,230 +1,254 @@ package org.dslul.ticketreader; import android.content.Context; import android.util.Log; import org.dslul.ticketreader.util.GttDate; import java.text.DateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; public class SmartCard { public enum Type { BIP, PYOU, EDISU } static final Map subscriptionCodes = new HashMap() {{ + put(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 int code; private boolean isValid; private boolean isTicket; private boolean isSubscription; private Date startDate; private Date endDate; public Item(byte[] data) { //get item type code = ((data[6] & 0xff) << 8) | data[7] & 0xff; if(code == 0) { isValid = false; } else { isValid = true; } if(ticketCodes.containsKey(code)) { isTicket = true; isSubscription = false; } else if(subscriptionCodes.containsKey(code)) { isTicket = false; isSubscription = true; } else { isTicket = false; isSubscription = false; } long minutes = ~(data[11] << 16 & 0xff0000 | data[12] << 8 & 0xff00 | data[13] & 0xff) & 0xffffff; startDate = GttDate.decode(minutes); minutes = ~(data[14] << 16 & 0xff0000 | data[15] << 8 & 0xff00 | data[16] & 0xff) & 0xffffff; endDate = GttDate.decode(minutes); } public int getCode() { return code; } public Date getStartDate() { return startDate; } public Date getEndDate() { return endDate; } public boolean isValid() { return isValid; } public boolean isSubscription() { if(isSubscription) return true; else return false; } public boolean isTicket() { if(isTicket) return true; else return false; } public String getTypeName() { if(isTicket) return ticketCodes.get(code); else if(isSubscription) return subscriptionCodes.get(code); else return "Sconosciuto"; } } private byte[] cardinfo = new byte[31]; private byte[] itemsdata = new byte[31*7]; private Date creationDate; private Type type; private List items = new ArrayList<>(); - private boolean isSubscription; + private boolean isSubscription = false; private Item lastItem; private int ridesLeft = 0; public SmartCard(byte[] content, Context context) { this.context = context; System.arraycopy(content, 0, cardinfo, 0, 31); System.arraycopy(content, 31*2+2, itemsdata, 0, 31*7); byte[] minutes = new byte[3]; System.arraycopy(cardinfo, 11, minutes, 0, 3); creationDate = GttDate.decode(minutes); if(cardinfo[30] == (byte)0xC0) type = Type.BIP; else if(cardinfo[30] == (byte)0xC1) type = Type.PYOU; else if(cardinfo[30] == (byte)0xC2) type = Type.EDISU; Date lastExpireDate = GttDate.getGttEpoch(); for (int i = 0; i < 7; i++) { byte[] itemdata = new byte[31]; System.arraycopy(itemsdata, 31*i, itemdata, 0, 31); Item item = new Item(itemdata); if(item.isValid()) { if(lastExpireDate.before(item.getEndDate())) { lastExpireDate = item.getEndDate(); + if(item.isSubscription()) + isSubscription = true; lastItem = item; if(item.isTicket()) { ridesLeft += 1; //TODO: count tickets in daily 7 carnets } } - isSubscription = item.isSubscription(); + //isSubscription = item.isSubscription(); items.add(item); } } } public String getName() { return type + " - " + lastItem.getTypeName(); } public String getDate() { return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) .format(lastItem.getEndDate()); } public boolean isExpired() { Calendar c = Calendar.getInstance(); return c.getTime().after(lastItem.getEndDate()); } public int getRemainingRides() { return ridesLeft; } public boolean isSubscription() { return isSubscription; } } diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 144b3f7..6c0e47f 100755 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -1,223 +1,223 @@ - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a471526..c62659d 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,30 +1,30 @@ 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 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... + Smartcards with tickets still not supported, stay tuned…