diff --git a/.gitignore b/.gitignore index 2fa2de7..a6a4480 100755 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build +/dump /captures .externalNativeBuild /app/build /app/libs /app/release diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 0ac0485..2fc209c 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/caches/gradle_models.ser b/.idea/caches/gradle_models.ser new file mode 100644 index 0000000..1e58ca5 Binary files /dev/null and b/.idea/caches/gradle_models.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 30aa626..ae78c11 100755 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,29 +1,113 @@ - - - - - - - - - - + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 7ac24c7..f43d428 100755 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,18 +1,21 @@ \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c0f68ed..d04819c 100755 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,34 +1,48 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index c39ecab..a089a46 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,37 +1,39 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { applicationId "org.dslul.ticketreader" minSdkVersion 15 - targetSdkVersion 28 - versionCode 33 - versionName "2.3" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + targetSdkVersion 29 + versionCode 37 + versionName "2.6" + testInstrumentationRunner "androidx.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:28.0.0' - implementation 'com.android.support:animated-vector-drawable:28.0.0' - implementation 'com.android.support:support-media-compat:28.0.0' - implementation 'com.android.support:support-v4:28.0.0' - implementation 'com.android.support:customtabs:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - implementation 'com.android.support:design:28.0.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0' + implementation 'androidx.media:media:1.1.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.browser:browser:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.vectordrawable:vectordrawable:1.1.0' 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:17.1.1' - implementation 'com.android.support:cardview-v7:28.0.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation 'com.google.android.gms:play-services-ads:18.3.0' + implementation 'androidx.cardview:cardview:1.0.0' implementation 'com.yarolegovich:lovely-dialog:1.1.0' } diff --git a/app/release/output.json b/app/release/output.json index d4ba1fa..4344130 100755 --- a/app/release/output.json +++ b/app/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":33,"versionName":"2.3","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] \ No newline at end of file +[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":37,"versionName":"2.6","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/androidTest/java/org/dslul/ticketreader/ExampleInstrumentedTest.java b/app/src/androidTest/java/org/dslul/ticketreader/ExampleInstrumentedTest.java index 951f201..26b0f37 100755 --- a/app/src/androidTest/java/org/dslul/ticketreader/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/org/dslul/ticketreader/ExampleInstrumentedTest.java @@ -1,26 +1,26 @@ package org.dslul.ticketreader; import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumented test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("org.dslul.ticketreader", appContext.getPackageName()); } } diff --git a/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java b/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java index ed2d90e..d932c43 100755 --- a/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java +++ b/app/src/main/java/org/dslul/ticketreader/ChipOnPaper.java @@ -1,125 +1,129 @@ package org.dslul.ticketreader; import android.util.Log; import org.dslul.ticketreader.util.GttDate; import java.text.DateFormat; import java.util.Calendar; import java.util.Date; import java.util.List; import static org.dslul.ticketreader.util.GttDate.addMinutesToDate; import static org.dslul.ticketreader.util.HelperFunctions.getBytesFromPage; public class ChipOnPaper { 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 || type == 650 || type == 651) { maxtime = 100; } - //Tour TODO: make a distinction between the two types - if(type == 704) { + //Tour 2 giorni + if(type == 288) { maxtime = 2*24*60; } + //Tour 3 giorni + if(type == 289) { + maxtime = 3*24*60; + } //daily if(type == 303 || type == 305) { remainingMins = GttDate.getMinutesUntilEndOfService(date); } else if(diff >= maxtime) { remainingMins = 0; } else { remainingMins = maxtime - diff; } //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; } 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 650: case 651: return "MultiCity"; - case 704: - return "Tour"; + case 288: + return "Tour 2 giorni"; + case 289: + return "Tour 3 giorni"; 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() { return DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) .format(date); } public int getRemainingRides() { return remainingRides; } public long getRemainingMinutes() { if(remainingMins < 0) return 0; else return remainingMins; } } diff --git a/app/src/main/java/org/dslul/ticketreader/MainActivity.java b/app/src/main/java/org/dslul/ticketreader/MainActivity.java index feb4d52..26d322c 100755 --- a/app/src/main/java/org/dslul/ticketreader/MainActivity.java +++ b/app/src/main/java/org/dslul/ticketreader/MainActivity.java @@ -1,417 +1,419 @@ package org.dslul.ticketreader; import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.ComponentName; -import android.content.SharedPreferences; import android.nfc.tech.IsoDep; import android.os.Bundle; import android.os.CountDownTimer; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.preference.PreferenceManager; -import android.support.v7.widget.CardView; -import android.support.v7.widget.Toolbar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceManager; +import androidx.cardview.widget.CardView; +import androidx.appcompat.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.content.pm.PackageManager; 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.google.android.gms.ads.initialization.InitializationStatus; +import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; import com.yarolegovich.lovelydialog.LovelyCustomDialog; -import com.yarolegovich.lovelydialog.LovelyInfoDialog; -import com.yarolegovich.lovelydialog.LovelyStandardDialog; import org.dslul.ticketreader.util.HelperFunctions; +import static org.dslul.ticketreader.util.HelperFunctions.millisToString; + 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); + //initialize NFC first + 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()); + setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); PreferenceManager.setDefaultValues(this, R.xml.preferences, false); 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()); + MobileAds.initialize(this, new OnInitializationCompleteListener() { + @Override + public void onInitializationComplete(InitializationStatus initializationStatus) { + } + }); + AdRequest adRequest = new AdRequest.Builder().build(); + adview.loadAd(adRequest); /* 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)) { + super.onNewIntent(intent); + if (intent.getAction().equals(NfcAdapter.ACTION_TECH_DISCOVERED)) { - NfcThread nfcThread = new NfcThread(getBaseContext(), intent, mContentHandler, mToastShortHandler, mToastLongHandler, mShowInfoDialogHandler); - nfcThread.start(); - } + 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 { 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)))); + statoBiglietto.setText(String.format("%s %s", + getResources().getString(R.string.in_corso), millisToString(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_settings) { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); 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 5692a9c..21415db 100755 --- a/app/src/main/java/org/dslul/ticketreader/NfcThread.java +++ b/app/src/main/java/org/dslul/ticketreader/NfcThread.java @@ -1,214 +1,220 @@ 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"))); //efValidation dumplist.add(isoDep.transceive(hexStringToByteArray("00B201CC1D"))); + setContentBuffer(dumplist); + 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) { + setContentBuffer(new ArrayList()); showToastLong(context.getString(R.string.read_failure)); } } } private void handleNfcA(Tag tagFromIntent) { final NfcA mfu = NfcA.get(tagFromIntent); if (mfu == null) { + setContentBuffer(new ArrayList()); 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) { + setContentBuffer(new ArrayList()); 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); } + setContentBuffer(dumplist); 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) { + setContentBuffer(new ArrayList()); showToastLong(context.getString(R.string.read_failure)); } catch (Exception e) { + setContentBuffer(new ArrayList()); 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/SettingsActivity.java b/app/src/main/java/org/dslul/ticketreader/SettingsActivity.java index 28dfd7d..30b32c2 100644 --- a/app/src/main/java/org/dslul/ticketreader/SettingsActivity.java +++ b/app/src/main/java/org/dslul/ticketreader/SettingsActivity.java @@ -1,45 +1,44 @@ package org.dslul.ticketreader; import android.content.ComponentName; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; -import android.support.v7.preference.PreferenceManager; -import android.support.v7.preference.SwitchPreferenceCompat; -import android.util.Log; +import androidx.preference.PreferenceManager; +import androidx.preference.SwitchPreferenceCompat; public class SettingsActivity extends AppCompatActivity { public static final String AUTOSTART_SWITCH = "switch_autostart"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportFragmentManager().beginTransaction() .replace(android.R.id.content, new SettingsFragment()) .commit(); SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(AUTOSTART_SWITCH)) { boolean switchAutostart = sharedPreferences.getBoolean (SettingsActivity.AUTOSTART_SWITCH, false); PackageManager pm = getApplicationContext().getPackageManager(); ComponentName compName = new ComponentName(getPackageName(), getPackageName() + ".AliasAutoStartMainActivity"); pm.setComponentEnabledSetting( compName, switchAutostart ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } } }; SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); sharedPref.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); } } \ No newline at end of file diff --git a/app/src/main/java/org/dslul/ticketreader/SettingsFragment.java b/app/src/main/java/org/dslul/ticketreader/SettingsFragment.java index f631160..961ef55 100644 --- a/app/src/main/java/org/dslul/ticketreader/SettingsFragment.java +++ b/app/src/main/java/org/dslul/ticketreader/SettingsFragment.java @@ -1,21 +1,21 @@ package org.dslul.ticketreader; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v7.preference.PreferenceFragmentCompat; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceFragmentCompat; /** * A simple {@link Fragment} subclass. */ public class SettingsFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preferences, rootKey); } } 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 5ac0743..85bc0b6 100644 --- a/app/src/main/java/org/dslul/ticketreader/util/HelperFunctions.java +++ b/app/src/main/java/org/dslul/ticketreader/util/HelperFunctions.java @@ -1,70 +1,92 @@ package org.dslul.ticketreader.util; 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 String millisToString(long ms) { + long totalSecs = ms/1000; + long hours = (totalSecs / 3600); + long mins = (totalSecs / 60) % 60; + long secs = totalSecs % 60; + String minsString = (mins == 0) + ? "00" + : ((mins < 10) + ? "0" + mins + : "" + mins); + String secsString = (secs == 0) + ? "00" + : ((secs < 10) + ? "0" + secs + : "" + secs); + if (hours > 0) + return hours + ":" + minsString + ":" + secsString; + else if (mins > 0) + return mins + ":" + secsString; + else return "00:" + secsString; + } + 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/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d512d40..14e2a0f 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,25 +1,25 @@ - - - - + - + diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 465051a..183a4d0 100755 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -1,223 +1,224 @@ - - + - - + - - + - - + ads:adUnitId="ca-app-pub-2102716674867426/2375937430"> + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 645a45d..4cd5695 100755 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,40 +1,40 @@ 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 + In corso 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.
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… 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! Impostazioni Disattiva se non vuoi che Android apra automaticamente l\'app quando viene avvicinato un biglietto Apertura automatica \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f78a7c..0e31855 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,41 +1,41 @@ 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 + In progress 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.
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! Settings Turn off if you don\'t want Android to start the app when a tag is scanned Automatically open
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 60e8097..3eef579 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,9 +1,9 @@ - \ No newline at end of file diff --git a/build.gradle b/build.gradle index eaa42a0..ddd15c8 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.2.1' + classpath 'com.android.tools.build:gradle:3.5.2' // 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 } diff --git a/gradle.properties b/gradle.properties index aac7c9b..9e6fce1 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,19 @@ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e9cd780..e597517 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Dec 01 10:58:45 CET 2018 +#Mon Aug 26 13:12:33 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip