donderdag 13 november 2014

Deploying an Android Wear app to a device

When you're developing an Android app, it's nice to test the result on an actual device. When developing Android Wear apps, this might even be more relevant because of the importance of the user experience (i.e. smaller screen size).

This post contains some steps/information which you might find use full in developing and deploying app to a wearable device using Android Studio.

Note: we assume that developer mode has already been activated on your handheld device.

Enable Debugging on SmartWatch (LG G-Watch)

Enable Developer Mode


  • Open Settings;
  • Open About; Find Build number and tap on it 7 times;
  • You're done when a toast popup appears with the message, "You are now a developer!" Swipe right (to go back) to the Settings menu. 

Enable ADB-debugging


  • Open Settings;
  • Open Developer options; 
  • Find and set ADB debugging to Enabled. You'll be asked if you're sure you want to enable. Tap the checkmark button to confirm.
  • Find and set Debug over Bluetooth to Enabled.


Enable USB Debugging on Android Smartphone

On the Android Wear app on your handheld, go to settings, and enable "Debugging over Bluetooth".



Connect the Smartwatch via ADB

Open a Terminal, if required navigate to the Android SDK Platform Tools,  and enter following commands:

$ ./adb forward tcp:4444 localabstract:/adb-hub; 
$ ./adb connect localhost:4444

Run the wearable app


  • In Android Studio, select your wearable app from the Run Configurations;
  • Press the "Play" button next to the selected configuration;
  • When the windows appears with the connected devices, you will see the wearable listed as a deployable device;
  • Select the wearable device and continue;
  • You can check in the "Run" status window if the application is successfully deployed;


woensdag 22 oktober 2014

Changing command line switches for Chrome on Android

While we were developing a web application, we ran into a little "snag" when a (Nexus 5 using a) Chrome browser tried to access our web application using a reversed proxy.  For some reason we received a Connection Closed after downloading the headers with some 8 kb of (corrupted) data.

When we narrowed it down that the data was compressed, we found out that Chrome enables SDCH by default, which is only supported by ... Google.  To narrow down the problem, we checked if we could access the server directly (worked via a VPN) and with different browsers (Opera and Firefox). When we used Firefox, it all worked perfectly (why? WebKit vs Gecko Engine...).

Because we actually wanted to point-out the problem, we wanted to be able to switch of the SDCH when using the Chrome browser (we used Chrome 38.0.2125.102 on Android 4.4.4). Here are the different steps we used:

Prerequisite:
  • You need to have root access on your Smartphone (we rooted a Samsung Galaxy Nexus with a Cyanogenmod);
  • You need developer access to your Smartphone;
  • You need to have adb (Android Debug Bridge) installed;
  • You need to have a good understanding of Linux/Terminal/VI;
As they say, with great power comes great responsibility. So please be careful on what you do with the root access!

Connect your device via a USB cable and start a Terminal. Connect to the device using:

$ ./adb shell

Change to root:

shell@maguro:/ $ su 


In your prompt VI a file called "/data/local/chrome-command-line" a

root@maguro:/ # vi /data/local/chrome-command-line 

Add this line (or whatever switches you may need) to this file:

 
--enable-sdch=0 --enable-sdch-over-https=0
~
~
~

Quite VI (:wq) and stop the Chrome instance running on your Android device. If you open the Chrome browser and navigate to "chrome://version", it should display the newly added switches  just below "User-Agent".








donderdag 16 oktober 2014

Robolectric + Google Play Service 5.x = java.lang.VerifyError

When I upgraded my application recently to include Play Services 5.x (for Wearable integration) I ran into the infamous VerifyError.

Thanks to this Google post, I could easily fix the issue.

In short, when you have this in your build file:
...
    //keep GMS play services at 4.4, 5.x only works with support-v4:21.+
    //compile 'com.google.android.gms:play-services:4.4.52'
    compile 'com.google.android.gms:play-services-wearable:5.0.77'

    compile 'org.bouncycastle:bcprov-jdk16:1.46'
...

Make sure to include:
tasks.whenTaskAdded { theTask ->
    def taskName = theTask.name.toString()
    //map "testLocalDebug" to your taskName!!
    if ("testLocalDebug".toString().equals(taskName)) {
        /**
         * Listen for when robolectric adds the 'testDebug' task and when it does, add the -noverify
         * option to that task's jvmArgs.  This allows us to turn off byte code verification when
         * running our unit tests.
         */
        theTask.jvmArgs('-noverify')
    }
}

To avoid:
java.lang.VerifyError: Expecting a stackmap frame at branch target 69
Exception Details:
  Location:
    com/google/android/gms/gcm/GoogleCloudMessaging.()V @31: goto
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0000000: b801 b54c 2ab7 011d 2ab6 0120 1301 2203
    0000010: 1202 b801 284d 2cc7 000b 2b10 2c04 54a7
...


maandag 18 augustus 2014

Code coverage reports using Robolectric and Android

Introduction


I've been writing quite a lot about Test Driven Development and some of the pitfalls that are complementing it. One of the import aspects of Test Driven Development, or Testing in General, is that you know what part of your code has been tested, and which still needs to be tested.

One of the frameworks you can use is JaCoCo, which integrates quite nicely with Gradle and Robolectric.


Update your build.gradle


First step is to update your build.gradle file. The excerpt can be found below. The full - working - example can be found on GitHub.

build.gradle

...

android {
    ...
    buildTypes {
        debug {
            runProguard false
            proguardFile 'proguard-rules.txt'
            debuggable true
            testCoverageEnabled = true

        }
    }

    ...
}

...

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.1.201405082137"
}

def coverageSourceDirs = [
        '../app/src/main/java'
]

task jacocoTestReport(type:JacocoReport, dependsOn: "testDebug") {
    group = "Reporting"

    description = "Generate Jacoco coverage reports"

    classDirectories = fileTree(
            dir: '../app/build/intermediates/classes/debug',
            excludes: ['**/R.class',
                       '**/R$*.class',
                       '**/*$ViewInjector*.*',
                       '**/BuildConfig.*',
                       '**/Manifest*.*']
    )

    additionalSourceDirs = files(coverageSourceDirs)
    sourceDirectories = files(coverageSourceDirs)
    executionData = files('../app/build/jacoco/testDebug.exec')

    reports {
        xml.enabled = true
        html.enabled = true
    }

}

Let's have a look at some of the most important elements.

  • You need to enable code coverage in your build type:
...

android {
    ...
    buildTypes {
        debug {
            runProguard false
            proguardFile 'proguard-rules.txt'
            debuggable true
            testCoverageEnabled = true

        }
    }

    ...
}
...
  • A new plugin is added and configured to the latest build version:
...

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.1.201405082137"
}

def coverageSourceDirs = [
        '../app/src/main/java'
]
...
  • In coverageSourceDirs you configure which folders are subject to introspection by JaCoCo. 
  • The JaCoCo plugin is configured to include your debug classes (which have been compiled) and to exclude the classes that you'll not test (e.g. the ButterKnife injections *$ViewInjector* classes).

...
task jacocoTestReport(type:JacocoReport, dependsOn: "testDebug") {
    group = "Reporting"

    description = "Generate Jacoco coverage reports"

    classDirectories = fileTree(
            dir: '../app/build/intermediates/classes/debug',
            excludes: ['**/R.class',
                       '**/R$*.class',
                       '**/*$ViewInjector*.*',
                       '**/BuildConfig.*',
                       '**/Manifest*.*']
    )

    additionalSourceDirs = files(coverageSourceDirs)
    sourceDirectories = files(coverageSourceDirs)
    executionData = files('../app/build/jacoco/testDebug.exec')

    reports {
        xml.enabled = true
        html.enabled = true
    }

}

Execute the gradle tasks

When you updated your gradle.build file, you will need to synchronise your development environment. Check if Gradle doesn't complain about any of the added plugins.

Before you can start to use the JaCoCo reports, you will need to provision the testDebug.exec file. The simplest way is to start a terminal window and executed the following gradle command on your project:

$ ./gradlew clean assemble


This will clear the compiled classes and assemble the build again.

Now you can render your JaCoCo reports via this command:

$ ./gradlew jacocoTestReport

