Wednesday, May 25, 2016

Rejsekort, NFC og smartphone kompatibilitet

Det sker ikke så sjældent, at jeg får en mail på noget lignende følgende form:
Min telefon har da NFC, hvorfor kan jeg så ikke downloade din app?
Hvorfor sker ikke noget når jeg scanner kortet med din app?
Jeg savner et sted hvor man kan se om hvilke telefoner der er kompatible med din app (Rejsekortscanner). Jeg kan finde nogle spredte informationer skrevet af dig. Nogle er 2 år gamle på din blog. Hvorfor vedligeholder du ikke bare en liste?
Det er jo gode spørgsmål som jeg har svaret på rigtig mange gange de seneste par år. Det siger sig selv, at som udvikler er jeg naturligvis interesseret i, at gøre det så nemt og smertefrit for potentielle brugere som muligt - men Rejsekort og NFC er og bliver bare et kompliceret emne. Herunder har jeg forsøgt at bryde emnet ned i let-fordøjlige bidder.
  • Hvad er NFC?
    NFC (Near Field Communication) udspringer af envejskommunikation RFID (Radio Frequency IDentification), og tillader kontaktløs tovejskommunikation med et smartcard. Det er altså muligt, over en meget kort afstand, at kommunikere elektronisk med en chip på f.eks. et adgangskort, Rejsekort eller Dankort. Dette sker på frekvensen 13.56MHz og med en båndbredde på 106 kbit/s. Det var firmaerne NXP (tidligere Philips) samt Sony, der pionerene NFC i sin tid og i dag er især NXP stadig en stor spiller inden for NFC, men Samsung, Broadcom mv. er også kommet til. NFC teknologien er nu (delvist) standardiseret af NFCForum under ISO/IEC 14443A. Men sådan har det ikke altid været!

  • Hvad er Mifare?
    Før NFC blev hvermandseje og indbygget i de fleste mobiltelefoner, var teknologien mere eksklusiv og blev primært brugt til elektroniske adgangskort, billetter mv. Til dette var der behov for et såkaldt krypteret "sikkert element", altså et område på kortet man ikke bare sådan uden videre kan læse og skrive til - af hensyn til sikkerhed og svindel. En af disse teknologier fra NXP hedder Mifare og det er dén teknologi rejsekortet også benytter. Selv om NXP var med til at standardisere NFC, så var de smarte nok til at sørge for en udvidelsesmekanisme, således at de stadig kunne bibeholde deres egen overbygning, i form af Mifare - uden at dette stred direkte imod standarden. Ligesom NXP har Mifare som overbygning, har Sony iøvrigt deres Felicia teknologi.

  • Burde alle NCF telefoner ikke kunne læse rejsekortet?
    I teorien jo, for protokol og frekvens er helt den samme og andre producenter end NXP kan stadig godt få lov til at kommunikere med Mifare typen, mod at betale en licens til NXP og implementere det i deres driver. Problemet er bare, at det gør ingen af de andre store spillere som Sony, Samsung og Broadcom. Istedet læner de sig, måske fornuftigt nok, op af den åbne standard - uden at tænke på evt. propriotære udvidelser. Det er derfor en smartphone skal have en NFC chip fra NXP, for overhovedet at have mulighed for at kunne læse rejsekortet!

  • Hvorfor vælger Rejsekort A/S at benytte sig af Mifare?
    Jo det gør de, fordi det i mange år har været de-facto standard inden for elektronisk rejsehjemmel og fordi teknologien er fra før mobiletelefoner med NFC blev hvermandseje. Rejsekort A/S har også haft krav om en pæn portion hukommelse på kortet, p.g.a. det danske system er et nationalt asynkront system, så der var ikke rigtig så mange alternativer. De har også haft krav til sikkerhed, p.g.a. risikoen for svindel med data på kortet. I praksis er der dog ikke megen sikkerhed, da krypteringen blev knækket tilbage i 2007 ligesom nøgler ligger spredt rundt på internettet og i diverse apps. Sikkerheden af systemet må derfor formodes primært at blive håndteret via overvågning af backend systemerne. Her i 2016 findes der åbne alternativer ligesom, eftersom den fysiske sikkerhed er ikke-eksisterende, man burde kunne formattere Mifare kortet til kun at bruge standardiserede NDEF records - så ville alle NFC telefoner i princippet kunne aflæse kortet, og så ville krypteringen blot ligge i software.

  • Findes der ikke en liste over telefoner et sted?
    Der er faktisk ikke et sted på nettet med troværdig information omkring en telefon har Mifare support eller ej, det bedste der findes er en liste på shopnfc men denne har ofte fejl og mangler. Jeg forsøgte faktisk også i starten at vedligeholde en liste, men det skulle hurtigt vise sig at være en uholdbar praksis. Der kommer nye telefoner, samt nye versioner af eksisterende telefoner til, og det er ingen nem sag at finde ud af, om den har en NFC chip fra NXP eller ej - nogen gange kan man finde ud af dette på ifixit, men her lander kun de største og mest kendte telefoner. Den eneste sikre metode er derfor simpelthen at gå ned i din lokale butik, og prøve at installere og afprøve f.eks. Rejsekortscanner.

  • Hvorfor virker Google Play filtreringen ikke altid?
    Prøv f.eks. at se det screenshot herunder, fra Google Play. Som du måske kan se, er der 432 manuelt udelukkede enheder alene fra Samsung! Dette er altså telefoner med NFC, men som jeg (af den ene eller anden årsag) ikke mener har en NFC chip fra NXP og derfor ikke vil kunne bruge min app Rejsekortscanner. Bemærk også, at der er flere forskellige S6, S6 Edge og S6+ modeller - så man KAN faktisk slet ikke skrive "Virker ikke med S6" for det kommer an på hvilken af de 15 versioner det drejer sig om. Samsung S4 Mini (som min kone har) har aldrig kunne læse Mifare, men seneste model kan sjovt nok godt, så nu har jeg specifikt åbnet for denne. Sidst men ikke mindst, ser jeg også en del rapporter fra brugere der har enheder som Google Play simpelthen ikke kender endnu (f.eks. PHICOMM PASSION).


Det bedste jeg p.t. kan gøre, er at medsende så meget jeg kan omkring en brugers telefon, når de indsender en fejlrapport i app'en. Dette er nogengange nok til, at jeg kan identificere den inde i Google Play og åbne på eller lukke ned for den. Så når der kommer nye modeller til (og versioner af modeller), har disse jo som udgangspunkt lov til at installere Rejsekortscanner. Kun når jeg bliver bekendt med modeller/versioner der giver problemer (typisk brugere der sender vrede mails eller giver én stjerne som bedømmelse) skynder jeg mig ind på Google Play for at tilføje i filtret. Det er dog ikke altid jeg når det i god tid (jeg har job, familie osv. som så mange andre) og så er jeg da lidt ked af at få hivet gennemsnittet ned på ratingen af app'en når det nu ikke er min skyld, men sådan er det bare.

Sidst men ikke mindst, er der nogen gange driver problemer på telefoner på bestemte versioner af Android. Eksemplerne på dette er:
  • Visse Samsung telefoner med Android 5, returnerer ved en fejl 17 bytes for en Mifare blok hvor den kun skal returnere 16
  • På visse HTC One kørende med Android 5 har er NfcA null i TechExtras og skal trækkes ud via et hack
  • På Sony Xperia Z3 findes der 2 NfcA med forskellige SAK værker i TechExtras, som skal patches med et hack
  • Nexus X5 som ser ud til at virke 1/10 gange om som derfor er blokeret indtil jeg kan finde en løsning


På grund af desse problemer, forsøger jeg nu at logge (via Google Analytics) når folk bruger Rejsekortscanner og de IKKE kan læse kortet p.g.a. manglende Mifare. Dette samler jeg op i noget statistik og snart vil jeg formentlig kunne bruge dette til at lukke ned noget hurtigere. Det betyder at jeg på sigt nok VIL være i stand til at lave en form for liste, men den vil være meget stor, særdeles teknisk og vil aldrig være så brugervenlig at hr og fru Danmark bare lige kan se "S5, jo den er på listen".

Håber det var et klart nok svar - i en desværre noget uklar NFC/Mifare verden.

Monday, May 2, 2016

Android: Remove a dependency from a build variation

Android's build variations (flavor and buildType) feature is an awesome way of maintaining several cuts from the same source code. I've used it both for white-label branding solutions and for Free vs. Pro versions. However, sometimes you have different dependencies between the versions where you will usually have to live with the fact, that all variations of your app gets the superset of dependencies. There are ways around this however, and I used it to shave over 400Kb off from a Free variation to a Pro variation.

The problem

Java is a static language, so you can't just go ahead and rely on late-bound linking and have Proguard slice and dice. It's unclear to me, whether the Android variations mechanism extends to Java source code, but I don't think it does. It's also unclear to me, whether an SPI approach could be used, letting the build class-path inject different implementations of some API for use at compile-time. What IS clear to me however, is that we can avoid pulling in a dependency for a flavor its not needed for, which means we will have to use reflection for the flavor that actually needs the stuff.

Removing AdMob from the Pro variation

It doesn't really matter what this app does, the important aspect is that there's a Pro flavor which does not contain the Google Ads from the Free version. So the first thing we're going to need, is to tell Gradle that the AdMob dependency is only for the free flavor. With Gradle, this is as easy as prepending the flavor name in front of the scope, such that "compile" becomes "freeCompile":



Now the dependency is not included for the Pro version. The next thing we need to do, is fix the problem of the static linking with the Ads dependency. The culprit code looks like the following:

 
 
