Monday, November 27, 2017

Klaphatte til app brugere

This entry is in Danish, as it contains quotes in this language which can not readily be translated without loss of meaning.

Som app udvikler, må man være parat til at modtage en del flak (læs: beskydning med spredhagl fra folk der ikke ved bedre men brokker sig i øst og vest) og det er jo et relativ kendt fænomen der er skrevet om utallige steder som f.eks. her. Der sker bare noget med folk når de i relativ anonymitet, får lov til at udtale sig og bedømme på et meget spinkelt grundlag - pludselig er de eksperter og kunne lave det meget bedre selv.

Eksempel på en klaphat
I dag modtog jeg f.eks. følgende review fra Jesper (fulde navn og email bekendt af redaktionen) som jeg, som så ofte før, besvarer inde på Google Play.



Jesper er jo et klassiske eksempel på en fejl-informeret bruger med en inkompatibel tlf. der ikke helt har brugt tid på at undersøge sagen nærmere. Fred være med det tænker jeg, NFC formater er også et tricky emne for alm. mennesker at forholde sig til. Men så modtager jeg en opfølgende email:

Min telefon har en NFC chip. Problemet er at appen ikke er sat op til alle nyere telefoner og kun meget få udvalgte. Det er jo idioti. Måske jeres app ville få en anelse bedre rating hvis den nu var udviklet ordentligt..

Ok, manden har tydeligvis ikke forstået noget af det jeg skriver hverken på Google Play, på min blog eller i svaret til hans review. Tilmed indgår nu også nedladende ord som "idioti" ligesom app'en ikke er "udviklet ordentligt". Jeg bliver lettere irriteret over tonen, og skriver tilbage igen, denne gang på hans eget sprog:

Hej Jesper,
Heldigvis er app'en en af de bedst ratede Rejsekort app's på Google Play. Ratingen ville dog være endnu bedre, hvis det ikke var for klaphatte som dig der tror de ved bedre end de gør. Hvis du vil vide lidt om hvordan det i virkeligheden forholder sig, så kan du læse hvad jeg skriver om sagen her:
http://blog.bangbits.com/2016/05/rejsekort-nfc-og-smartphone.html
For at opsummere; nej NFC er ikke bare NFC, der finder mange variationer og Rejsekort A/S benytter sig af Mifare som desværre ikke alle NFC chip understøtter. App'en er sat op til altid at tillade installation af Rejsekortscanner på nye tlf, kun hvis jeg får tilbagemeldinger fra brugere, har jeg mulighed for at filtrere fra i Google Play. De fleste mennesker er flinke og forstående for denne udfordring som jeg ikke selv er herre over, andre tror de ved bedre og giver dårlig rating selv om det drejer sig om en gratis app der hjælper 20.000 mennesker til daglig. Du bestemmer naturligvis selv hvilken kategori du ønsker at tilhøre. :)

Her synes jeg ligesom jeg gør det synligt hvilket tyndt grundlag Jesper egentlig befinder sig på ligesom jeg giver ham en mulighed for at bekende sig til den kategori at personer der får lært noget nyt og indrømmer det - det gør vi alle jo fra tid til anden. Men nej...

Kalder du mig for en klaphat..?? Nu er det jo ikke mig der har udviklet noget der ikke virker. Det er jo dig. Så klaphatten må jo være dig selv. Måske du kunne finde ud af at udvikle en converter, så app'en kan bruges på endnu flere. Hvis det kun er 20000 ud af 2 mio. Rejsekortbrugere må man jo sige at du laaaaaaaaaangt fra har gjort dit arbejde godt nok. Men fint med mig at du ikke kan tåle ærlig og konstruktiv kritik og at du bare bliver modbydelig og ond. Men det er fint at du har givet mig det på skrift, så jeg kan videregive til én af mine rigtig gode venner der er journalist og som hader sådan nogle mennesker som bare sviner sine kunder eller måske kommende kunder til. Det er i alle fald ikke det bedste udgangspunkt at sætte sig selv i. Der er noget der hedder at kunden altid har ret. Måske du skulle tænke lidt mere over det..
Jeg ved ikke hvad der får Jesper til at opfatte sig selv som kunde (de betaler normalt for noget) for han er allerhøjst bruger af en app jeg gratis stiller til rådighed. Nuvel, jeg overvejer at ignorere klaphatten men vælger alligevel at forsøge at forklare ham hvorfor jeg ikke mener hans overfladiske og usaglige kritik er speciel konstruktiv:

Jesper,
Fakta er, at jeg skriver højt og tydeligt i Google Play følgende:
"Hvis app'en ikke virker på din smartphone p.g.a. manglende hardware support, undlad venligst at give negativ feedback, da det reelt er telefonens og ikke programmets skyld! Jeg hører dog gerne fra dig, således at jeg kan opdatere listen og undgå situationen for andre. :)"
Men det har du tydeligvis ikke taget dig tid til at læse, men brokker dig med en meget negativ tone som om du har forstand på hvad du taler om, og det har du tydeligvis ikke hvorfor jeg skriver direkte til dig omkring årsagen til at app'en ikke virker på din tlf. og hvorfor den ej heller nogensinde kan komme til det. Jeg linker også til en artikel på min blog der i årevis har beskrevet situationen, som du nemt kunne have fundet hvis du havde gjort dig den ulejlighed at søge på Google engang.
Dét du kommer med er ikke konstruktiv kritik, af de årsager jeg nævner ovenover - det er ikke noget at gøre, du er nødt til at købe din en anden tlf. hvis du vil bruge app'en! Desværre fortsætter du din dårlige stil, hvor du overhovedet ikke forholder dig til hvad jeg forklarer (har du overhovedet læst det?) og jeg kan derfor naturligvis ikke bruge mere tid på dig, da du virker uden for pædagogisk rækkevidde. Jeg ved ikke hvad du tror du får ud af din journalist trussel, men det vil jeg da i givet fald glæde mig til at se - du vil nemlig primært udstille dig selv som noget af en klaphat.
Jeg skulle nok bare have fulgt min intuition og ignoreret ham, for snart efter modtager jeg følgende:

Casper klaphat...
Spadser-Casper...
Mongol-Casper...
Casper Knold...
Casper Papkasse...
Lorte-Casper...
Casper Jubel-Idiot...
Koka-Casper...
Ja jeg kunne blive ved. Det er faktisk meget sjovt at du tror at du forstand på udvikling af apps.. Jeg ved tilfældigvis at det kan lade sig gøre at konvertere eksempelvis en bluetooth-version og dermed ville det også, hvis man altså er dygtig og intelligent nok til det, være muligt at udvikle en konverter til en NFC chip..
Men det formår du nok ikke med din mikroskopiske fuglehjerne.. Jeg tænker bare: Du kan jo ikke formulere dig korrekt, ej heller stave, og ej heller argumentere godt nok for din viden. Og jo jeg har set din blog, hvilken der ikke er basis for at prale af.. Den ligner noget min 8-årige nevø kunne lave meget bedre. Han kunne i alle fald have stavet, formuleret og konstrueret den bedre end du har formået.
Men jeg takker for din venlighed og for din uduelighed. Der er jo tydeligvis ingen grænser for hvad jeg kan tillade mig, med den måde du opfører dig på. Nu har jeg jo ikke tidligere kaldt dig alle mulige mærkelige ting, selvom du allerede den første gang du svarede på min forespørgsel svinede mig til.
Jeg tror ikke jeg behøver at kommentere på ovenstående, det taler vist for sig selv.

Konklusion
Man kan ikke gøre alle tilfredse og nogen mennesker forbliver bare uden for pædagogisk rækkevidde. Den tid jeg har brugt på klaphatten Jesper kunne jeg have brugt på min familie eller noget reelt arbejde. Så lektionen for i dag må være, pas på klaphatte hvis eneste formål er at stjæle din tid!

For at få bare en lille smule ud af den tid jeg har brugt på at forsøge at trænge igennem til Jesper, har jeg foreviget den uredigerede dialog i dette blog-entry - så har klaphatten også noget på skrift til hans journalist ven. :)


Thursday, April 20, 2017

Android NFC radio control using instrumentation

I have always worked a lot with NFC on Android. For this reason, I tend to favor real devices over emulators, since missing an NFC radio means there's no way to truly test the intricacies of radio communication. Unfortunately, one can not power cycle the NFC radio using any official API unless going through hoops and using rooted devices, so ensuring NFC radio power state during testing is an uphill battle. For instrumented test scenarios however, there is actually a way forward.

UIAutomator to the rescue

While not as elegant as using an API, we can launch the settings screen for NFC and manipulate it through the use of instrumentation. This is NOT possible using modern Espresso which limits you to the app under test, but thankfully the UIAutomator framework is still available. The accompanying UIAutomator Viewer tool (which has now moved to sdk/tools/bin/uiautomatorviewer) is a great asset in this regard as it helps us identify the widget we need to manipulate.


What the NFC toggle button is named is not consistent across devices and versions of the operating system, so we have to get a bit heuristic here. In practice, looking through my some 10 devices with various versions of Android using various custom skinning, I have identified 3 unique resourceId's for the toggle button. These are com.android.settings:id/switch_widget, android:id/switchWidget and android:id/switch_widget. Unfortunately, on Android 7 (for Huawei devices anyway) it seems as if launching the ACTION_NFC_SETTINGS intent will not actually get you to where you want, but requires an additional navigational step. This complicates the code a bit but it's still possible to make it work.

To launch the Settings activity prior to any Activity under test, we need to pass along the Intent.FLAG_ACTIVITY_NEW_TASK flag. From there, we can write our logic to help us toggle NFC state.

    private void toggleNfc(final Context context) {

        final Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);

        final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        findAndToggleNfcInUI(device);
    }

    private void findAndToggleNfcInUI(final UiDevice device) {

        final UiObject toggleButton = device.findObject(new UiSelector()
                .resourceIdMatches("(com.android.settings:id/switch_widget|android:id/switchWidget|android:id/switch_widget)"));

        try{
            toggleButton.click();
            device.pressBack();
            return;
        }catch(UiObjectNotFoundException e){
            UiObject2 nfcMenuItem = device.findObject(By.textContains("NFC"));

            // Move up in the view hierachy until we're at a clickable item
            while(!nfcMenuItem.isClickable()){
                nfcMenuItem = nfcMenuItem.getParent();
            }

            // Issue click to navigate into menu
            nfcMenuItem.click();

            // Wait for any UI jitter to settle
            getInstrumentation().waitForIdleSync();

            // Try to toggle NFC button using this new child activity
            findAndToggleNfcInUI(device);
        }
    }


Composable test aspect using a JUnit rule

The code above is fine and dandy, but I'm a big proponent of composable and reusable aspects, so lets take advantage of the fact, that we can encapsulate the functionality nicely using JUnit's rule mechanism. If you're new to these you may read up on them here. The resulting NfcStateRule.class can be seen below.

/**
 * JUnit test rule for controlling NFC radio power state. Useful in order to ensure NFC is
 * enabled or disabled prior to executing a test.
 */
public class NfcStateRule implements TestRule {

    private static final String NFC_TOGGLE_WIDGET_RESOURCEIDS =
            "(com.android.settings:id/switch_widget|android:id/switchWidget|android:id/switch_widget)";

    private final boolean desiredState;

    public NfcStateRule(boolean desiredState) {
        this.desiredState = desiredState;
    }

    @Override
    public Statement apply(final Statement base, final Description description) {
        return new Statement() {
            public void evaluate() throws Throwable {

                try{
                    final Context context = InstrumentationRegistry.getTargetContext();
                    ensureNfcState(context, desiredState);

                }catch(final Throwable e){
                    e.printStackTrace();
                }
                base.evaluate();
            }
        };
    }

    private void ensureNfcState(final Context context, final boolean desiredState) {
        if(desiredState){
            ensureNfcIsEnabled(context);
        }else{
            ensureNfcIsDisabled(context);
        }
    }

    private void ensureNfcIsDisabled(final Context context) {
        if(isNfcEnabled(context)){
            toggleNfc(context);
        }
    }

    private void ensureNfcIsEnabled(final Context context) {
        if(!isNfcEnabled(context)){
            toggleNfc(context);
        }
    }

    private boolean isNfcEnabled(final Context context) {
        final NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(context);

        if (nfcAdapter == null) {
            return false;
        }

        return nfcAdapter.isEnabled();
    }