The terminal will start your gradle build script, and one of the last tasks should be the rendering of the JaCoCo reports:
$ ./gradlew jacocoTestReport
:app:preBuild                
:app:preDebugBuild                
:app:checkDebugManifest                
:app:preReleaseBuild                 
:app:prepareComAndroidSupportSupportV42000Library UP-TO-DATE      
:app:prepareDeKeyboardsurferAndroidWidgetCrouton184Library UP-TO-DATE      
:app:prepareDebugDependencies                 
:app:compileDebugAidl UP-TO-DATE      
:app:compileDebugRenderscript UP-TO-DATE      
:app:generateDebugBuildConfig UP-TO-DATE      
:app:generateDebugAssets UP-TO-DATE      
:app:mergeDebugAssets UP-TO-DATE      
:app:generateDebugResValues UP-TO-DATE      
:app:generateDebugResources UP-TO-DATE      
:app:mergeDebugResources UP-TO-DATE      
:app:processDebugManifest UP-TO-DATE      
:app:processDebugResources UP-TO-DATE      
:app:generateDebugSources UP-TO-DATE      
:app:compileDebugJava UP-TO-DATE      
:app:compileTestDebugJava                                                                    
:app:processTestDebugResources UP-TO-DATE      
:app:testDebugClasses                 
:app:testDebug                                                             
:app:jacocoTestReport                                                           
               
BUILD SUCCESSFUL
               
Total time: 29.482 secs

The code coverage report will be stored in ./build/reports/jacoco/jacocoTestReport and might look alike the image below.


Some afterthoughts

  • The name of your Android application:
    • In our example, the name of the Android module is "app", ergo we include the sources from '../app/src/main/java'. If your Android app is called differently (e.g. FooBar), rename the paths (ALL OF THEM), accordingly, e.g. "../FooBar/src/main/java"
  • Product flavours
    • In the example we don't use product flavours, so the task depends on "testDebug" and you include classes from "../app/build/intermediates/classes/debug". If you would use product flavours in your android application (e.g. Local), Gradle will not find the "testDebug" task. You need to use the correct naming, e.g. testLocalDebug and include the correct classes, e.g. "../app/build/intermediates/classes/local/debug"


If you have any questions, please don't hesitate to ask. The code on GitHub has been updated accordingly, so please check it out.

maandag 14 juli 2014

MyRobolectricTestApp upgraded for Android Studio 0.8.1

With some delay I finally managed to upgrade the RobolectricTestApp to Android Studio 0.8.1. The project can be found here: https://github.com/kvandermast/my-robolectric-app.

I'll make some adjustments to the project in the next days:

  • Add Android Wear compatible notifications 
    • Already the case, as we were using the NotificationCompat classes
  • Add a small Android Wear app
If you have any questions, don't hesitate to ask.

Android and SSL HTTPClient

When you're developing an Android application you will inevitably - in your lifetime - need to access a REST Service (or alike). Android provides basic HTTP communication framework out-of-the-box, but you'll need to go the extra mile when you want to use HTTP/S communications.

The additional "S" in "HTTPS" is added to "Securely" transfer data between the client and the server (or between two pairs). I'm not going to explain the concepts behind HTTPS, you can read that on wikipedia, but an important note is that HTTPS certificates signed by known CA's are generally not an issue: these are already available in Trusted Certificate Authorities on your Android distribution.

If you would have a self-signed certificate (for development purposes) or a certificate signed by a not-so-well-known CA, you might run into some trouble.

I've divided the blogpost in two sections: the first one is what definitely not to do. The second section is how you should do it.

For the sake of completion, I found my inspiration from this blog: http://blog.crazybob.org/2010/02/android-trusting-ssl-certificates.html.

Not the way to go

I'm a big fan of StackOverflow, unfortunately, it sometimes overflows of half-founded-solutions or even bad solutions. One of the bad solutions is the solution below. 

    private class MySSLSocketFactory extends SSLSocketFactory {
        SSLContext sslContext = SSLContext.getInstance("TLS");

        public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, 
                                                              KeyManagementException, KeyStoreException, 
                                                              UnrecoverableKeyException {
            super(truststore);

            TrustManager tm = new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            };

            sslContext.init(null, new TrustManager[]{tm}, null);
        }

        @Override
        public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
            return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
        }

        @Override
        public Socket createSocket() throws IOException {
            return sslContext.getSocketFactory().createSocket();
        }
    }

In general, the above solution would allow ANY certificate to be accepted. Accepting any certificate could endanger data integrity, security, etc. You should always try to assess that the information is sent to the end-point you expect it to, and that the information is sent by the client you expect it from.


A better way to do it


The example below is using the BouncyCastle (BKS) algorithm.
import android.content.Context;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.KeyStore;


public class MyRestService {
    private final static String TAG = MyRestService.class.getSimpleName();

    private HttpClient httpClient = null;

    private Context context = null;

    public MyRestService(Context context) {
        this.context = context;

        this.httpClient = new MyHttpClient(this.context);
    }


    protected String post(URI api, Header[] headers, HttpEntity entity) /* ... throws  */ {
        String responseBody;

        try {
            HttpPost method = new HttpPost(api);
            method.setEntity(entity);
            method.setHeaders(headers);

            HttpResponse response = httpClient.execute(method);

            StatusLine status = response.getStatusLine();

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            response.getEntity().writeTo(out);
            out.close();
            responseBody = out.toString();

            if (status.getStatusCode() != HttpStatus.SC_OK) {
                if (status.getStatusCode() == HttpStatus.SC_FORBIDDEN) {
                    //process forbidden status code
                } else if (status.getStatusCode() == HttpStatus.SC_REQUEST_TIMEOUT) {
                    //process request time out code
                } else {
                    //process unrecoverable errors
                }
                //...
            }
        } catch (IOException e) {
            //Log the exception, e.g. Log.e(TAG, e.getMessage(), e); or Logger.e(TAG, e);

            //process the exception
        }

        return responseBody;
    }

    protected Context getContext() {
        return context;
    }


    private class MyHttpClient extends DefaultHttpClient {

        final Context context;

        public MyHttpClient(Context ctx) {
            this.context = ctx;
        }

        @Override
        protected ClientConnectionManager createClientConnectionManager() {
            SchemeRegistry registry = new SchemeRegistry();

            registry.register("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register("https", newSslSocketFactory(), 443));

            HttpParams httpParameters = getParams();
            HttpConnectionParams.setConnectionTimeout(httpParameters, 10000);
            HttpConnectionParams.setSoTimeout(httpParameters, 10000);
            HttpProtocolParams.setVersion(httpParameters, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setUseExpectContinue(httpParameters, false);

            return new ThreadSafeClientConnManager(httpParameters, registry);
        }

        private SSLSocketFactory newSslSocketFactory() {
            try {
                KeyStore trusted = KeyStore.getInstance("BKS");
                InputStream in = this.context.getResources().openRawResource(R.raw.mystore);
                try {
                    trusted.load(in, "MyStorePassword");
                } finally {
                    in.close();
                }
                return new SSLSocketFactory(trusted);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
    }
}

Let's break the example in some munchable parts:


  • We created  a MyRestService class which facilitates the execution of POST, GET, etc methods to your endpoint;
  • Instead of using the standard HttpClient, we created a MyHttpClient which extends from DefaultHttpClient.
    • When a connection is created, it will register our custom SSLSocketFactory class when accessing HTTPS resources;
    • When the SSLSocketFactory is created, we load the keystore that contains the certificate using a password (in this case MyStorePassword).
  • You can now use the MyHttpClient as you would with a normal DefaultHttpClient instance. Only if a connection is made your REST service, it will load the appropriate certificate.
How you can build the KeyStore, stored in /raw/mystore is explained in the article from Crazy Bob's blog: http://blog.crazybob.org/2010/02/android-trusting-ssl-certificates.html.

maandag 23 juni 2014

Logging and Proguard

When you develop an Android app, you tend - I hope - to add (debug) logging information in your code. Unfortunately, forgotten debug logging, is more than often a source of data leaks.

Proguard, which we discussed in this post, will give you the opportunity to strip these logging statements from your compiled code. "Cool!" (you'll undoubtably utter these words while dancing around your desk, praising the Java Gods).

Well, sort of "Cool!". If you use logging correctly many of your basic logging statements will be stripped from the compiled code. But Proguard has a downside, that it doesn't know what to do with String manipulations.

Imagine that we start with this example:

    private final static String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.inject(this);

        //you can start working with the annotated widgets after you injected them

        Log.d(TAG, "Log1 - onCreate completed");
        Logger.d(TAG, "Log2 - onCreate completed");

        Log.d(TAG, "Log3 - onCreate was called from class " + TAG);
        Logger.d(TAG, "Log4 - onCreate was called from class %s", TAG);
    }

You all know the "Log.x" statements (verbose, debug, info, warn, error and wtf) from the android.util.Log class. The "Logger.x" class is a custom class, call it a wrapper for the (in)famous android.util.Log class.

The result of the debugging statements might look like this:

MainActivity - Log1 - onCreate completed
MainActivity - Log2 - onCreate completed
MainActivity - Log3 - onCreate was called from class MainActivity
MainActivity - Log4 - onCreate was called from class MainActivity

If you would like to strip the logging statements from the compiled classes, you might have added these rules to the Proguard script:

...
# remove logging
-assumenosideeffects class android.util.Log {
    *;
}
...

We now extend the rules taking into account the Logging wrapper class (from the be.acuzio.mrta package) when compiling the classes:

...
# remove logging wrapper
-assumenosideeffects class be.acuzio.mrta.util.Logger {
    *;
}
...

What you'll end up with, is this compiled class (if you decompile it):
  
  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2130903064);
    ButterKnife.inject(this);
    new StringBuilder("Log3 - onCreate was called from class ").append(a);
    new java.lang.Object[1][0] = a;
  }

What you'll notice is that all the "trivial" logging statements are removed without any problem from your code:

...
        Log.d(TAG, "Log1 - onCreate completed");
        Logger.d(TAG, "Log2 - onCreate completed");
...

The problem arises with the logging statements that manipulate Strings:

...
        Log.d(TAG, "Log3 - onCreate was called from class " + TAG);
        Logger.d(TAG, "Log4 - onCreate was called from class %s", TAG);
...

As you can see, the Log.d statement has been removed, but the actual String is still readable in the decompiled code. This can still be considered as a data leak or security issue as it is now quite easy to figure out what the content the variable ("a" in the decompiled code) is meant to be (it's the name of the class, if you got a bit lost by now).

So what's so special about our custom Logger class? Let's have a look:

package be.acuzio.mrta.util;

import android.util.Log;

public final class Logger {
    /**
     * A wrapper for the Log.d function
     * * e.g. 
     * Logger.d(TAG, "Batman goes like %d, %d, %d, \"%s\"", 1, 2, 3, "Pow!");
     * //prints: Batman goes like 1, 2, 3, "Pow!"
     * Logger.d(TAG, "Holy Cows Batman!");
     * //prints: Holy Cows Batman!
     * 
     *
     * @param tag     The tag used in the logcat
     * @param message The message, which can be formatted according to String.format
     * @param args    The arguments, optional
     */
    public static void d(String tag, String message, Object... args) {
        Log.d(tag, String.format(message, args));
    }

    /**
     * A wrapper for the Log.i function
     * 
* e.g. * Logger.i(TAG, "Batman goes like %d, %d, %d, \"%s\"", 1, 2, 3, "Pow!"); * //prints: Batman goes like 1, 2, 3, "Pow!" * Logger.i(TAG, "Holy Cows Batman!"); * //prints: Holy Cows Batman! * * * @param tag The tag used in the logcat * @param message The message, which can be formatted according to String.format * @param args The arguments, optional */ public static void i(String tag, String message, Object... args) { Log.i(tag, String.format(message, args)); } /** * A wrapper for the Log.e function *
* e.g. * * try { * ... * throw new Exception("Holy Cow Batman!"); * } catch(Exception e) { * Logger.e("MyTag", e); * //prints: * // ... E/MyTag﹕ Holy Cows Batman! * // java.lang.Exception: Holy Cows Batman! * // at ... * // at ... * } * * * @param tag The tag used in the logcat * @param e The exception */ public static void e(String tag, Exception e) { Log.e(tag, e.getMessage(), e); } public static void e(String tag, String message, Exception e) { Log.e(tag, message, e); } }

Because we offer a fixed structure to Proguard, it finds it much easier to strip the logging statements from the compiled code. We just end up with this:

    ...
    new java.lang.Object[1][0] = a;
    ...

Instead of this:

    ...
    new StringBuilder("Log3 - onCreate was called from class ").append(a);
    ...

So that's already much better! Purist will say that it still indicates that you want to do something there. True. But that you can easily fix. We defined this function in our Logger class, which takes an array of Objects as final argument:
    public static void d(String tag, String message, Object... args) {
        ...
    }

If you don't want to use the array, just define a method per variable, like:

    public static void d(String tag, String message, Object arg1);
    public static void d(String tag, String message, Object arg1, Object arg2);
    public static void d(String tag, String message, Object arg1, Object arg2, Object arg3);    
    ... //you catch my drift

In the latter case you will remove all the logging statements that were defined by the rules in the Proguard file. Why? Because you always provide a fixed structure for printing information.

Hope this helps in improving your code, if you have any suggestions, please feel free to post them! The code is also available on GitHub.





Robolectric plugin upgraded to 0.11.x

It seems that I missed the upgrade for the Robolectric Gradle plug-in, but then again, they renamed the plugin from gradle-android-test-plugin to robolectric-gradle-plugin, an easy one to miss I would say.

In the previous post I mentioned that upgrading to Android Studio 0.6.x broke the Robolectric Gradle plugin with the nice fault on "java.lang.NoClassDefFoundError: com.android.builder.BuilderConstants". This seems to be fixed no, and the test reports are rendered again too.

Please upgrade your build script, I've updated the code on GitHub too.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.11.+'
        //new plugin
        classpath 'org.robolectric:robolectric-gradle-plugin:0.11.+'
    }
}

allprojects {
    repositories {
        mavenCentral()
    }
}

apply plugin: 'android'
apply plugin: 'robolectric' //NOTE the change!! No longer 'android-test'

android {
    compileSdkVersion 19
    buildToolsVersion '19.1.0'
    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName '1.0'
    }
    buildTypes {
        debug {
            runProguard true
            proguardFiles 'proguard-rules.txt'
        }
        release {
            runProguard false
            proguardFiles 'proguard-rules.txt'
        }
    }
    sourceSets {
        androidTest.setRoot('src/test')
    }
    lintOptions {
        abortOnError false
        disable 'InvalidPackage'
    }
    productFlavors {
    }
}

dependencies {
    // ================== LIBRARIES ======================
    compile fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
    compile 'com.android.support:appcompat-v7:+'
    //for more information on the Butterknife injection framework
    //visit https://github.com/JakeWharton/butterknife
    compile 'com.jakewharton:butterknife:5.1.0'
    // for more information on the Crouton framework,
    // visit https://github.com/keyboardsurfer/Crouton
    compile 'de.keyboardsurfer.android.widget:crouton:1.8.4'



    // ================== TESTING LIBRARIES ======================
    androidTestCompile 'junit:junit:4.10'
    androidTestCompile 'org.robolectric:robolectric:2.3'
    androidTestCompile 'com.squareup:fest-android:1.0.+'
    androidTestCompile 'org.bouncycastle:bcprov-jdk15on:1.50'
    androidTestCompile 'com.jakewharton:butterknife:5.1.0'
}

//NOTE! Used to be androidTest
robolectric {
    // configure the set of classes for JUnit tests
    include '**/*Test.class'
   
    // configure max heap size of the test JVM
    maxHeapSize = "2048m"
}

dinsdag 10 juni 2014

Robolectric and Android Studio 0.6.0

Quite recently a new update on Android Studio (version 0.6.0) was released. With the new release also came an upgrade of Gradle. Unfortunately the upgrade breaks your Robolectric testing if you use the gradle-android-test-plugin.

I did hope that the plugin team would release a quick-fix, but at the time of writing, this still isn't the case.

You can temporarily fix this by updating your build.gradle file with a forked plugin.

buildscript {
    repositories {
        mavenCentral()
        mavenLocal()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
        //required to get the plugin working again with gradle:0.11+ and AS0.6.0
        maven { url 'https://github.com/rockerhieu/mvn-repo/raw/master/' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.11.+'
        //classpath 'org.robolectric.gradle:gradle-android-test-plugin:0.10.+'
        //required to get the plugin working again on AS 0.6.0
        classpath 'org.robolectric.gradle:gradle-android-test-plugin:0.10.1-SNAPSHOT'
    }
}
...

See https://github.com/robolectric/robolectric-gradle-plugin/issues/31 for more information on the issue status.


I haven't updated the code on Github, because I hope that the plugin will be fixed quite soon.

Update

A fix has been released, read more in this post: http://raptordigital.blogspot.be/2014/06/robolectric-plugin-upgraded-to-011x.html.


dinsdag 3 juni 2014

MyRobolectricTestApp on Github

I just uploaded the source code of the MyRobolectric Test App on Github. This example contains


  • An example proguard file which aims at optimisation and obfuscation of your code.
  • An example on the extremely handy Butterknife framework.
  • An example on the refreshing Crouton framework.
  • Some basis examples on getting you started with Robolectric and Gradle.
I'll try to update the repository as much as possible, with new examples or updated settings when Android Studio and Gradle are updated.

Please note that this code is put at your disposal "as-is", and comes with no warranty or guarantee what-so-ever. You may use it at your discretion, but know that its purpose is purely illustrative.

The GitHub repository is located here: https://github.com/kvandermast/my-robolectric-app.


 

maandag 26 mei 2014

Robolectric, Gradle and Android - Test Driven Development Part 2

In a previous blogpost I spoke about Test Driven Android Development using Robolectric and a plug-in maintained by Novoda. It did the trick, but failed me short when I was using different productFlavors in my build.gradle script.

Fortunately, a nice team revived the plugin once created by Jake Wharton, which also resolves the annoying problem of the Windows fix you had to foresee.

Quite recently (May 20th) Robolectric also released a stable 2.3 version of their testing library, so it is time to update our build scripts.

Update June 2nd: a new version of Android Studio (Canary Build 0.5.9) has been released. I've updated the blogpost to reflect the updated Gradle version. For more information, please refer to the release notes.

Update June 3rd: the code has been pushed to Github, as described in this post.

build.gradle


buildscript {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
    dependencies {
        //Prior to AS 0.5.9: classpath 'com.android.tools.build:gradle:0.9.+'
        classpath 'com.android.tools.build:gradle:0.10.1'

        //Prior to AS 0.5.9: classpath 'org.robolectric.gradle:gradle-android-test-plugin:0.9.+'
        classpath 'org.robolectric.gradle:gradle-android-test-plugin:0.10.0'
        //previous plugin >> classpath 'com.novoda.gradle:robolectric-plugin:0.0.1-SNAPSHOT'
    }
}

allprojects {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
}

apply plugin: 'android'
apply plugin: 'android-test' //previously >> apply plugin: 'robolectric'

android {
    compileSdkVersion 19
    buildToolsVersion '19.1.0'

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        debug {
            runProguard true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }

    sourceSets {
        androidTest.setRoot('src/test') //note that this is androidTest instead of instrumentTest
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
    compile 'com.android.support:appcompat-v7:+'

    androidTestCompile 'junit:junit:4.10'
    //include the stable robolectric 2.3 library
    androidTestCompile 'org.robolectric:robolectric:2.3'
    androidTestCompile 'com.squareup:fest-android:1.0.+'

}

//only include files that are suffixed with Test.class and set the max heap size
androidTest {
    include '**/*Test.class'
    maxHeapSize = "2048m"
}

Your unit tests

With the new plugin, we also need to update our tests. If you run your tests with the standard RobolectricTestRunner, you might see this exception:
be.acuzio.mrta.test.DummyTest > testShouldFail FAILED
    java.lang.UnsupportedOperationException
Your logfiles will probably show you an exception that looks like this:
java.lang.UnsupportedOperationException: Robolectric does not support API level 19, sorry!
 at org.robolectric.SdkConfig.(SdkConfig.java:24)
 at org.robolectric.RobolectricTestRunner.pickSdkVersion(
        ...
You can fix this by adding the @Config(emulateSdk=x) to your class, eg:
package be.acuzio.mrta.test;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

@Config (emulateSdk = 18) //Robolectric support API level 18,17, 16, but not 19
@RunWith(RobolectricTestRunner.class)
public class DummyTest {
    @Before
    public void setup() {
        //do whatever is necessary before every test
    }

    @Test
    public void testWhoppingComplex() {
        Assert.assertTrue(Boolean.TRUE);
    }
}

Running the tests

Previously you started the tests using:
$ ./gradlew robolectric

Now, you can start the tests using this command:
$ ./gradlew test

The test results will still be stored in your build folder (e.g. /app/build/test-report/index.html).

donderdag 17 april 2014

Android and Proguard

I wanted to write a prologue here, on the motivation for this blog post. For some reason, I couldn't formulate it in a way that we would call "politically correct".

To give you some context: in the last months, I've been developing a high-profile app for a governmental institution, during which I needed to do quite some R&D on the do's-and-don'ts of mobile development. So I took my Android phone and connected it to my computer, and found to my surprise that some of the apps where spawning results from REST services to their logcat, which included confidential information.

Astonished I looked a bit further, how easy would it be to fetch the application from a non-rooted device. Quite easy so it seemed.
Would I be able access the "internals" of the applications? Yes I was! Some of the hybrid solutions (Cordova) were even easier to look at! I had direct access to Javascript and HTML source files. Wow! So it should be more difficult for Native Android applications, right? Wrong!

"WTF?!" was about the only thing that keept going through my mind.
How could we be so sloppy in developing Android applications!
Are we ignorant of the available tools?
Or are we just lazy?
Or are we not specialised enough?

Catching my drift here? So it was quite difficult to write an intro here.

This post will try to provide some insights in decompiling an APK file and securing your app using proguard.

PS/ This is not the Holy Grail of app security. It helps you in delivering higher quality apps. But it is still up to you to use your common sense. If it needs to be secure, or the information is sensitive: don't store it. Period. That's my advice.

An insight on the APK

When you install an Android application from Google Play, you download an APK file. This APK contains the assets (images, layouts, etc) and code (DEX-file) required for your application to run on your Android Smartphone.

An APK is "no more" than a ZIP file constructed using your favourite IDE (e.g. Android Studio & Gradle) which respects a certain structure. Some believe that you need a Jailbroken or Rooted device to download the APK, but that's actually not the case. The Android SDK is shipped with the required tools to download the APK from your mobile device.

One does not simply download an APK

Actually, you do.
  • First, connect your smart device to your computer, and make sure that you've enabled the developer option on the smartphone (no, that's not rooting)
  • Once connected, list the installed packages on your phone
  • Look for the package you need and check the install location
  • "Pull" the APK from the install location

Show me the packages!

Start a Terminal window and go to the "platform-tools" folder (if it's not in your path, that is). Connect to your smartphone using the "adb shell" command.

$ adb shell pm list packages

The result will be a list of all the installed packages on your smart device, e.g:

package:com.imdb.mobile
...
package:com.google.android.apps.books
package:com.google.android.videos
package:com.lge.update
package:be.acuzio.foobar
package:com.google.android.inputmethod.pinyin
...
package:com.google.android.syncadapters.contacts
package:com.google.android.backup


O Package, where art thou?
Let's say that we are interested in the package "be.acuzio.foobar" and we want to know the path:

$ adb shell pm path be.acuzio.foobar
package:/data/app/be.acuzio.foobar-1.apk

Excellent! Next, let's pull the APK from the location /data/app/be.acuzio.foobar-1.apk


Download the APK
$ adb pull /data/app/be.acuzio.foobar-1.apk
4551 KB/s (6484838 bytes in 1.391s)

$ ls -alh
total 16920
drwxr-xr-x   9 vandekr  staff   306B Feb 10 20:05 .
drwxr-x---@ 14 vandekr  staff   476B Jan 21 10:24 ..
...
-rw-r--r--   1 vandekr  staff   6.2M Feb 10 20:05 be.acuzio.foobar-1.apk
...

Eh, voila, you are now the proud owner of the zip APK file.

Mastering the ZIP

Unzipping the APK is one thing you can do to check the internals of the app. If the app was developed as a native app, you can use the dex2jar and JD-GUI tools to have a look at the code.

Dex-de-compilation
You can use the dex2jar tool to extract the DEX File from the APK and convert it to a JAR (which we need for JD-GUI)

$ d2j-dex2jar.sh -f -o ~/Desktop/output_jar.jar ~/Desktop/be.acuzio.foobar-1.apk

Look at the code
Start the JD-GUI tool and open the created "output_jar.jar" from your desktop.



Now that looks like readable code to me. Let's have a look at the actual code.

package be.acuzio.foobar;

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;


public class MainActivity extends ActionBarActivity {
    private final static String TAG = MainActivity.class.getSimpleName();

    private Button btnLogin = null;
    private EditText etUsername = null;
    private EditText etPassword = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate was called");

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.etPassword = (EditText) this.findViewById(R.id.et_password);
        this.etUsername = (EditText) this.findViewById(R.id.et_username);
        this.btnLogin = (Button) this.findViewById(R.id.btn_login);

        this.btnLogin.setOnClickListener(new LoginOnClickListener());
    }


    class LoginOnClickListener implements View.OnClickListener {

        @Override
        public void onClick(View v) {
            Log.d(TAG, "onClick was triggered for Login");

            String username = etUsername.getText().toString();
            String password = etPassword.getText().toString();

            Log.d(TAG, String.format("  > Username: %1$s", username));
            Log.d(TAG, String.format("  > hashing password: %1$s", password));
        }
    }

}


Even without much effort, you can read and understand the decompiled code. It's not even that much different from the actual code.

So let us talk  about what we can do to make it more difficult to decompile and understand.

Note: it's a way to optimize your code and avoid data leaks via logging. By no means what so ever is it an encryption of values. A stubborn developer with enough time on his/her hands will still be able to figure your code, even when it is obfuscated.


Proguard

Proguard was chosen by Google/Android as the preferred framework for code obfuscation and mimification. Proguard is not the only framework available for obfuscation: a more complex (and commercial) version is DexGuard.

When you develop an Android application, you might want to consider the following:

  • During development and (unit) testing, you added logging to your application. This logging might have confidential information: data consumed from remote services, usernames, contents of arrays, etc. This data dumping is the primary source of information leaks! You just need to connect your Android device to your computer and look at the rendered logcat to see what is spawned by the different applications (PS/ we assume the usage of the standard Android Logging);
  • You use libraries to facilitate your development. Sometimes you just need a small portions (e.g.  Bouncy Castle encryption, Apache Commons libs, Custom JAR files, etc) of the provided libraries.
  • During development, you might have URL's, passwords, hashes etc stored clear text in your application to secure the communication between the app and the remote service.
  • You've worked very hard (for a client), and you're not snappy on the idea of sharing your code with the world.

Although not perfect, Proguard will help you in the above points, resulting in an app that is:
  • Not leaking information via Logging and cleaning up your code;
  • Packaging the code you use and removing the libraries or classes that are solely there as dead-weight;
  • Obfuscate your code, making it harder to decompile and understand what you did

A Proguard example

Proguard is not the easiest framework to master. So a generic proguard file is already provided by your Android SDK. It might resemble a set-up like this one:

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html

-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose


# Keep a fixed source file attribute and all line number tables to get line
# numbers in the stack traces.
# You can comment this out if you're not interested in stack traces.

-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable

# keep annotations
-keepattributes *Annotation*

# keep the licencing service classes from Google
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
    native <methods>;
}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

#############################################################################
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version.  We know about them, and they are safe.
-dontwarn android.support.**

# an example if you don't want to be warned about missing libraries
# -dontwarn javax.naming.**

#############################################################################
# remove logging, note that this removes ALL logging, including the 
# Log.e statements
-assumenosideeffects class android.util.Log {
    *;
}

# an example on how to keep an entire package
# -keep class com.google.zxing.**

#############################################################################
# repackage and optimize
-repackageclasses ""
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify

Update the Gradle build file

Having a proguard file is just one thing, you need to activate it during your build. I do suggest that you add a proguard file to your debug assembly: because of its intrusive nature, you really need to test your app after the obfuscation.

apply plugin: 'android'

android {
    compileSdkVersion 19
    buildToolsVersion "19.0.3"

    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        debug {
            runProguard true
            proguardFiles 'proguard-rules.txt'
        }
    }
}

dependencies {
    compile 'com.android.support:appcompat-v7:19.+'
    compile fileTree(dir: 'libs', include: ['*.jar'])
}


If you deploy the app to a device or run the assembleDebug command, a new APK will be built using the proguard settings.

Assembling a debug APK using gradle is a simple as running:

$ ./gradlew assembleDebug


The .apk will be stored in the "build/apk" folder of your app project.


Have a look now


Now that already looks a bit better!

  • The logging is removed from the class;
  • The names are obfuscated, but the methods and classes that are required by the AndroidManifest or the Android ecosystem are kept (e.g. onCreate). 
  • The packages are flattened and renamed, so happy hunting if you have a complex project!


donderdag 10 april 2014

Change your MySQL datafolder

When you use MySQL, it defaults its data files to /var/lib/mysql. In case you have a separate data disk mounted (e.g. /data). You might want to change the data folder used by MySQL. You can change the folder if you follow these steps:

Stop your mysql instance:

$ sudo service mysql stop

Create the datadir folder, eg. /data/mysql/data

$ mkdir -p /data/mysql/data

Change the owner of the data folder

$ sudo chown -R mysql:mysql /data/mysql/data

Copy the existing data from the /var/lib/mysql folder to /data/mysql/data

$ sudo cp -a /var/lib/mysql/* /data/mysql/data/

Note: this would copy a directory and retain the permissions of the copied folder

$ sudo cp -rp /var/lib/mysql/* /data/mysql/data/


Edit the MySQL config file (my.cnf) and update the datadir configuration

$ sudo vi /etc/mysql/my.cnf
 
...
socket = /var/run/mysqld/mysqld.sock
port = 3306
basedir = /usr
# datadir = /var/lib/mysql
datadir = /data/mysql/data
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
...

AppArmor is not keen on changes. So make sure that it allows you to change the datadir of MySQL

$ sudo vi /etc/apparmor.d/usr.sbin.mysqld
 
# vim:syntax=apparmor
 
# Last Modified: Tue Jun 19 17:37:30 2007
#include 
 
/usr/sbin/mysqld {
...
 
/var/lib/mysql/ r,
/var/lib/mysql/** rwk,
/var/log/mysql/ r,
/var/log/mysql/* rw,
...
/run/mysqld/mysqld.sock w,
/data/mysql/data/ r,
/data/mysql/data/** rwk,
 
...
}

Add the configuration of the datadir at the bottom (in our case /data/mysql/data/)


Restart both apparmor and mysql

$ sudo service apparmor restart
* Reloading AppArmor profiles Skipping profile in /etc/apparmor.d/disable: usr.sbin.rsyslogd
[ OK ]
$ sudo service mysql restart
mysql stop/waiting
mysql start/running, process 1532

And now you are done.

vrijdag 4 april 2014

A 10-step guide in setting up Push Notifications in XCode5/iOS7

Prerequisites

We are assuming that:

  • You have XCode5 installed; 
  • At least know your way around XCode; 
  • Have a developer account at Apple.

Introduction

This document describes the different steps to take when setting up an iOS app with Remote Notifications. The detailed information about sending Remote Notifications can be found here:

https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction.html.

The summary:

  • You need to generate a certificate that is tightly linked with your app. With the certificate the APNS servers know where to send the messages to, and if you are allowed to do so; 
  • The certificates open an SSL connection over which a payload is sent. 
  • The payload contains a JSON encoded message, which at least should have a body like:
    {
        aps =     {
            alert = "FooBarPushDemo test!";
        };
    }
    
  • The APS key in the payload is mandatory, other keys can be added (e.g. foobar to store additional information)
    {
        aps =     {
            alert = "FooBarPushDemo test!";
        };
        foobar =     {
            some = "data";
        };
    
    }

Step 1: Generate a Certificate Signing Request (CSR)

  • Open the Keychain Access application on your Mac; 
  • Open the Keychain Access menu, and choose Certificate Assistant > Request a Certificate from a Certificate Authority 


  • Enter the following information: 
    • User E-mail Address: your email address;
    • Common name: the name of the application, e.g. FooBarPushDemo
    • Select the option to save the CSR to disk (save it to a specific folder as you will need to store more files there, e.g. ~/Desktop/FooBarPushDemo and name it FooBarPushDemo.certSigningRequest 
  • Close the window after completion, but stay in the Keychain Access application

Step 2: export the private key

  • In the Keychain Access application, select the newly created Private Key  
  • Right-click (or ctrl-click) it and select Export “FooBarPushDemo” 
  • Name the file FooBarPushDemoKey.p12 and save it to the same location as your CSR file; 
  • You will be prompted for a password (passphrase) to protect the exported key. Choose “foobar” as passphrase 
  • Next you will be prompted for your Mac password, and continue 
  • Save the exported key in the same folder as the CSR.


Step 3: create a new iOS application

  • Create a new XCode iOS project via File > New > Project 
  • From the iOS Application menu, select Single View Application and click ‘Next’ 

  • Enter the following information for: 
    • Product Name: FooBarPushDemo 
    • Organisation Name: your company name
    • Company identifier: be.acuzio.ios.demo
    • Class Prefix: FooBar 
    • Devices: iPhone 

  • The bundle identifier in our example now is be.acuzio.ios.demo.FooBarPushDemo. This bundle identifier will be need in the next steps when you create an App ID. 
  • Press Next to choose the location where to store your project and create the project

Step 4: set-up an AppID

  • Log in to the iOS Development Central (https://developer.apple.com/devcenter/ios/index.action)
  • Select Certificates, Identifiers & Profiles from the iOS Developer Program menu 
  • Select Identifiers from the iOS Apps menu 
  • In the App ID’s option, add a new identifer with this information: 
    • App ID description: FooBarPushDemo 
    • Select “Explicit App ID”, which is a requirement when you are using the Apple Push Notifications Service (APNS) 
    • Enter the bundle identifier: be.acuzio.ios.demo.FooBarPushDemo 
    • Check the box next to Push Notifications in the App Services block 
  • Click Continue and confirm by hitting Submit 
  • Click Done to return to the App IDs overview, you should now see the newly created AppID in the list

Step 5: set-up the Push Notification SSL Certificate


  • Select the newly created AppID from the list, it should look more or less like this: 

  • The option “push notifications” in development is in orange, which means it’s not set-up yet. Click on Edit and scroll down to the Push Notifications Section 
  • Click on “Create Certificate” for the “Development SSL Certificate” group 

  • You will be informed that you will need a CSR file (see Step 1: Generate a Certificate Signing Request (CSR)). 
  • Click “Continue” 
  • Upload the CSR file you created before and click on “Generate” 

  • Download the certificate and move it to the same folder e.g. ~/Desktop/FooBarPushDemo, make sure it is named “aps_development.cer” 

  • Add the certificate by double clicking on the .cer file. It will add a public key to your private key

Step 6: create a PEM file

  • Open a Terminal window and change to the directory where you stored the CSR, P12 key and Development certificate

$ cd ~/Desktop/FooBarPushDemo  

  • Convert the .cer file into a .pem file

$ openssl x509 -in aps_development.cer -inform der -out FooBarPushDemoCert.pem

  • Convert the .p12 key into a .pem file

$ openssl pkcs12 -nocerts -in FooBarPushDemoKey.p12 -out FooBarPushDemoKey.pem
Enter Import Password:
MAC verified OK
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

  • The first time you will be prompted for the Passphrase, which in this example is “foobar”. 
  • The second and third password will be to protect your newly created key. I chose the same “foobar” password, but you can choose a different one. This password will actually be required to set-up the SSL connection to the Apple Push Notification Service servers. 
  • Concat the 2 .pem files into a combined .pem file

$ cat FooBarPushDemoCert.pem FooBarPushDemoKey.pem > FooBarPushDemo.pem

  • Now check if a connection can be set-up to the servers:

$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert FooBarPushDemoCert.pem -key FooBarPushDemoKey.pem

  • If all goes well, you should see a lot of information on the certificate chain the SSL handshake etc. You will not see you prompt, because the service is actually waiting for you to send a payload over the SSL connection. Just enter “Hello” and enter. The connection will be closed. You’ll see something like this:

…
---
No client certificate CA names sent
---
SSL handshake has read 2731 bytes and written 2189 bytes
---
New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : AES256-SHA
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: BFED02C173464345CE8D6B59CB93D5B383260560ACB87A81C441A8145E646CCBDE5F01BFC545946A147824D2222CF647
    Key-Arg   : None
    Start Time: 1396596337
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---
hello
closed
$

  • In development you will be using the sandbox APNs, in production you will need the other one.

Step 7: create a provisioning file

  • Log in to the iOS developer portal, as you did when creating the App ID
  • Navigate to the “Certificates, Identifiers & Profiles” menu and go to the provisioning files
  • Click on the add button to add a new provisioning profile 
  • Select the iOS App Development as type of provisioning file and click “continue” 

  • Select the FooBarPushDemo App ID from the drop-down list and continue 


  • Select the certificates you wish to include in this provisioning profile. To use this profile to install an app, the certificate the app was signed with must be included 
  • Select the devices on which you are going to test. You will need a physical device to test on, as push notifications will not work on your simulator 
  • Enter “FooBarPushDemo Provisioning File” as name and click “Generate” 
  • When the generation finishes, click on “Download” and save the file next to the other certificates etc. 
  • To add the provisioning file to XCode, drag the “.mobileprovision” file and drop it on the XCode icon

Step 8: Making your iOS app Push-Notifications enabled

  • Open the application delegate (in our case FooBarAppDelegate.m” - When the application is launched, it needs to register with APNS in order to be able to receive Remote Notification (Push Messages). - You’ll need to add the registration code to your “didFinishLaunchingWithOptions” method

@implementation FooBarAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:
        (UIRemoteNotificationTypeAlert |
         UIRemoteNotificationTypeBadge |
         UIRemoteNotificationTypeSound)];
    
    
    return YES;
}

  • This registration will show the user, when the application is first launched, a message that this app uses remote notifications. 
  • A device token is used to send messages to a specific device. This can be accomplished by adding these lines of code:

 
- (void) application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSLog(@" Registered device for remote notifications: %@", 
    [deviceToken.description stringByReplacingOccurrencesOfString:@" " withString:@""]);
}

- (void) application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@" Failed to register for remote notifications: %@", error);
}

  • Start the application on a physical device, you log should print out a message resembling this (the device token is 64 characters in length):

2014-04-04 10:39:26.764 FooBarPushDemo[2369:60b]  Registered device for remote notifications: 

  • We now know when the application is registered, of failed to register. The AppDelegate also captures the remote notification itself. In order to process the information, implement the following method:

- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@" Received remote notifcation: %@", userInfo);
}

Step 9: change build settings

  • Make sure that the correct “team” is selected, it should match the signing identity you used for signing your certificate and provisioning file 

  • Enable the “Remote notifications” background mode: 

  • Open the build settings and look for the entry “provisioning profile” 

  • Change it to match the added Provisioning Profile

Step 10: sending push notifications


  • When you want to send a Push notification to a device, you need to open an SSL connection to the APNs servers using the generated certificate. 
  • When the connection is established, you can transmit a binary payload. 
  • If no feedback is returned, it means that the message was sent successfully! 
  • An example PHP script:

<?php

$device_tokens = array("e2f8cd253f2…d5b78ba7055e44f6…d1a77fddf350");

$passphrase = "foobar";

$message = "FooBarPushDemo test!";

$cert = dirname(__FILE__) . '/FooBarPushDemo.pem';

///////////////////////////////////////////////////////////////////////////////
$ctx = stream_context_create();

stream_context_set_option($ctx, 'ssl', 'local_cert', $cert);
stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);


for($i = 0; $i < count($device_tokens); $i++) {
    $device_token = mb_convert_encoding($device_tokens[$i], 'utf-8');


    echo "Device token : $device_token, length <" . strlen($device_token) . ">" . PHP_EOL;

    // Open a connection to the APNS server
    $fp = stream_socket_client(
        'ssl://gateway.sandbox.push.apple.com:2195', $err,
        $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);

    stream_set_blocking($fp, 0);
    stream_set_write_buffer($fp, 0);

    if (!$fp)
        exit("Failed to connect: $err $errstr" . PHP_EOL);

    echo 'Connected to APNS' . PHP_EOL;

    // Create the payload body
    $body['aps'] = array(
        'alert' => $message
    );

    // Encode the payload as JSON
    $payload = mb_convert_encoding(stripslashes(json_encode($body)), 'utf-8');

    // Build the binary notification

    if(strlen($device_token) != 64) {
        die("Device token has invalid length");
    }

    if(strlen($payload) < 10) {
        die('Invalid payload size');
    }

    $msg = chr(0)
            .pack('n', 32) //token length
            .pack('H*', $device_token) //token
            .pack('n', strlen($payload)) //length of payload
            .$payload;


    // Send it to the server
    $result = fwrite($fp, $msg /*, strlen($msg) */);

    if (!$result)
        echo 'Message not delivered' . PHP_EOL;
    else {
        echo 'Message successfully delivered, result:' . $result . PHP_EOL;

        echo "Sent {" . strlen($msg) . "} to server, received {" . $result . "}" . PHP_EOL;

        $response = fread($fp, 6);
        var_dump($response);

        $messageResult = unpack('Ccommand/CstatusCode/Nidentifier', $response);
        var_dump($messageResult);

    }

    //connect to the APNS feedback servers
    //make sure you're using the right dev/production server & cert combo!
    
    $apns = stream_socket_client('ssl://feedback.sandbox.push.apple.com:2196', $errcode, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
    if(!$apns) {
        echo "ERROR $errcode: $errstr\n";
        return;
    }

    echo "Feedback returned: " . PHP_EOL;

    $feedback_tokens = array();
    //and read the data on the connection:
    while(!feof($apns)) {
        $data = fread($apns, 38);
        var_dump($data);
        if(strlen($data)) {
            $feedback_tokens[] = unpack("N1timestamp/n1length/H*devtoken", $data);
        }
    }

    var_dump($feedback_tokens);

    fclose($apns);
    fclose($fp);
}

donderdag 27 februari 2014

Robolectric plugin - Fix the plugin-flaw on a Windows Environment

In the previous blogpost we worked with the Robolectric plugin (developed by Novoda.com) for starting our Robolectric tests from Gradle. This neat little plugin unfortunealty has a flaw which prohibits it from working correctly if you are developing on a Windows environment (or you switch to Mac or Linux, just an idea...).

The problem with the current plugin is that it does not escape the File.Seperator correctly, and although a merge-request is pending on their Git repository, they haven't fixed this to a new stable or snapshot release.

The code of the current (0.0.1 - SNAPSHOT) currently states:
     def buildDir = dir.getAbsolutePath().split(File.separator)
Where it should be
      def buildDir = dir.getAbsolutePath().split(Pattern.quote(File.separator))

Fortuneatly enough, the source code is publicly available as a Groovy based plugin. For ease-of-use (and my lack of understanding on how to compile a plugin :) ), I've posted my Gradle configuration below.

Next to having your normal buildscript and android configuration, you will need to :
  • Import two additional classes, being Callable and Pattern
  • Define the plugin: robolectric
  • Paste-in the code that starts with "class robolectric implements Plugin


Note
With the release of Android Studio 0.5.0, some upgrading of your Gradle build file is required (using the Gradle plugin version 0.9.+). In the dependencies change the instrumentTestCompile command to androidTestCompile. If you'll have exceptions during the Project Gradle Sync, make sure to restart your IDE and invalidate the caches (File > Invalidate Caches/Restart). I've update the build file in the provided examples.

The code

//--- import additional classes ---
import java.util.concurrent.Callable
import java.util.regex.Pattern
//--- end import ---

buildscript {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.9.+'
        classpath 'com.novoda.gradle:robolectric-plugin:0.0.1-SNAPSHOT'
    }
}

apply plugin: 'android'
//--- use the new plugin robolectric, which is the classname of the plugin
apply plugin: robolectric

android {
    compileSdkVersion 19
    buildToolsVersion '19.0.1'

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }

    sourceSets {
        //the tests are still stored in /src/test
        instrumentTest.setRoot('src/test')
    }

    buildTypes {
    }
}


dependencies {
    compile 'com.android.support:appcompat-v7:+'
    
    androidTestCompile 'org.robolectric:robolectric:2.+'
    androidTestCompile 'junit:junit:4.+'
}

//ignore classes that do not have the extension Test in their classname
tasks.withType(Test) {
    scanForTestClasses = false
    include "**/*Test.class"
}
/**
 * -----------------------------------------------------------
 * ROBOLECTRIC PLUGIN CLAS
 * -----------------------------------------------------------
 */
class robolectric implements Plugin<Project> {

    public static final String ANDROID_PLUGIN_NAME = "android";
    public static final String ANDROID_LIBRARY_PLUGIN_NAME = "android-library";

    public static final String COMPILE_CONFIGURATION_NAME = "compile";
    public static final String TEST_COMPILE_CONFIGURATION_NAME = "robolectricTestCompile";
    public static final String RUNTIME_CONFIGURATION_NAME = "runtime";
    public static final String TEST_RUNTIME_CONFIGURATION_NAME = "robolectricTestRuntime";

    public static final String ROBOLECTRIC_SOURCE_SET_NAME = "robolectric";
    public static final String ROBOLECTRIC_CONFIGURATION_NAME = "robolectric";
    public static final String ROBOLECTRIC_TASK_NAME = "robolectric";

    void apply(Project project) {
        project.getPlugins().apply(JavaBasePlugin.class);
        ensureValidProject(project);

        JavaPluginConvention javaConvention = project.getConvention().getPlugin(JavaPluginConvention.class);
        configureConfigurations(project);
        configureSourceSets(javaConvention);

        configureTest(project, javaConvention);

        project.afterEvaluate {
            configureAndroidDependency(project, javaConvention)
        }
    }

    def configureAndroidDependency(Project project, JavaPluginConvention pluginConvention) {
        SourceSet robolectric = pluginConvention.getSourceSets().findByName(ROBOLECTRIC_SOURCE_SET_NAME);

        (getAndroidPlugin(project)).mainSourceSet.java.srcDirs.each { dir ->
            //Fault: def buildDir = dir.getAbsolutePath().split(File.separator)
            def buildDir = dir.getAbsolutePath().split(Pattern.quote(File.separator))
            buildDir = (buildDir[0..(buildDir.length - 4)] + ['build', 'classes', 'debug']).join(File.separator)
            robolectric.compileClasspath += project.files(buildDir)
            robolectric.runtimeClasspath += project.files(buildDir)
        }

        getAndroidPlugin(project).variantDataList.each {
            it.variantDependency.getJarDependencies().each {
                robolectric.compileClasspath += project.files(it.jarFile)
                robolectric.runtimeClasspath += project.files(it.jarFile)
            }
        }

        // AAR files
        getAndroidPlugin(project).prepareTaskMap.each {
            robolectric.compileClasspath += project.fileTree(dir: it.value.explodedDir, include: '*.jar')
            robolectric.runtimeClasspath += project.fileTree(dir: it.value.explodedDir, include: '*.jar')
        }

        // Default Android jar
        getAndroidPlugin(project).getRuntimeJarList().each {
            robolectric.compileClasspath += project.files(it)
            robolectric.runtimeClasspath += project.files(it)
        }

        robolectric.runtimeClasspath = robolectric.runtimeClasspath.filter {
            it
            true
        }
    }

    private void ensureValidProject(Project project) {
        boolean isAndroidProject = project.getPlugins().hasPlugin(ANDROID_PLUGIN_NAME);
        boolean isAndroidLibProject = project.getPlugins().hasPlugin(ANDROID_LIBRARY_PLUGIN_NAME);
        if (!(isAndroidLibProject | isAndroidProject)) {
            throw new RuntimeException("Not a valid Android project");
        }
    }

    void configureConfigurations(Project project) {
        ConfigurationContainer configurations = project.getConfigurations();
        Configuration compileConfiguration = configurations.getByName(COMPILE_CONFIGURATION_NAME);
        Configuration robolectric = configurations.create(ROBOLECTRIC_CONFIGURATION_NAME);
        robolectric.extendsFrom(compileConfiguration);
    }

    private void configureSourceSets(final JavaPluginConvention pluginConvention) {
        final Project project = pluginConvention.getProject();

        SourceSet robolectric = pluginConvention.getSourceSets().create(ROBOLECTRIC_SOURCE_SET_NAME);

        robolectric.java.srcDir project.file('src/test/java')
        robolectric.compileClasspath += project.configurations.robolectric
        robolectric.runtimeClasspath += robolectric.compileClasspath
    }

    private void configureTest(final Project project, final JavaPluginConvention pluginConvention) {
        project.getTasks().withType(Test.class, new Action<Test>() {
            public void execute(final Test test) {
                test.workingDir 'src/main'
                test.getConventionMapping().map("testClassesDir", new Callable<Object>() {
                    public Object call() throws Exception {
                        return pluginConvention.getSourceSets().getByName("robolectric").getOutput().getClassesDir();
                    }
                });

                test.getConventionMapping().map("classpath", new Callable<Object>() {
                    public Object call() throws Exception {
                        return pluginConvention.getSourceSets().getByName("robolectric").getRuntimeClasspath();
                    }
                });

                test.getConventionMapping().map("testSrcDirs", new Callable<Object>() {
                    public Object call() throws Exception {
                        return new ArrayList<File>(pluginConvention.getSourceSets().getByName("robolectric").getJava().getSrcDirs());
                    }
                });
            }
        });

        Test test = project.getTasks().create(ROBOLECTRIC_TASK_NAME, Test.class);
        project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(test);
        test.setDescription("Runs the unit tests using robolectric.");
        test.setGroup(JavaBasePlugin.VERIFICATION_GROUP);

        test.dependsOn(project.getTasks().findByName('robolectricClasses'))
        test.dependsOn(project.getTasks().findByName('assemble'))
    }

    private Plugin getAndroidPlugin(Project project) {
        if (project.getPlugins().hasPlugin(ANDROID_LIBRARY_PLUGIN_NAME)) {
            return  project.getPlugins().findPlugin(ANDROID_LIBRARY_PLUGIN_NAME);
        }
        return project.getPlugins().findPlugin(ANDROID_PLUGIN_NAME);
    }

}
//--- END CLASS DEFINITION --

woensdag 12 februari 2014

Robolectric - BroadcastReceiver and IntentService testing

One of the added values on mobile apps is the ability to send Push notifications from a back-end application to a specific device (i.e. Push message) or multiple devices (i.e. Broadcast message).

When implementing a form of Push or Broadcast messaging, you generally require two components: a BroadcastReceiver and an IntentService, which we kindly submit to rigorous unit testing.

Testing the BroadcastReceiver