protected void installAd(){ final AdView adView = (AdView)findViewById(R.id.adView); // Only the "free" versions of layouts has an AdView if(adView != null){ final AdRequest adRequest = new AdRequest.Builder().build(); adView.loadAd(adRequest); } }

The above works great in the Free version, but since we modified the Gradle build script, the Pro version no longer compiles! The second thing we then need, is a way to only use the Google Ads for the Free flavor, and do so through reflection in order to not require compile-time linking.

 
 
protected void installAd(){ final View adViewInstance = findViewById(R.id.adView); // Only the "free" versions of layouts has an AdView if(adViewInstance != null){ try { final Class builderClass = Class.forName("com.google.android.gms.ads.AdRequest$Builder"); final Object builderInstance = builderClass.newInstance(); final Method buildMethod = builderClass.getDeclaredMethod ("build"); final Object adRequest = buildMethod.invoke(builderInstance); final Class adViewClass = Class.forName("com.google.android.gms.ads.AdView"); final Method loadAdMethod = adViewClass.getDeclaredMethod("loadAd", adRequest.getClass()); loadAdMethod.invoke(adViewInstance, adRequest); } catch (Exception e) { Log.w(TAG, "Ads could not be loaded due to: " + e.getMessage()); } } }


Note that in the code examples above there's actually no build flavor check to be seen. For this, I rely on the flavor mechanism again. Only the layout from the Free flavor has the R.id.adView view. If this view can not be found, as is the case for the Pro flavor, I skip over doing anything.



The third and final step is easy to forget. The powerful post-processing tool Proguard will remove all unused classes in an attempt to trim down the size of our shippable production build. However, we now rely on reflection in the code, which Proguard's static analysis of the source code won't be able to tell. In stead, we need to instruct Proguard NOT to optimize away the specific code that we rely on. If we don't do this, the reflection magic will have no effect and in my case that means no ads will load but it's also not uncommon to see the app simply crash! Edit your proguard-rules.pro and include the appropriate directives:

 
 
-keepclassmembers class com.google.android.gms.ads.* { 
   public *; 
} 
 

And while I'm in there, I remove all that logging which serves no purpose in a production build anyway. I think it's safe to assume that no end user is sitting with adb and logcat!

 
 
-assumenosideeffects class android.util.Log { 
    public static boolean isLoggable(java.lang.String, int); 
    public static int v(...); 
    public static int i(...); 
    public static int w(...); 
    public static int d(...); 
    public static int e(...); 
} 


There's a fourth and somewhat optional step to mention. The code that you removed, may also let you remove permissions from your application manifest. This is the case for me with the Google Ads, which requires "android.permission.INTERNET" and "android.permission.ACCESS_NETWORK_STATE". My Pro flavor no longer needs these, and no app should request more permissions than it needs, so lets remove this. Once again, the power of the Android flavor mechanism comes to the rescue. Previously I had just one AndroidManifest.xml located in /src/main folder. We can leave this one as the "master" and remove the AdMob specific stuff, which is not applicable to the Pro flavor. Then add a new AndroidManifest.xml under /src/free/ and include the stuff specific to the Free version, namely the requires permissions which will be merged into one.

<?xml version="1.0" encoding="utf-8"?> 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.bangbits.rejsekortscanner"> 
 
    <!-- This is needed for Google AdMob --> 
    <uses-permission android:name="android.permission.INTERNET" /> 
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 
 
</manifest> 


That's it! The Pro flavor is now trimmed down to about 400Kb lighter. This results in a smaller download, speedier load-time and lower memory pressure. Do you know of a better way then please let me know though the comments!





Wednesday, April 27, 2016

Rejsekortscanner Pro

Forleden frigav jeg version 2.0 af Rejsekortscanner, der repræsenterer en væsentlig stabilisering af fortolkningen af rejsekortet samt forbedringer i brugeroplevelsen. Men alt dette blev faktisk sat i gang p.g.a. folk der skrev til mig, at de ønskede en version uden reklamer - en sådan version er nu tilgængelig!



Versionen uden reklamer har jeg valgt blot at kalde Pro, fordi noget skulle den jo hedde der adskilte den fra gratis versionen. App'en henvender sig til folk der bevidst hellere smider en skilling istedet for at være udsat for et reklamebanner. Jeg hader selv reklamer, hvorfor jeg har været tæt på helt at droppe reklamebanneret. Men selv om man ikke ligefrem bliver rig af at have en niche app der er installeret hos 17.000 brugere, så får man trods alt betalt lidt mobilregning mv. og der ligger stadig rigtig mange timers arbejde bag Rejsekortscanner.

Spørgsmål og svar

  • Er 25kr ikke lige dyrt nok?
    Næææ det synes jeg egentlig ikke. Momsen udgør 5kr, Google skal have 6kr og skattefar skal have 6-7kr - så jeg tjener også omkring 6-7kr netto, hvilket er nok til en Mars bar!

  • Hvorfor har det taget så lang tid?
    Udover at dette er foregået i min fritid, så har jeg også følt at kvaliteten skulle væsentligt i vejret når man begynder at tage betaling for noget. Der er også brugt meget energi på at få en masse gode test eksempler op. Jeg har ikke resourcer der tilnærmelsesvis kommer tæt på hvad Rejsekort f.eks. selv kunne stille med, så jeg er dybt afhængig af de fejlrapporter brugere indrapporterer som typisk ender med at indgå som tests. Sidst men ikke mindre, jeg er nok lidt af en perfektionist - kompromiser kan være nødvendige, men jeg bryder mig ikke nødvendigt om dem.

  • Er den eneste forskel ved Pro versionen, at der ikke er reklamer i den?
    Ja, men dette betyder dog også, at Pro versionen fylder mindre, er knapt så ressourcetung og derfor også starter hurtigere. Derudover er det sandsynligt, at jeg vil betragte Pro versionen som mere stabil og "røgteste" nye features i den gratis version.

  • Hvorfor kan app'en ikke installeres på min enhed?
    Det skyldes desværre, at app'en kræver en enhed med understøttelse for en bestemt type NFC ved navn Mifare Classic. Det er kun NFC chips fra firmaet NXP der har denne support og det svinger meget hvilke fabrikanter og modeller der bruger disse. Derfor forsøger jeg at styre hvad Google Play tilbyder, men det er lidt af en umulig kamp da nye enheder hele tiden kommer til.

  • Hvilken enhed skal jeg bruge for at kunne benytte app'en?
    Generelt så kan mange Sony modeller (Z1, Z3, Z5) læse rejsekortet, ligesom en del fra HTC (M7, M8, M9) men det er kun lidt ældre modeller fra Samsung der med sikkerhed kan bruges (S3 og S5) samt nyeste S4 Mini. Huawei (P7 og P8) har også enheder på markedet der kan benyttes. Det samme gør sig gældende for LG (G3 og G4). Google's egne Nexus 6P og Nexus 5X skulle også være et sikkert valg. Men hvis du står og skal til at købe ny mobil og ønsker at kunne følge med på dit rejsekort, er det sikreste simpelthen at prøve app'en i butikken før du køber - det har jeg selv gjort et par gange.

  • Kan jeg teste app'en selv om den ikke er tilgængelig via Google Play?
    Ja det kan du godt, men det kræver du slår "Ukendte kilder" til inde under sikkerhedsindstillingerne på din enhed. Dernæst kan du installere den rå APK fil uden om Google Play, som du kan downloade her. Jeg hører meget gerne om dine erfaringer så jeg kan opdatere listen på Google Play - email mig på casper+rejsekortscanner@bangbits.com.

  • Hvad er fremtidsplanerne for app'en?
    Jeg kunne rigtig godt tænke mig at kunne vise rabatniveauer (rabattrin) samt serviceniveauer (standard, DSB 1' mv.) til brugeren så det er nok det næste jeg vil kigge på.



Thursday, April 21, 2016

Rejsekortscanner 2.0

Jeg har i længere tid arbejdet på en betalingsversion af Rejsekortscanner, til dem der efterspørger at komme af med reklamerne. Men når man lancerer noget man tager betaling for, vil kunder naturligt forvente høj kvalitet og jeg følte ikke helt at den gamle version kunne leve op til dette.

Dette betød, at jeg i større grad skulle benytte mig af den dokumentation jeg har fået adgang til fra Rejsekort A/S, ligesom jeg havde brug for en stor pulje af tests for at sikre imod regressioner - for jeg har stadig begrænset adgang til den forretningslogik der ligger til grund for rejsekortet (hvad er den maksimale rejsetid, hvad er den maksimale transit tid, hvordan hånderes vintertid osv.).

Derfor besluttede jeg mig for, at både den gratis (med reklamer i) samt kommende betalingsversion, skulle baseres på samme kode og derfor begyndte arbejdet først og fremmest på en version 2.0 af den gratis udgave. Det har taget længere tid end ønsket, men det var dét der skulle til. App'en er altså blevet skrevet helt om fra bunden af, med et utal af forbedringer. Her er et par af dem:
  • Afkodningen af data på rejsekortet er langt mere troværdig da jeg har haft adgang til den officielle dokumentation fra Rejsekort A/A
  • Over 600 unit tests bliver brugt til at sikre imod introduktioner af nye fejl (regressioner)
  • App'en har fået et friskt nyt design, fordelt på skærmene "Status", "Hændelser" samt "Detaljer"
  • På trods af, at app'en henter meget data ud, er den blevet hurtigere til at kommunikere med rejsekortet, typisk 1/3 sekund
  • App'en vi nu fortælle dig om du har glemt at checke ud hvis det er længere end 12 timer siden du er checket ind
  • Sommer/vintertid-problematik håndteret som det skal - dette har altid være problematisk i den gamle version
  • Understøttelse af Dansk og Engelsk sprog - før kunne kun benyttes Dansk
  • Minimeret programstørrelse til blot 1.9Mb - på trods af nye features og bedre grafik


Et af de få minuser jeg bør nævne er, at jeg har været nødt at fjerne supporten for gamle Android 2.3 (API 10) enheder således at minimumskrevet nu er Android 4.03 (API 15). Der var mindre end 0.1% af mine brugere der benyttede sig af så gamle enheder, at det ganske enkelt ikke længere gav mening at bruge energi på at holde app'en kompatibel.

Betalingsversionen kommer meget snart, jeg skal lige have testet den nye kode via gratis-versionen først. Herunder kan du se nogle eksempler på skærme fra den nye app.

Status skærmen for et kort der er checket ind


Status skærmen for et kort der er checket ud


Status skærmen for et kort der ikke er checked ud i tide


Status skærmen for et kort der er blevet blokeret p.g.a. misbrug og lignende


"Hændelser" skærmen hvor de seneste rejser kan aflæses


"Detaljer" skærmen hvor information om kortnr, type, status, saldo, optankningsbeløb mv. kan aflæses




Find app'en i Google Play, eller hent den hér via Dropbox. Pro versionen uden reklamer skulle også lande på Google Play i løbet af nogle dage.

Tuesday, April 19, 2016

Beware of SQLite and the Turkish Locale

Today I came across a truly puzzling issue on Android. An otherwise tried and tested application crashed consistently when running on a device using Turkish as the current locale.

App crashing on a Turkish device

The problem occurs in a large app developed for a customer, inside a proprietary binary component, so no direct debugging was possible. All I had available was a vague stack trace showing the root problem to be a NullPointerException from trying to parse an integer:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Integer.intValue()' on a null object reference

Not much to go on, the goal was now to figure out just what could cause such a problem just from running on a device using Turkish Locale.



Having spent some time on Google, searching for other people's trouble with the Turkish Locale, it became clear that there are issues with lowercase Strings in Turkish. I started working on a small isolated app which would aid in narrowing down the exact issue. It wasn't long until the suspicion I had turned out to be spot on.

SQLite with Turkish Locale

The problem was caused by relying on the device Locale for representing SQL Strings and the fact that the SQLite parser must be converting the SQL literals to uppercase. That becomes a problem, because in Turkish the lower case "i" becomes "İ" in uppercase. Inside SQLite, the parser is therefore seeing "İNSERT" rather than "INSERT" which is of course not valid SQL. This is actually specified in the Android documentation for the Locale class and the subject was also covered by none other than Jeff Atwood some years ago.

To reproduce the problem, create a new project in Android Studio with an empty activity. Place the following XML for the layout:

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
 
    <Button 
        android:layout_width="300dp" 
        android:layout_height="48dp" 
        android:text="SQL insert in lowercase" 
        android:gravity="center_horizontal" 
        android:onClick="insert"/> 
 
</RelativeLayout> 


...and add the following Java code to the activity:
public class MainActivity extends AppCompatActivity { 
 
    private final static String DML_CREATE = "CREATE TABLE IF NOT EXISTS demotable(Name VARCHAR);"; 
    private final static String SQL_INSERT = "insert into demotable values('John Doe');"; 
 
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); try(SQLiteDatabase demoDatabase = openOrCreateDatabase("demotable",MODE_PRIVATE,null)){ demoDatabase.execSQL(DML_CREATE); } setContentView(R.layout.activity_main); }
public void insert(View view){ try(SQLiteDatabase demoDatabase = openOrCreateDatabase("demotable",MODE_PRIVATE,null)){ demoDatabase.execSQL(SQL_INSERT); } } }

Although being perfectly valid Java and SQL, the code above will crash consistently when running on a device with a Turkish Locale!

Caused by: android.database.sqlite.SQLiteException: near "İNSERT": syntax error (code 1): , while compiling: İNSERT İNTO DEMOTABLE VALUES('JOHN DOE');

The stack trace makes it blatantly obvious what happens. If only I had such a nice stack trace originally I could've saved a few hours, but such is life when you depend on proprietary closed source components. :(

Solutions

There are two typical proposals for how to solve the issue. This can either be done by always converting SQL Strings to upper-case:
 
 
public void insertUppercase(View view){ try(SQLiteDatabase demoDatabase = openOrCreateDatabase("demotable",MODE_PRIVATE,null)){ demoDatabase.execSQL(SQL_INSERT.toUpperCase()); } }

...or (better in my opinion) make sure SQL Strings are always interpreted using the root locale by converting to lower-case while providing the neutral root locale:
 
 
public void insertLowercaseEnglish(View view){ try(SQLiteDatabase demoDatabase = openOrCreateDatabase("demotable",MODE_PRIVATE,null)){ demoDatabase.execSQL(SQL_INSERT.toLowerCase(Locale.ROOT)); } }

Although I have not tested it, it's also likely that a solution could be to externalize the SQL as a simpler character set (ASCII) rendering the the issue moot. However, most developers will probably just embed SQL in Java unicode Strings. Feel free to comment on this if you have other solutions.

Lesson learned

Something tells me this is a very common bug out there, the intricate details of "i" in the Turkish Locale are probably only known to Turks. The only reason why more Java developers don't run into this, is probably because they typically operate and control the host runtime. However, when dealing with an app, the environment is not as neatly encapsulated and testable so this problem bubbles up to the surface and hits you hard.

The lesson learned must be to never rely on the default Locale, not even for something as innocent as Java Strings. While we are able to remedy the issue when using SQL as an embedded DSL, one can easily imagine the same bug sneaking in countless other ways. For example, annotations come to mind, where you do not have the luxury of being able to manipulate the String before its sent to be parsed by some annotation processor!