    private void toggleNfc(final Context context) {

        final Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);

        final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        findAndToggleNfcInUI(device);
    }

    private void findAndToggleNfcInUI(final UiDevice device) {

        final UiObject toggleButton = device.findObject(new UiSelector()
                .resourceIdMatches(NFC_TOGGLE_WIDGET_RESOURCEIDS));

        try{
            toggleButton.click();
            device.pressBack();
            return;
        }catch(UiObjectNotFoundException e){
            UiObject2 nfcMenuItem = device.findObject(By.textContains("NFC"));

            // Move up in the view hierachy until we're at a clickable item
            while(!nfcMenuItem.isClickable()){
                nfcMenuItem = nfcMenuItem.getParent();
            }

            // Issue click to navigate into menu
            nfcMenuItem.click();

            // Wait for any UI jitter to settle
            getInstrumentation().waitForIdleSync();

            // Try to toggle NFC button using this new child activity
            findAndToggleNfcInUI(device);
        }
    }
}

To use our NfcStateRule in an actual test, simply include it as a member and specify the desired NFC radio power state by passing a boolean with the constructor.

@LargeTest
@RunWith(AndroidJUnit4.class)
public class NfcDisabledTest {

    @ClassRule
    public static final NfcStateRule nfcStateRule = new NfcStateRule(false); // Make sure NFC is disabled

    ...actual test...
}

Voila, now it's possible to setup test scenarios correctly using the NFC radio. This is important for many of my Espresso tests to work consistently and reliably every time, as demonstrated by a screen below which tests the UI when the user has disabled NFC.

An example of an Activity/Fragment whos UI-state depends of the state of the NFC radio.

Conclusion

Where there is a will, there is a way! The above is not nearly as clean as having an API which we have available for WiFi, GPS etc. For acceptance testing however, I much prefer this kind of automated UI manipulation over mocking or polluting short-circuiting logic within the app itself.

By definition, the approach must be considered fragile since the NFC toggle button can be called something different on devices I have not yet had my hands on! If you run into this problem, the fix is easy - simply use the UIAutomator Viewer and expand the regular expression to work with this custom view. In a test scenario you usually have full control of the devices anyway so it's not really a practical concern since end-users will never be exposed to the code.

As usual, the code may be buggy, may not work on all versions of Android and is definitely not production safe. You may assume a Public Domain license of the code snippets above. Feel free to contribute back in the comments if you want to share your findings or experiences regarding the matter.

Thursday, January 5, 2017

BangBits Privacy Policy


Welcome to the BangBits Privacy Policy

When you use apps and other software developer by BangBits, you trust us with your information. This Privacy Policy is meant to help you understand what data we collect, why we collect it, and what we do with it. It is important to understand, that BangBits operate both as an owner of given software and as a proxy for work developer by Customers. App's published by BangBits but taking part of a specific Customer solution are treated separately in the "Specific Products" section below.


Information we collect and why we collect it

We collect information to provide better customer experience. This may happen through various forms of remote logging using Google Analytics, Firebase Analytics or similar tool. At no time is personal data directly mappable to an identifiable user being collected. What can be collected is:
  • Device identifiers (DeviceID, IMEI and handset identifiers) in order to black-list and/or white-list otherwise fraudulent and/or abusive users.
  • Stack traces and associated debugging data when app is behaving unexpectedly
  • Behavioral data to better understand how the user is using the software

Specific Products

The following notices explain specific privacy practices with respect to specific products offered by BangBits that you may use:

"Rejsekort Kontrol"

The software known as "Rejsekort Kontrol" is located on a closed business domain on Google Play, and as such, is only accessable to (invited) users of that Organization. The app under control of BangBits takes part of a bigger software system owned by Rejsekort A/S, the danish national transit ticketing authority. No personal identifiable data is collected or transmittet to/from the app. Recent travel data from a Travelcard is inspected and collected to be submitted for backend processing by Rejsekort A/S, but this is governed by Rejsekort A/S' own Privacy Policy at: 


https://www.rejsekort.dk/~/media/rejsekort/pdf/privatlivspolitik/privatlivspolitik---13-10-2014.pdf