In this example, we just created a BroadcastReceiver that dispatches the intent to the IntentService for further processing.

MyBroadcastReceiver

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import be.acuzio.mrta.service.MyBroadcastIntentService;


public class MyBroadcastReceiver extends BroadcastReceiver {
    private final static String TAG = MyBroadcastReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "onReceive was triggered");

        Intent service = new Intent(context, MyBroadcastIntentService.class);
        service.putExtra("ACTION", intent.getStringExtra("PERFORM"));

        //start the service which needs to handle the intent
        context.startService(service);
    }
}

In the application manifest we defined the receiver to listen to incoming GCM messages, but you might even have BroadcastReceivers listening to local broadcast intents.

ApplicationManifest

      
        ...
        <receiver android:name=".receiver.MyBroadcastReceiver" 
                     android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="be.acuzio.mrta" />
            </intent-filter>
        </receiver>
        ...


MyBroadcastReceiverTest

  
package be.acuzio.mrta.test.receiver;

import android.content.BroadcastReceiver;
import android.content.Intent;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowLog;

import java.util.List;

import be.acuzio.mrta.receiver.MyBroadcastReceiver;
import be.acuzio.mrta.service.MyBroadcastIntentService;

/**
 * Created by vandekr on 12/02/14.
 */

@RunWith(RobolectricTestRunner.class)
public class MyBroadcastReceiverTest {
    static {
        // redirect the Log.x output to stdout. Stdout will be recorded in the test result report
        ShadowLog.stream = System.out;
    }

    @Before
    public void setup() {}

    /**
     * Let's first test if the BroadcastReceiver, which was defined in the manifest, is correctly
     * load in our tests
     */
    @Test
    public void testBroadcastReceiverRegistered() {
        List<ShadowApplication.Wrapper> registeredReceivers = Robolectric.getShadowApplication().getRegisteredReceivers();

        Assert.assertFalse(registeredReceivers.isEmpty());

        boolean receiverFound = false;
        for (ShadowApplication.Wrapper wrapper : registeredReceivers) {
            if (!receiverFound)
                receiverFound = MyBroadcastReceiver.class.getSimpleName().equals(
                                         wrapper.broadcastReceiver.getClass().getSimpleName());
        }

        Assert.assertTrue(receiverFound); //will be false if not found
    }

    @Test
    public void testIntentHandling() {
    /** TEST 1
         ----------
         We defined the Broadcast receiver with a certain action, so we should check if we have
         receivers listening to the defined action
         */
        Intent intent = new Intent("com.google.android.c2dm.intent.RECEIVE");

        ShadowApplication shadowApplication = Robolectric.getShadowApplication();
        Assert.assertTrue(shadowApplication.hasReceiverForIntent(intent));

        /**
         * TEST 2
         * ----------
         * Lets be sure that we only have a single receiver assigned for this intent
         */
        List<broadcastreceiver> receiversForIntent = shadowApplication.getReceiversForIntent(intent);

        Assert.assertEquals("Expected one broadcast receiver", 1, receiversForIntent.size());

        /**
         * TEST 3
         * ----------
         * Fetch the Broadcast receiver and cast it to the correct class.
         * Next call the "onReceive" method and check if the MyBroadcastIntentService was started
         */
        MyBroadcastReceiver receiver = (MyBroadcastReceiver) receiversForIntent.get(0);
        receiver.onReceive(Robolectric.getShadowApplication().getApplicationContext(), intent);

        Intent serviceIntent = Robolectric.getShadowApplication().peekNextStartedService();
        Assert.assertEquals("Expected the MyBroadcast service to be invoked",
                MyBroadcastIntentService.class.getCanonicalName(),
                serviceIntent.getComponent().getClassName());

    }
}


Testing the IntentService

Testing IntentServices is a somewhat more complicated matter. The current implementation of Robolectric does not invoke the "onHandleIntent", ergo nothing happens. A way to fix this, is to stub or mock your IntentService class in your tests and override the "onHandleIntent(Intent intent)" method to broaden its scope from protected to public.

Because of the introduction of Mock objects in our project, we need to update the build.gradle file to exclude all files that are not named "Test" from the TestRunner. Add  this snippet to the bottom of your build.gradle file.

// prevent the "superClassName is empty" error for classes not annotated as tests
tasks.withType(Test) {
    scanForTestClasses = false
    include "**/*Test.class" // whatever Ant pattern matches your test class files
}

As mentioned in the comment, if you do not add this snippet, you will get these nasty "superClassName is empty" exceptions, which can give you a true run for your money.

MyBroadcastIntentService

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import be.acuzio.mrta.MainActivity;
import be.acuzio.mrta.R;

public class MyBroadcastIntentService extends IntentService {
    private final static String TAG = MyBroadcastIntentService.class.getSimpleName();
    public final static int NOTIFICATION_ID = 335446435;
    public final static String NOTIFICATION_TAG = "BNTAG_ACTION";

    public MyBroadcastIntentService() {
        super(MyBroadcastIntentService.class.getSimpleName());
        Log.d(TAG, "Creating new instance of MyBroadcastIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent was called");

        Bundle extras = intent.getExtras();

        if (extras != null && !extras.isEmpty()) {  // has effect of unparcelling Bundle
            Log.d(TAG, "Extras were found");

            String action = intent.getStringExtra("ACTION");

            this.sendNotification(action);
        }
    }

    private void sendNotification(String action) {
        Log.d(TAG, "Sending notification");

        NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);

        //set-up the action for authorizing the action
        Intent intent = new Intent(getApplicationContext(), MainActivity.class);

        PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);


        NotificationCompat.Builder builder =
                new NotificationCompat.Builder(this)
                        .setSmallIcon(R.drawable.ic_launcher)
                        .setContentTitle(this.getString(R.string.app_name))
                        .setAutoCancel(Boolean.TRUE)
                        .setContentText("You are going to " + action);


        builder.setContentIntent(pendingIntent);

        notificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build());
    }
}



MyBroadcastIntentServiceTest
Our IntentService test will have to test cases: one without a bundle (which should display no notification) and one with a correctly provided bundle (which does display 1 notification).

In the latter case we will also verify that the text (displayed in the notification) contains "You are going to eat an apple", which contains the actual action that was sent using the intent in the test case.

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowNotification;
import org.robolectric.shadows.ShadowNotificationManager;

import be.acuzio.mrta.service.MyBroadcastIntentService;


@RunWith(RobolectricTestRunner.class)
public class MyBroadcastIntentServiceTest {
    static {
        ShadowLog.stream = System.out;
    }

    @Before
    public void setup() {}

    @Test
    public void testNoBundleExtrasFound() {
        Intent serviceIntent = new Intent(Robolectric.application, MyBroadcastIntentServiceMock.class);
        NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE);

        //Robolectric.getShadowApplication().startService(serviceIntent);
        MyBroadcastIntentServiceMock service = new MyBroadcastIntentServiceMock();
        service.onCreate();
        service.onHandleIntent(serviceIntent);

        Assert.assertEquals("Expected no notifications", 0, Robolectric.shadowOf(notificationManager).size());
    }

    @Test
    public void testWithBundleExtrasFound() {
        Intent serviceIntent = new Intent(Robolectric.application, MyBroadcastIntentServiceMock.class);
        Bundle bundle = new Bundle();
        bundle.putString("ACTION", "eat an apple");
        serviceIntent.putExtras(bundle);

        NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE);

        //Robolectric.getShadowApplication().startService(serviceIntent);
        MyBroadcastIntentServiceMock service = new MyBroadcastIntentServiceMock();
        service.onCreate();
        service.onHandleIntent(serviceIntent);


        ShadowNotificationManager manager = Robolectric.shadowOf(notificationManager);
        Assert.assertEquals("Expected one notification", 1, manager.size());

        Notification notification = manager.getNotification(MyBroadcastIntentService.NOTIFICATION_TAG, MyBroadcastIntentService.NOTIFICATION_ID);
        Assert.assertNotNull("Expected notification object", notification);

        ShadowNotification shadowNotification = Robolectric.shadowOf(notification);
        Assert.assertNotNull("Expected shadow notification object", shadowNotification);

        Assert.assertEquals("You are going to eat an apple", shadowNotification.getLatestEventInfo().getContentText());
    }

    class MyBroadcastIntentServiceMock extends MyBroadcastIntentService {
        @Override
        public void onHandleIntent(Intent intent) {
            super.onHandleIntent(intent);
        }
    }
}


The test class contains the MyBroadcastIntentServiceMock (might deserve a better name) which extends the MyBroadcastIntentService. By overriding and exposing the onHandleIntent(Intent intent) method we are able to actually test the code.