Montag, 2. Dezember 2013

Android Push/GCM - A very simple sample

Mobile applications often get even more interesting, if there is some exchange with a server application. The usual request-response-model looks as follows: the client – the app in this case – sends a request to the server and receives a response – immediately and synchronously.


In many cases this is enough. But what does it look like, if interesting things happen on the server and the client has to be informed about it?


Regarding the client to be responsible to fetch the data, the client could be implemented to poll, i.e. ask the server regularly, if there is something new. The disadvantages are evident: if the interval is to small, unnecessary communication will happen in many cases, if the interval is to big, the information might be „not recent enough“.
So, there has to be a possibility to access the mobile devices from the server side. Finally the distributors of the mobile systems have to be responsible to do this, since only they have the information to access the devices. The options to notify the device via call or short message are not fine-grained enough, since they reach the whole device and not one single application. Therefore the providers provide so called „Push“ services, which allow to access the applications from the server side. The responsibilty to transmit the information is transfered to the server. For iPhones of course Apple provides the appropriate service. For Android the „Google Cloud Messaging“ (GCM) is a part of the so-called „Google Play Services“. These further contain other interfaces to Google services, e.g. Google Maps, Google+ or the In-App-Payment.


The mobile application has to register as a potential receiver for push messages (1, 2) and provide the connection data to the server component (3). Using the identification of the client, the server is able to notify a relevant information (4) to the push service (5). This is responsible for submitting it soon to the mobile device (6).
To demonstrate this topic as simple as possible, I will present an app that transfers its communication data directly to the GCM service. This example only makes sense for demonstration purposes. At least I can't imagine an application that could reasonably notify itself via the Push service. At most the notification of the app on another device would be a possibility. But from an architectual point of view this would simply replace the server component in the case described above.


To gain further information there is an useful „getting startted“ tutorial available on the Google website (http://developer.android.com/google/gcm/index.html). I will restrict myself to the pure Android application.

Preparation: Google Console

In the Google Cloud Console „https://cloud.google.com/console“ there has to be created a (new) project. This one has to set under „APIs & auth / APIs“ the „Google Cloud Messaging for Android“ to „ON“. The „Project Number“ should be noted, we need it later.
A ServerKey is necessary to secure accessing the GCM service. If it is not already available (to be found at „Registered apps“ as „ServerKey“), you have to generate it. This step is quite difficult in the Google Cloud Console, as you need a SHA fingerprint – described in the tutorial. But you still have the possibility to use the „old“ Google Apis Console. („https://code.google.com/apis/console/“), where you are able to generate a ServerKey by pressing a button (at „API Access“, „Create new Server key“). The Google Apis Console tries to redirect you to the Google Cloud Console. Actually – November 2013 – it is possible to call the old Console via „Dismiss“. If this way is not available anymore, you have to do generate the key as described in the tutorial.
The ServerKey is transferred to the GCM later. The Key is the one propagated at „API KEY“ and may be copied as text. The Key should be active and „Any IP address is allowed“ should be displayed.

Preparation: Development environment

The GCM is part of the „Google Play Services“. Those have to be installed in the „Android SDK Manager“. You find them in the folder „Extras“.
To use the library in Eclipse, it has to be imported into the workspace. Just use the „File > Import“ dialog to choose „Android > Existing Android Code Into Workspace“ and enter the root folder and „Copy Projects Into Workspace“. The root folder for the Google Play Library Project is „/extras/google/google_play_services/libproject/google-play-services_lib“. To use other environments than eclipse, please consult the GCM tutorial.
For the app to be created, a new android project is created, using the Google Play Library. Therefore in the properties of the new project at „Android“ under „Library“ add the „google-play-services_lib“ project.

Preparation: Architecture

We create an app, consisting of a single activity. This just contains a button, to trigger the sending of the push message. To receive the push message, there has to be implemented a BroadcastReceiver.
In the Manifest file some permissions need to be defined:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<permission
        android:name="de.kluck.push.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />

<uses-permission android:name="de.kluck.push.permission.C2D_MESSAGE" />
For the use of the latest version of the Google Play Library inside of the application tag the following entry is needed:

<meta-data android:name="com.google.android.gms.version" 
 android:value="@integer/google_play_services_version" />

Activity first try – very simple

Two steps are necessary at least: registering the App at GCM and triggering the push message.
For the most simple case we use an Activity containing 2 Buttons, that trigger exactly these operations. We use the button with the id „registerButton“ for registering and querying the registration id. The registration id defines, for which App the device should be accessed. Since it is an external service access, it has to be done in an AsyncTask. We need the PROJECT_NUMBER for this access. This is the one, that is shown in the Google Cloud Console – as mentioned above. So, the device registers itself for exactly this project:

String PROJECT_NUMBER = "...";
Context context;
String regid;

...

context = getApplicationContext();
View registerButton = findViewById(R.id.registerButton);
registerButton.setOnClickListener(new OnClickListener() {
 @Override
 public void onClick(View v) {
  new AsyncTask() {
   @Override
   protected String doInBackground(String... arg0) {
    String regId = null;
    try {
     String projectNumber = arg0[0];
         GoogleCloudMessaging gcm = 
      GoogleCloudMessaging.getInstance(context);
         regId = gcm.register(projectNumber);
              } catch (IOException e) {
     Log.e(TAG, "Error on register", e);
              }
    return regId;
   }
   @Override
   protected void onPostExecute(String result) {
    super.onPostExecute(result);
    regid = result;
   }
  }.execute(PROJECT_NUMBER);
     }
});
As trigger for the „real“ functionality we use a button with the id „pushButton“. Once again the call is done in a AsyncTask, that is propagated the registration id of the device, besides the API-Key of the application – received in the Google Cloud resp. Google Apis Console as described above. The latter will be hooked as Authorization Key to the HttpConnection. This should make sure, that only permitted access is possible. The RegistrationId is part of the content of the request. Besides the possibility to put the load data in an JSON string, there is the possibility to put the data right into the URI. This is not as flexible, espacially you are only able to access a single RegistrationId. In other words: you are able to give a list of RegistrationIds and the assigned devices are accessed in parallel. The „data“ part of the JSON object is not flexible at will, finally it is just a list of key value pairs.

View pushButton = findViewById(R.id.pushButton);
pushButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        new AsyncTask() {
            @Override
                protected Void doInBackground(String... arg0) {
                    try {
                        String apiKey = arg0[0];
                        String regId = arg0[1];
                        URL url =
                            new URL("https://android.googleapis.com/gcm/send");
                        HttpURLConnection connection =
                            (HttpURLConnection)url.openConnection();
                        connection.setDoOutput(true);
                        connection.setRequestMethod("POST");
                        connection.setRequestProperty("Authorization",
                            "key=" + apiKey);
                        connection.setRequestProperty("Content-Type",
                            "application/json");
                        OutputStreamWriter writer =
                            new OutputStreamWriter(
                                connection.getOutputStream());
                        writer.write("{\"registration_ids\" : [\"" + regId +
                            "\"],\"data\" : { \"action\" : \"push\"}}");
                        writer.close();

                        int responseCode = connection.getResponseCode();
                        Log.i(TAG, "responseCode: " + responseCode);
                    } catch (Exception e) {
                        Log.e(TAG, "Error on service call", e);
                    }
                    return null;
                }
            }.execute(API_KEY, regid);
        }
    });
}

BroadcastReceiver

The BroadcastReceiver to receive the push messages is defined in the Manifest file with the appropriate rights and intent-filter:

<receiver
    android:name="de.kluck.push.GcmBroadcastReceiver"
    android:permission="com.google.android.c2dm.permission.SEND" >
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <category android:name="de.kluck.push" />
    </intent-filter>
</receiver>
The implementation in this example only „toasts“ the „action“ content of the Intent Extra:

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Result: " +
            intent.getStringExtra("action"), Toast.LENGTH_SHORT).show();
        setResultCode(Activity.RESULT_OK);
    }
}
The key value pairs of the JSON „data“ part of the HTTP request mentioned above are found in the Extras of the Intent. So you have the possibility to transfer data from the server to the device. Quantity and structure are restricted, so in many cases it will be implemented, so that for example just an id is transfered via push and the complex data is fetched via the „normal“ request-response access.

Activity second try – a little bit more clean

At the example above two things are disturbing: at first the GooglePlayServices are regarded as existing by the App. Since they are services that have to be installed seperately, the existence should be checked, before they are used. Secondly the registration of the App has to be done explicitely via pushing the button. A „real“ application should to this transparently in the background and since it is an external call, this should not be done more often then absolutely necessary.
To check if the availability of the GooglePlayServices on the device, a GooglePlayServicesUtil is available, practically containing methods for the output of the appropriate error dialogs:

private static final String TAG = "MainActivity";
private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;

private boolean checkPlayServices() {
    int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
    if (resultCode != ConnectionResult.SUCCESS) {
        if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
            GooglePlayServicesUtil.getErrorDialog(resultCode, this,
                PLAY_SERVICES_RESOLUTION_REQUEST).show();
        } else {
            Log.i(TAG, "This device is not supported.");
            finish();
        }
        return false;
    }
    return true;
}
Only in the case of successfully passing the test, the RegistrationId of the device can be requested via the GoogleCloudMessaging. The RegistrationId finally fixes, which App should be accessed on the device:

private static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";

String PROJECT_NUMBER = "...";
String API_KEY = "...";

GoogleCloudMessaging gcm;
Context context;
String regid;

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

    // ...  

    context = getApplicationContext();
        
    // Check device for Play Services APK
    if (checkPlayServices()) {
        //  Proceed with GCM registration.
        gcm = GoogleCloudMessaging.getInstance(this);
        regid = getRegistrationId(context);

        if (regid.isEmpty()) {
            registerInBackground();
        }
    } else {
        Log.i(TAG, "No valid Google Play Services APK found.");
    }
}
According to the GCM tutorial and with the background, that the access of the GCM service should not be done with every start of the App, the RegistrationId is stored in the SharedPreferences of the app and only requested again if necessary. Namely if the app was changed significantly.

private String getRegistrationId(Context context) {
    final SharedPreferences prefs = getGCMPreferences(context);
    String registrationId = prefs.getString(PROPERTY_REG_ID, "");
    if (registrationId.isEmpty()) {
        Log.i(TAG, "Registration not found.");
        return "";
    }
    int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION,
        Integer.MIN_VALUE);
    int currentVersion = getAppVersion(context);
    if (registeredVersion != currentVersion) {
        Log.i(TAG, "App version changed.");
        return "";
    }
    return registrationId;
}

private SharedPreferences getGCMPreferences(Context context) {
    return getSharedPreferences(MainActivity.class.getSimpleName(),
        Context.MODE_PRIVATE);
}

private static int getAppVersion(Context context) {
    try {
        PackageInfo packageInfo = context.getPackageManager()
            .getPackageInfo(context.getPackageName(), 0);
        return packageInfo.versionCode;
    } catch (NameNotFoundException e) {
        throw new RuntimeException("Could not get package name: " + e);
    }
}
Only if no or no actual RegistrationId is stored in the SharedPreferences, the service has to be accessed - using an asynchronous access, of course:

private void registerInBackground() {
    new AsyncTask() {
        @Override
        protected String doInBackground(Void... params) {
            String msg = "";
            try {
                if (gcm == null) {
                    gcm = GoogleCloudMessaging.getInstance(context);
                }
                regid = gcm.register(PROJECT_NUMBER);
                msg = "Device registered, registration ID=" + regid;

                storeRegistrationId(context, regid);
            } catch (IOException ex) {
                msg = "Error :" + ex.getMessage();
            }
            return msg;
        }

        @Override
        protected void onPostExecute(String msg) {
         Log.i(TAG, msg);
        }
    }.execute(null, null, null);
}

private void storeRegistrationId(Context context, String regId) {
    final SharedPreferences prefs = getGCMPreferences(context);
    int appVersion = getAppVersion(context);
    Log.i(TAG, "Saving regId on app version " + appVersion);
    SharedPreferences.Editor editor = prefs.edit();
    editor.putString(PROPERTY_REG_ID, regId);
    editor.putInt(PROPERTY_APP_VERSION, appVersion);
    editor.commit();
}
That was the administrative overhead. To really be clean, you should check the PlayServices in the onResume method of the activity, just to be sure, that the have not been unistalled meanwhile:

@Override
protected void onResume() {
    super.onResume();
    checkPlayServices();
}

Testing the App

The given app is not able to be executed in a virtual device and has to be tested on a real device. After the button is pressed there should relatively soon the Toast with the message be seen – not very exciting.

Summary

As already mentioned above, this example is unrealistic because of the lack of a server component. I hope to have demonstrated in a nutshell, what is necessary to use the push service in the App. The relevant parts of the server component are restricted to accessing an HTTP service. This has to happen analoguous to the access inside of the App.

Freitag, 29. November 2013

Android Push/GCM – Ein sehr einfaches Beispiel

Mobile Applikationen – also Apps – werden oft erst wirklich interessant, wenn ein Daten-Austausch mit einer Server Applikation erfolgt. Das übliche Request-Response-Modell sieht dann so aus, dass der Client – in diesem Fall die App – eine Anfrage (Request) an den Server sendet und sofort synchron die Antwort des Servers (Response) bekommt.


In vielen Fällen ist das völlig ausreichend. Wie sieht es aber aus, wenn interessante Dinge auf dem Server passieren und der Client darüber informiert werden muss?


Sieht man die „Hol-Schuld“ beim Client, würde der Client „pollen“, also in regelmäßigen Zeitabständen beim Server nachfragen, ob es was Neues gibt. Die Nachteile liegen auf der Hand: ist das Zeitintervall zu klein, findet in zu vielen Fällen ein unnötige Kommunikation statt, ist das Zeitintervall zu groß ist die Information unter Umständen „nicht ausreichend aktuell“.
Es muss also eine Möglichkeit geben, die mobilen Geräte seitens des Servers anzusprechen. Letztendlich kann die Erreichbarkeit der Geräte nur unter Mithilfe der Anbieter der Mobiltelefon-Systeme gewährleistet werden, da nur diese die entsprechenden Information besitzen. Die Optionen, das Gerät per Anruf oder SMS zu benachrichtigen, sind in diesem Fall nicht fein-granular genug, da sie das gesamte Gerät und nicht eine spezielle App erreichen. Daher stellen die Anbieter sogenannte „Push“-Dienste zur Verfügung, die es erlauben die Apps servergesteuert zu erreichen – die „Bring-Schuld“ liegt in diesem Fall also beim Server. Für iPhones stellt selbstverständlich Apple den entsprechenden Service zur Verfügung, für Android ist das als „Google Cloud Messaging“ (GCM) Bestandteil der sogenannten „Google Play Services“. Darunter sind weitere Schnittstellen zu Google-Diensten zusammengefasst, wie z.B. Google Maps, Google+ oder auch für die Abwicklung von Bezahlvorgängen (In-App-Payment).


Die App muss sich dazu als potenzieller Empfänger von Push-Nachrichten registrieren (1, 2) und die entsprechenden Verbindungsdaten der Server-Komponente zur Verfügung stellen (3). Mit der Identifikation des Clients, kann der Server ein für den Client interessantes Ereignis (4) dem Push-Service mitteilen (5). Dieser ist dann dafür verantwortlich, dieses zeitnah dem Mobilgerät weiterzuleiten (6).
Um das Thema einfachst möglich zu demonstrieren, werde ich hier eine App vorstellen, die mit ihren Verbindungsdaten den GCM-Service direkt anspricht. Das Beispiel ist eigentlich nur zu Demonstrationszwecken sinnvoll, jedenfalls fällt mir keine Anwendung ein, für die es sinnvoll wäre, dass eine App sich selbst über GCM benachrichtigen sollte. Allenfalls die Benachrichtigung der App auf einem anderen Device wäre eine vorstellbare Möglichkeit. Diese wäre dann aber aus architektonischer Sicht letztendlich nur ein Ersatz der Server-Komponente im oberen Fall.


Für weitere Informationen gibt es bei Google es ein sehr brauchbares „Getting Started“-Tutorial (http://developer.android.com/google/gcm/index.html). Ich will mich aber auf die reine Android-App beschränken.

Vorbereitung: Google-Console

In der Google Cloud Console „https://cloud.google.com/console“ ist ein (neues) Projekt anzulegen. Für dieses muss unter „APIs &; auth / APIs“ das „Google Cloud Messaging for Android“ auf „ON“ geschaltet sein. Die „Project Number“ kann man sich schon mal merken (im Sinne von kopieren), die benötigen wir später noch.
Zur Absicherung des Zugriffs auf den GCM-Service benötigt man einen ServerKey für das Projekt. Falls dieser noch nicht vorhanden ist (zu finden bei „Registered apps“ als „ServerKey“), muss er erzeugt werden. In der Google Cloud Console ist das relativ kompliziert, da man einen SHA-Fingerprint benötigt – beschrieben im Tutorial. Noch gibt es allerdings die „alte“ Google Apis Console („https://code.google.com/apis/console/“), unter der ein ServerKey per Knopfdruck generiert werden kann (dort unter „API Access“, „Create new Server key“). Die Google Apis Console versucht, auf die Google Cloud Console weiterzuleiten. Noch – Stand November 2013 – ist es jedoch möglich, per „Dismiss“ die alte Console aufzurufen. Falls das irgendwann nicht mehr möglich ist, muss wohl der im Tutorial beschriebene Weg verwendet werden.
Der ServerKey wird später dem GCM übergeben und kann als Text herauskopiert werden. Der Key sollte aktiv sein und „Any IP address is allowed“ sollte angezeigt werden.

Vorbereitung: Entwicklungsumgebung

Das GCM ist ein Bestandteil der „Google Play Services“. Diese müssen im „Android SDK Manager“ installiert sein. Zu finden sind sie unter dem Ordner „Extras“.
Um unter Eclipse auf die Library zuzugreifen, muss sie in den Workspace importiert werden. Dazu über den „File > Import“ Dialog „Android > Existing Android Code Into Workspace“ auswählen und dort das Root Verzeichnis und „Copy Projects Into Workspace“ wählen. Das Root-Verzeichnis für das Google Play Library Project ist „/extras/google/google_play_services/libproject/google-play-services_lib“. Zum Vorgehen bei anderen Umgebungen als Eclipse: siehe GCM-Tutorial.
Für die zu erstellende App wird ein Android-Projekt angelegt, dass die Google Play Library verwendet. Dafür in den Properties des neuen Projekts im Punkt „Android“ unter „Library“ das „google-play-services_lib“ Projekt hinzufügen.

Vorbereitung: Architektur

Wir erstellen eine App, die nur aus einer Activity besteht. Diese enthält nur den Button, um das Senden des Push auszulösen. Zum Empfangen der Push-Nachricht muss ein BroadcastReceiver implementiert werden.
Im Manifest-File müssen einige permissions definiert werden:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<permission
        android:name="de.kluck.push.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />

<uses-permission android:name="de.kluck.push.permission.C2D_MESSAGE" />
Für die neueste Version der Google Play Library muss innerhalb des application-Tags folgender Eintrag vorhanden sein:

<meta-data android:name="com.google.android.gms.version" 
 android:value="@integer/google_play_services_version" />

Activity 1. Anlauf – ganz einfach

Es sind zwei Schritte unbedingt notwendig: das Registrieren der App am GCM und das eigentlich Auslösen der Push-Nachricht.
Im einfachsten Fall verwenden wir eine Activity mit 2 Buttons, die genau diese Operationen auslösen. Wir verwenden den Button mit der Id „registerButton“ zum Registrieren und Holen der RegistrationId. Die RegistrationId legt letztendlich fest, für welche App das Device angesprochen werden soll. Da es sich um einen externen Service-Aufruf handelt, muss er in einem AsyncTask erfolgen. Dazu wird die PROJECT_NUMBER benötigt. Das ist diejenige, die in der Google Cloud Console angezeigt wird – siehe oben. Das Device registriert sich also für genau dieses Projekt:

String PROJECT_NUMBER = "...";
Context context;
String regid;

...

context = getApplicationContext();
View registerButton = findViewById(R.id.registerButton);
registerButton.setOnClickListener(new OnClickListener() {
 @Override
 public void onClick(View v) {
  new AsyncTask() {
   @Override
   protected String doInBackground(String... arg0) {
    String regId = null;
    try {
     String projectNumber = arg0[0];
         GoogleCloudMessaging gcm = 
      GoogleCloudMessaging.getInstance(context);
         regId = gcm.register(projectNumber);
              } catch (IOException e) {
     Log.e(TAG, "Error on register", e);
              }
    return regId;
   }
   @Override
   protected void onPostExecute(String result) {
    super.onPostExecute(result);
    regid = result;
   }
  }.execute(PROJECT_NUMBER);
     }
});
Als Auslöser der „eigentlichen“ Funktionalität verwenden wir einen Button mit der Id „pushButton“. Der Aufruf erfolgt auch hier in einem AsyncTask, dem die gerade erworbene RegistrationId des Devices ebenso übergeben wird, wie der API-Key der Anwendung – wie oben beschrieben aus der Google Cloud bzw. Google Apis Console zu beziehen. Letzterer wird dann als Authorization Key an die HttpConnection gehängt. Dadurch soll sichergestellt werden, dass nur berechtige Zugriffe erfolgen. Die Registration Id ist Bestandteil des Contents des Requests. Neben der hier verwendeten Möglichkeit, die Nutzdaten in einem JSON-String zu verpacken, gibt es auch die Möglichkeit, Daten direkt in der URL mitzugeben. Das ist aber nicht ganz so flexibel, insbesondere kann nur eine einzelne RegistrationId angesprochen werden. Mit anderen Worten: es kann durchaus durch Angabe einer Liste von RegistrationsIds eine ganze Reihe von Devices parallel angesprochen werden! Der „data“-Teil des JSON-Objekts ist nicht beliebig flexibel, letztlich ist es nur eine Liste von Key-Value-Paaren.

View pushButton = findViewById(R.id.pushButton);
pushButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        new AsyncTask() {
            @Override
                protected Void doInBackground(String... arg0) {
                    try {
                        String apiKey = arg0[0];
                        String regId = arg0[1];
                        URL url =
                            new URL("https://android.googleapis.com/gcm/send");
                        HttpURLConnection connection =
                            (HttpURLConnection)url.openConnection();
                        connection.setDoOutput(true);
                        connection.setRequestMethod("POST");
                        connection.setRequestProperty("Authorization",
                            "key=" + apiKey);
                        connection.setRequestProperty("Content-Type",
                            "application/json");
                        OutputStreamWriter writer =
                            new OutputStreamWriter(
                                connection.getOutputStream());
                        writer.write("{\"registration_ids\" : [\"" + regId +
                            "\"],\"data\" : { \"action\" : \"push\"}}");
                        writer.close();

                        int responseCode = connection.getResponseCode();
                        Log.i(TAG, "responseCode: " + responseCode);
                    } catch (Exception e) {
                        Log.e(TAG, "Error on service call", e);
                    }
                    return null;
                }
            }.execute(API_KEY, regid);
        }
    });
}

BroadcastReceiver

Der BroadcastReceiver zum Empfangen der Push-Nachricht wird im Manifest-File mit der entsprechenden Berechtigung und dem passenden Intent-Filter definiert:

<receiver
    android:name="de.kluck.push.GcmBroadcastReceiver"
    android:permission="com.google.android.c2dm.permission.SEND" >
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <category android:name="de.kluck.push" />
    </intent-filter>
</receiver>
Die Implementierung soll für dieses Beispiel einfach nur den „action“-Betandteil des Intent-Extras „toasten“:

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Result: " +
            intent.getStringExtra("action"), Toast.LENGTH_SHORT).show();
        setResultCode(Activity.RESULT_OK);
    }
}
Die oben angesprochenen Key-Value-Paare des JSON-“data“-Bestandteil des HTTP-Requests finden sich tatsächlich hier in den „Extras“ des Intents wieder. Man hat so also die Möglichkeit, Daten vom Server zum Device zu transportieren. Umfang und Struktur sind zwar eingeschränkt, aber es besteht ja immer noch die Möglichkeit, beispielsweise eine Id zu übergeben und die kompletten Daten über den „normalen“ Request-Response-Weg zu holen.

Activity 2. Anlauf – etwas sauberer, bitte

An dem obigen Beispiel stören zwei Dinge: zum einen werden die Google Play Services von der App als vorhanden vorausgesetzt. Da es sich um einen zu installierenden Service handelt, sollte die Existenz geprüft werden, bevor er verwendet wird. Zum anderen muss die Registrierung der App explizit per Knopfdruck erfolgen. Bei einer richtigen Anwendung sollte das transparent im Hintergrund stattfinden und da es sich um einen externen Aufruf handelt, auch nicht öfter als unbedingt notwendig.
Um das Vorhandensein der GooglePlayServices auf dem Device zu prüfen, steht ein GooglePlayServicesUtil zur Verfügung, was praktischerweise auch gleich Methoden zur Ausgabe der entsprechenden Fehler-Dialoge enthält:

private static final String TAG = "MainActivity";
private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;

private boolean checkPlayServices() {
    int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
    if (resultCode != ConnectionResult.SUCCESS) {
        if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
            GooglePlayServicesUtil.getErrorDialog(resultCode, this,
                PLAY_SERVICES_RESOLUTION_REQUEST).show();
        } else {
            Log.i(TAG, "This device is not supported.");
            finish();
        }
        return false;
    }
    return true;
}
Nur wenn dieser Test erfolgreich ist, kann über das GoogleCloudMessaging die RegistrationId des Devices geholt werden:

private static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";

String PROJECT_NUMBER = "...";
String API_KEY = "...";

GoogleCloudMessaging gcm;
Context context;
String regid;

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

    // ...  

    context = getApplicationContext();
        
    // Check device for Play Services APK
    if (checkPlayServices()) {
        //  Proceed with GCM registration.
        gcm = GoogleCloudMessaging.getInstance(this);
        regid = getRegistrationId(context);

        if (regid.isEmpty()) {
            registerInBackground();
        }
    } else {
        Log.i(TAG, "No valid Google Play Services APK found.");
    }
}
Angelehnt an das GCM-Tutorial und mit dem Hintergrund, dass der Zugriff auf den GCM-Service nicht bei jedem Start der App erfolgen soll, wird hier die RegistrationId in den SharedPreferences der App gespeichert und nur neu geholt, wenn notwendig. Nämlich dann, wenn sich die App signifikant geändert hat.

private String getRegistrationId(Context context) {
    final SharedPreferences prefs = getGCMPreferences(context);
    String registrationId = prefs.getString(PROPERTY_REG_ID, "");
    if (registrationId.isEmpty()) {
        Log.i(TAG, "Registration not found.");
        return "";
    }
    int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION,
        Integer.MIN_VALUE);
    int currentVersion = getAppVersion(context);
    if (registeredVersion != currentVersion) {
        Log.i(TAG, "App version changed.");
        return "";
    }
    return registrationId;
}

private SharedPreferences getGCMPreferences(Context context) {
    return getSharedPreferences(MainActivity.class.getSimpleName(),
        Context.MODE_PRIVATE);
}

private static int getAppVersion(Context context) {
    try {
        PackageInfo packageInfo = context.getPackageManager()
            .getPackageInfo(context.getPackageName(), 0);
        return packageInfo.versionCode;
    } catch (NameNotFoundException e) {
        throw new RuntimeException("Could not get package name: " + e);
    }
}
Nur dann, wenn keine (aktuelle) RegistrationId in den SharedPreferences hinterlegt ist, muss der Service aufgerufen werden – auch hier natürlich über einen asynchronen Aufruf.

private void registerInBackground() {
    new AsyncTask() {
        @Override
        protected String doInBackground(Void... params) {
            String msg = "";
            try {
                if (gcm == null) {
                    gcm = GoogleCloudMessaging.getInstance(context);
                }
                regid = gcm.register(PROJECT_NUMBER);
                msg = "Device registered, registration ID=" + regid;

                storeRegistrationId(context, regid);
            } catch (IOException ex) {
                msg = "Error :" + ex.getMessage();
            }
            return msg;
        }

        @Override
        protected void onPostExecute(String msg) {
         Log.i(TAG, msg);
        }
    }.execute(null, null, null);
}

private void storeRegistrationId(Context context, String regId) {
    final SharedPreferences prefs = getGCMPreferences(context);
    int appVersion = getAppVersion(context);
    Log.i(TAG, "Saving regId on app version " + appVersion);
    SharedPreferences.Editor editor = prefs.edit();
    editor.putString(PROPERTY_REG_ID, regId);
    editor.putInt(PROPERTY_APP_VERSION, appVersion);
    editor.commit();
}
Das war dann auch „schon“ der notwendige Verwaltung-Overhead. Um ganz sauber zu sein, sollte man auch noch in der onResume-Methode der Activity die PlayServices abchecken, um sicher zu stellen, dass sie nicht zwischenzeitlich deinstalliert wurden:

@Override
protected void onResume() {
    super.onResume();
    checkPlayServices();
}

Test der App

Die App ist in der vorgestellten Form nicht im Emulator lauffähig und muss daher auf einem realen Gerät getestet werden. Dort sollte nach einem Druck auf den Button relativ schnell der Toast mit der Nachricht erscheinen – ganz unspektakulär.

Fazit

Wie bereits gesagt, dieses Beispiel ist weltfremd durch den Verzicht auf eine Server-Komponente. Ich hoffe aber, damit in aller Kürze demonstriert zu haben, was seitens der Android-App notwendig ist, um den Push-Dienst zu verwenden. Die relevanten Teile der Server-Komponente wären auch nur das Ansprechen des HTTP-Dienstes, was inhaltlich völlig analog zum hier dargestellten Zugriff innerhalb der App erfolgen würde.

Dienstag, 22. Mai 2012

ActionBar

The menu control of Android is an interesting example for how the developers of the system react on experiences with the usage of the system. In the beginning - before 3.0 - the options menu was regarded so important, that there was given a seperate key for it. The menu was realized as a pure popup menu. Even the name speaks for the purpose of the menu as editing settings of the application. Indeed it happened that many applications mis-used the options menu to realize navigation.
With 3.0 the concept was re-worked. Surely it was taken into account, that the purpose of this version was mainly to support tablets and so there was enough space on the screen. The action bar as a totally new element was introduced and the options menu was integrated in this component. The action bar as a given part by default is always visible on the screen (though it may be deactivated). Since Android 4.0 the action bar is also part of the smartphone variant of the operating system. The menu key is not longer a required element of a smartphone.
The action bar is mainly devided into 4 parts ("app icon", "view control", "action buttons" and "action overflow"). The visible appearance is configurable and depends on the space available on the actual device. So the "view control" may be shown in an additional row below the main action bar (as visible in the image) or the "action buttons" may use the space on the bottom of the screen.
The "app icon" (left in the image) serves to identify the running application. It can also be used to navigate "home" or "back".
The "view control" is used to navigate in between the application, for example to activate different pages (fragments) of an activity in form of tabs.
The "action buttons" (right in the image) get close to the options menu used so far. They serve to call context sensitive additional actions. For actions used regularly this can be done directly by icon in this area, less reqularly used actions can be placed in the "action overflow" part. If there is a menu key on the device, the "action overflow" is activated by this key, otherwise there will be shown an appropriate selection list.
Not only relating to content and to handling the actions buttons of Android 3.0 replace the options menu of the earlier versions, but also from the programmers view. The transfer is very transparent.
With 2.x an options menu could be simply declared as following:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
 <item android:id="@+id/mainMenuItem" android:title="@string/main" />
    <item android:id="@+id/layoutDemoMenuItem" android:title="@string/layoutDemo" />
    <item android:id="@+id/layoutDemo2MenuItem" android:title="@string/layoutDemo" />
    <item android:id="@+id/fragmentMenuItem" android:title="@string/fragment" />
</menu>
Methods to add the options menu and to react on the selection of an item are overwritten on the Activity. In this case in form of a BaseActivity, where all the Acivity-Classes that use the options menu may be derived.

public class BaseActivity extends Activity {

     @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  MenuInflater menuInflater = getMenuInflater();
  menuInflater.inflate(R.menu.menu, menu);
  return true;
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
  case R.id.mainMenuItem:
  case android.R.id.home:
   Log.v(getLocalClassName(), "call main");
   Intent intent = new Intent(this, Workshop4Activity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
   startActivity(intent);
   break;
  case R.id.fragmentMenuItem:
   Log.v(getLocalClassName(), "call fragment");
   startActivity(new Intent(this, FragmentActivity.class));
   break;
  case R.id.layoutDemoMenuItem:
   Log.v(getLocalClassName(), "call layoutDemo");
   startActivity(new Intent(this, LayoutDemoActivity.class));
   break;
  case R.id.layoutDemo2MenuItem:
   Log.v(getLocalClassName(), "call layoutDemo");
   startActivity(new Intent(this, LayoutDemoActivity2.class));
   break;
  }
  return true;
 }
}
That's all for the trivial case.
To make the entries of the options menu visible in the action bar on Android 4.0, the corresponding entry of the XML file is given an attribute "android:showAsAction". Possible values are "never", "always", "ifRoom", "withText" and "collapseActionView". Those values may be - if it makes sense - combined with "|".
That's all so far, the BaseActivity does not need to be changed.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
 <item android:id="@+id/mainMenuItem" android:title="@string/main"
  android:showAsAction="ifRoom|withText" />
    <item android:id="@+id/layoutDemoMenuItem" android:title="@string/layoutDemo"
   android:showAsAction="always" />
    <item android:id="@+id/layoutDemo2MenuItem" android:title="@string/layoutDemo"
  android:showAsAction="ifRoom" />
    <item android:id="@+id/fragmentMenuItem" android:title="@string/fragment"
  android:showAsAction="ifRoom" />
</menu>
It is possible to replace the "action buttons" by more complicated controls. The possibilities surely are limited by the rare available space. An ActionProvider may replace the complete logic. "collapseActionView" allows a "short view", from which by click a more detailed view is opened.
The navigation in the "view control" part of the action bar also is not very complicated. The developer documentation describes this very useful (http://developer.android.com/guide/topics/ui/actionbar.html). There is for the case, that the pages to navigate to are realized as Fragments, simply implemented an ActionBar.TabListener. This one instantiiates the concrete Fragments as needed. Each "view control" entry of the action bar is linked with the corresponding Listener. At last only the NavigationMode of the action bar has to be set to "NAVIGATION_MODE_TABS" - in the onCreate-method of the Activity. The ContentView of the Activity has not to be set, only the default container is used. It also is relatively simple, to use a Spinner-control instead of a tab navigation. The elements of the list are defined in an array ressource and the selection of the elements is realized by a SpinnerAdapter and an OnNavigationListener.

ActionBar

Die Menu-Steuerung in Android ist ein interessantes Beispiel dafür, wie die Entwickler des Systems auf Erfahrungen mit der Verwendung des Systems eingehen. Ursprünglich – vor 3.0 - wurde das Options-Menu so wichtig erachtet, dass dafür sogar eine eigene Taste spendiert wurde. Das Menu wurde als reines Popup-Menu realisiert. Schon der Name spricht dafür, dass dieses Menu in erster Linie dazu gedacht war, Einstellungen für die App vorzunehmen. Es stellte sich allerdings heraus, dass viele Anwendungen das Options-Menu dazu missbrauchten, Navigationen zu realisieren.
Ab 3.0 wurde das Konzept überarbeitet – wobei sicherlich auch die Tatsache von Bedeutung war, dass diese Version speziell für Tablets vorgesehen und damit reichlich Platz auf dem Screen vorhanden war. Die Action-Bar als vollkommen neues Element wurde eingeführt und das bisherige Options-Menu in diese Komponente integriert. Die Action-Bar ist als fester Bestandteil per Default immer am Screen sichtbar (kann aber auch deaktiviert werden). Ab Android 4.0 ist die Action-Bar dann auch Bestandteil der Smartphone-Variante des Betriebssystems. Die Menu-Taste ist damit kein Pflichtelement eines Smartphones mehr.
Die ActionBar ist im wesentlichen in 4 Bereiche unterteilt („App-Icon“, „View-Control“, „Action-Buttons“ und „Action-Overflow“), wobei die Darstellung konfigurierbar ist und auch davon abhängig, wie viel Platz auf dem aktuellen Gerät zur Verfügung steht. So kann der „View-Control“-Bereich in einer zusätzlichen Zeile unterhalb der Haupt-ActionBar dargestellt werden (wie im Bild erkennbar) oder die „Action-Buttons“ verwenden den Platz am Fuß des Bildschirms.
Das „App-Icon“ (im Bild ganz links) soll natürlich in erster Linie dazu dienen, die aktuell laufende App zu identifizieren. Es kann auch dazu verwendet werden, zur „Home“-Activity der App oder einfach „Back“ zu navigieren.
Die „View-Control“ wird zur Navigation innerhalb der App verwendet, beispielsweise um in Form von Tabs verschiedene Seiten (Fragmente) einer Activity zu aktivieren.
Die „Action-Buttons“ (im Bild ganz rechts) kommen dem bisherigen Options-Menu noch am nächsten. Sie sollen dazu dienen, kontextabhängig zusätzliche Aktionen aufzurufen. Für häufige Aktionen kann das direkt per Icon in diesem Bereich erfolgen, weniger häufig benötigte Aktionen können im „Action-Overflow“-Bereich liegen. Wenn das Device eine Menu-Taste besitzt, wird dieser „Action-Overflow“-Bereich durch Klick auf die Taste aktiviert, ansonsten wird ein entsprechende Auswahl-Liste dargestellt.
Nicht nur bezüglich Inhalt und Bedienung lösen die Action-Buttons ab Android 3.0 das Options Menu der vorhergehenden Versionen weitestgehend ab, auch aus Sicht der Programmierung. Der Übergang ist auf dieser Ebene sehr transparent:
Unter 2.x konnte ein Options Menu einfach in der folgenden Form deklariert werden:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
 <item android:id="@+id/mainMenuItem" android:title="@string/main" />
    <item android:id="@+id/layoutDemoMenuItem" android:title="@string/layoutDemo" />
    <item android:id="@+id/layoutDemo2MenuItem" android:title="@string/layoutDemo" />
    <item android:id="@+id/fragmentMenuItem" android:title="@string/fragment" />
</menu>
Methoden zum Anhängen des Options Menu und zur Reaktion auf die Auswahl eines Items werden an der Activity überschrieben. Hier in Form einer BaseActivity, von der alle Activities abgeleitet werden können, die das Options Menu bereitstellen sollen:

public class BaseActivity extends Activity {

     @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  MenuInflater menuInflater = getMenuInflater();
  menuInflater.inflate(R.menu.menu, menu);
  return true;
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
  case R.id.mainMenuItem:
  case android.R.id.home:
   Log.v(getLocalClassName(), "call main");
   Intent intent = new Intent(this, Workshop4Activity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
   startActivity(intent);
   break;
  case R.id.fragmentMenuItem:
   Log.v(getLocalClassName(), "call fragment");
   startActivity(new Intent(this, FragmentActivity.class));
   break;
  case R.id.layoutDemoMenuItem:
   Log.v(getLocalClassName(), "call layoutDemo");
   startActivity(new Intent(this, LayoutDemoActivity.class));
   break;
  case R.id.layoutDemo2MenuItem:
   Log.v(getLocalClassName(), "call layoutDemo");
   startActivity(new Intent(this, LayoutDemoActivity2.class));
   break;
  }
  return true;
 }
}
Das ist im Trivialfall schon alles.
Um nun unter Android 4.0 die Einträge des Options Menus in der ActionBar erscheinen zu lassen, wird der entsprechende Eintrag in der XML-Datei einfach mit einem Attribute „android:showAsAction“ versehen. Mögliche Werte sind „never“, „always“, „ifRoom“, „withText“ und „collapseActionView“. Diese können auch – falls sinnvoll – mit „|“ verknüpft werden.
Ansonsten ist das schon alles, die BaseActivity bleibt unverändert.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
 <item android:id="@+id/mainMenuItem" android:title="@string/main"
  android:showAsAction="ifRoom|withText" />
    <item android:id="@+id/layoutDemoMenuItem" android:title="@string/layoutDemo"
   android:showAsAction="always" />
    <item android:id="@+id/layoutDemo2MenuItem" android:title="@string/layoutDemo"
  android:showAsAction="ifRoom" />
    <item android:id="@+id/fragmentMenuItem" android:title="@string/fragment"
  android:showAsAction="ifRoom" />
</menu>
Es ist möglich, die „Action-Buttons“ durch kompliziertere Controls zu ersetzen. Die Möglichkeiten sind natürlich immer durch den äußerst geringen Platz begrenzt. Ein ActionProvider ersetzt sogar die komplette Logik. „collapseActionView“ erlaubt dann eine „Kurzdarstellung“, aus der per Klick eine ausführliche Darstellung aufklappt.
Die Navigation im „View-Control“-Bereich der ActionBar ist auch nicht sonderlich kompliziert. Die Entwicklerdokumentation beschreibt das sehr anschaulich (http://developer.android.com/guide/topics/ui/actionbar.html). Dort wird für den Fall, dass die einzelnen Seiten der Navigation als Fragmente realisiert sind, einfach ein ActionBar.TabListener implementiert. Dieser instantiiert bei Bedarf die entsprechenden Fragmente. Jeder „View-Cotrol“-Eintrag der ActionBar wird dann mit dem entsprechenden Listener verknüpft. Schließlich muss nur noch der NavigationMode der ActionBar auf „NAVIGATION_MODE_TABS“ gesetzt werden – in der onCreate-Methode der Activity. Die ContentView der Activity wird nicht gesetzt, hier wird einfach der Default-Container verwendet. Relativ einfach ist es dann auch, statt einer Tab-Navigation einen Spinner zu verwenden. Die Liste der Elemente muss in der entsprechenden Array-Resource abgelegt werden. Und die Auswahl der Elemente wird über einen SpinnerAdapter und einen OnNavigationListener realisiert.

Samstag, 3. März 2012

Use of Fragments in Android

Android-Apps generally should be able to run on several different devices. Especially resolution and orientation (portrait, landscape) of an application havte to be taken into account. In particular the latter is a challenge for the developer of an application. To build an app that looks acceptable horizontally and vertically is not a trivial task - particularly if you want to avoid to much scrolling.
Introduced by Android 3.0 Honeycomb the Fragments might be helpful. Those are parts of an Activity, getting developed independently. Particularly they have a lifecycle for themselves - you find details in the android documentation. Fragments are only usable as part of an Activity.
To show you how to handle Fragments, I would like to introduce a rudimentary use case, that might serve as a base for more general cases.
What will be implemented is a list of values (first fragment) and the detail view of one selected value (second fragment). In the landscape view list and detail shall be shown beside (meaning one Activity). In the portrait view at first only the list will be seen and the selection of an element will fork into the detail view (two Activities).
The code of the base Activity is the same in both cases:

public class FragmentActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment);
    }
}
The layouts have to be seperated. For landscape in /res/layout-land/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

</LinearLayout>
For portrait in /res/layout-port/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
For the detail view in the portrait mode another layout has to be created (/res/layout-port/fragment_detail_activity.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
And the Activity for the detail view has to be coded, too.

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		setContentView(R.layout.fragment_detail_activity);
		Bundle extras = getIntent().getExtras();
		if (extras != null) {
			String s = extras.getString("selectedValue");
			TextView view = (TextView) findViewById(R.id.text_detail);
			view.setText(s);
		}
	}
}
The development of the list fragment looks quite simple, since Android already provides a ListFragment class. You just need to subclass this class and set its ListAdapter. A layout is not needed, as far as the standard layout is used for the list entries.
Even in this class the click event for the list entry has to be processed. This is the only place where it is necessary to distinguish the modes the Activity might be in. Depending on this in the landscape mode simply the selected value has to be shown in the detail Fragment and in portrait mode the detail Activity is started via Intent.

public class FragmentList extends ListFragment {
	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		String[] values = new String[] { "One", "Two", "Three", "Four", "Five" };
		ArrayAdapter adapter = new ArrayAdapter(getActivity(),
				android.R.layout.simple_list_item_1, values);
		setListAdapter(adapter);
	}

	@Override
	public void onListItemClick(ListView l, View v, int position, long id) {
		String item = (String) getListAdapter().getItem(position);
		FragmentDetail fragment = (FragmentDetail)getFragmentManager().findFragmentById(R.id.fragment_detail);
		if (fragment != null && fragment.isInLayout()) {
			fragment.setText(item);
		} else {
			Intent intent = new Intent(getActivity().getApplicationContext(), FragmentDetailActivity.class);
			intent.putExtra("selectedValue", item);
			startActivity(intent);

		}
	}    
}
The detail fragment implements a method to set the text.

public class FragmentDetail extends Fragment {
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.fragment_detail, container, false);
		return view;
	}

	public void setText(String item) {
		TextView view = (TextView) getView().findViewById(R.id.text_detail);
		view.setText(item);
	}
}
The layout for the detail fragment is kept simple in this case (/res/layout/fragment_detail.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/text_detail"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal|center_vertical"
        android:layout_marginTop="20dp"
        android:text="Detail"
        android:textSize="30dp" />
    
</LinearLayout>
What finally misses is the code for the detail Activity, that just needs to fetch the detail text of the Intent at start and set it on the Fragment.

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		setContentView(R.layout.fragment_detail_activity);
		Bundle extras = getIntent().getExtras();
		if (extras != null) {
			String s = extras.getString("selectedValue");
			TextView view = (TextView) findViewById(R.id.text_detail);
			view.setText(s);
		}
	}
}
So far, so simple. As you can see, by the use of the Fragments their code is simply reusable. But the developer still has to maintain the layouts for the different modes.
Remark: just like the ListFragment there are PreferenceFragment and DialogFrament classes for the specific use cases.

Verwendung von Fragmenten in Android

Android-Apps erheben generell den Anspruch auf verschiedensten Devices zu laufen. Insbesondere Auflösung und Ausrichtung (Portrait, Landscape) einer Application gilt es zu unterscheiden. Für den Entwickler einer Applikation stellt gerade Letzteres eine Herausforderung dar. Eine Application sowohl horizontal, wie auch vertikal ordentlich aussehen zu lassen ist nicht trivial – insbesondere wenn man dem Nutzer Scroll-Orgien ersparen will.
Eine Hilfe dabei sind die ab Android 3.0 Honeycomb eingeführten Fragments. Dabei handelt es sich um Teile eine Activity, die eigenständig entwickelt werden können. Insbesondere besitzen sie auch einen eigenen Lifecycle – Details dazu in der Android-Doku. Fragments können aber nur im Rahmen einer Activity verwendet werden.
Um den Umgang mit Fragments zu veranschaulichen, werde ich einen rudimentären Anwendungsfall vorstellen, der als Basis für allgemeinere Fälle verwendet werden kann.
Grundlage ist eine Liste mit Werten (Fragment 1) und die Detailansicht eines ausgewählten Wertes (Fragment 2). Während in der Landscape-Sicht Liste und Detail nebeneinander dargestellt werden sollen (also in einer Activity), soll in der Portrait-Sicht zunächst nur die Liste zu sehen sein und die Auswahl eines Elementes in die Detailsicht verzweigen (zwei Activities).
Der Code der Basis-Activity ist in beiden Fällen der gleiche:

public class FragmentActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment);
    }
}
Hingegen müssen die Layouts unterschieden werden. Für Landscape in /res/layout-land/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

</LinearLayout>
Für Portrait in /res/layout-port/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
Für die Detailansicht im Portrait-Layout muss ein weiteres Layout angelegt werden (/res/layout-port/fragment_detail_activity.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
Und die Activity für die Detailansicht muss auch codiert werden:

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
  setContentView(R.layout.fragment_detail_activity);
  Bundle extras = getIntent().getExtras();
  if (extras != null) {
   String s = extras.getString("selectedValue");
   TextView view = (TextView) findViewById(R.id.text_detail);
   view.setText(s);
  }
 }
}
Die Entwicklung des List-Fragments gestaltet sich ganz einfach, da Android bereits eine entsprechende ListFragment-Klasse zur Verfügung stellt. Es muss nur davon abgeleitet und ein ListAdapter gesetzt werden. Ein Layout wird nicht benötigt, sofern das Standard-Layout für die Listen-Einträge verwendet wird.
Allerdings muss in dieser Klasse auch das Click-Event für den Listen-Eintrag bearbeitet werden und das ist die Stelle, wo tatsächlich geprüft werden muss, in welchem Modus sich die zugeordnete Activity befindet. Abhängig davon wird dann im Landscape-Modus einfach nur der selektierte Wert im Detail-Fragment dargestellt bzw. im Portrait-Modus die Detail-Activity via Intent gestartet.

public class FragmentList extends ListFragment {
 @Override
 public void onActivityCreated(Bundle savedInstanceState) {
  super.onActivityCreated(savedInstanceState);
  String[] values = new String[] { "One", "Two", "Three", "Four", "Five" };
  ArrayAdapter adapter = new ArrayAdapter(getActivity(),
    android.R.layout.simple_list_item_1, values);
  setListAdapter(adapter);
 }

 @Override
 public void onListItemClick(ListView l, View v, int position, long id) {
  String item = (String) getListAdapter().getItem(position);
  FragmentDetail fragment = (FragmentDetail)getFragmentManager().findFragmentById(R.id.fragment_detail);
  if (fragment != null && fragment.isInLayout()) {
   fragment.setText(item);
  } else {
   Intent intent = new Intent(getActivity().getApplicationContext(), FragmentDetailActivity.class);
   intent.putExtra("selectedValue", item);
   startActivity(intent);

  }
 }    
}
Das Detail-Fragment bekommt eine Methode, um den Text zu setzen:

public class FragmentDetail extends Fragment {
 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container,
   Bundle savedInstanceState) {
  View view = inflater.inflate(R.layout.fragment_detail, container, false);
  return view;
 }

 public void setText(String item) {
  TextView view = (TextView) getView().findViewById(R.id.text_detail);
  view.setText(item);
 }
}
Das Layout für das Detail-Fragment ist hier auch bewusst einfach gehalten (/res/layout/fragment_detail.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/text_detail"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal|center_vertical"
        android:layout_marginTop="20dp"
        android:text="Detail"
        android:textSize="30dp" />
    
</LinearLayout>
Schließlich fehlt noch der Code für die Detail-Activity, der aber letztendlich nur beim Starten den Detail-Text aus dem Intent holen und dem Fragment übergeben muss.

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
  setContentView(R.layout.fragment_detail_activity);
  Bundle extras = getIntent().getExtras();
  if (extras != null) {
   String s = extras.getString("selectedValue");
   TextView view = (TextView) findViewById(R.id.text_detail);
   view.setText(s);
  }
 }
}
So weit, so einfach. Es ist gut erkennbar, dass zwar zum einen durch die Fragmente der entsprechende Code sehr schön wiederverwendbar ist, zum anderen aber dem Entwickler nicht die Aufgabe abgenommen wird, die Layouts für die verschiedenen Modus zu pflegen.
Als Anmerkung: ähnlich dem ListFragment gibt es auch PreferenceFragment und DialogFragment Klassen für die entsprechenden Spezialfälle.

Sonntag, 26. Februar 2012

AsyncTask and AsyncTaskLoader

As soon as you develop some larger application based on the android platform, you will soon discover, that the system refuses to execute runtime-extensive tasks in the UI thread. This are accesses via the network, but also to the file system, to preferences and to the database. Depending on the API level it leads to a corresponding message ("application not responding") or even to an exception.

So the relevant topic has to be rolled out to a asynchronous task. Not rarely such a task will have to change the UI after being completed successfully, for instance to show the results. Since it is not possible to access the UI thread from this thread again, the synchronisation is part of the problem.

The familiar approach for a Java developper would be simply to start a java thread. But from this thread it is not possible to access the UI. For this case the android Activity provides a "runOnThread" method. This given a Runnable to process the tasks in the UI thread. Schematically it would look like this:

Thread thread = new Thread(new Runnable() {
   public void run() {
      // ...
      // do long running tasks here
      // …

      // running in Activity context
      runOnUiThread(new Runnable() {
         public void run() {
            // ...
            // update UI here
            // ...
         }
      });
   }
});
thread.start();


A little more elegant and "android like" is the usage of the "AsyncTask<>". It will be subclassed and some methods have to be implemented. The template parameters define the parameters of the "doInBackground" call, the "onProgressUpdate" method and the type of the return value for the "onPostExecute" method. The latter will be called in the context of the UI thread.


public void useTask() {
   new FooTask().execute("param1", "param2");
}

class FooTask extends AsyncTask {
   protected String doInBackground(String... params) {
      int progress = 0;
      String result = params.length > 0 ? params[0] : "";
      // ...
      // do long running tasks here
      // ...
      publishProgress(progress);
      // ...
      // work
      // ...
      return result;
   }
   protected void onProgressUpdate(Integer... progress) {
      // ...
      // propagate progress to UI
      //
   }
   protected void onPostExecute(String result) {
      // ...
      // update UI here
      // ...
   }
}


Another possibility to solve the problem would be to implement a specific service for the task. The communication between service and UI thread would be done via Intents and BroadcastReceiver, making the implementation quite complex and unclear.

All these approaches have the downside, that it has to be reagarded, if the Activity is terminated prematurely. This can soon be the case for instance just by turning the device. The asynchronous task in this case would continue running in its thread. The processing of the results could soon lead to unrequested results.

Since Android 3.0, API-Level 11 another possibility enters the game with the "Loader" concept. By using the V4 support library it is also available for earlier API levels. Loader in Android are managed by a Loader manager, that for instance also should handle the case of premature termination of the Activity. The name implicates that the Loader specifically is destined for such topics where by access of some (external) resource data has to be loaded. For the case described in this post, specifically the "AsyncTaskLoader<>" is relevant.

Again it will be subclassed with the type of the return value of the loading operation as parameter of the template. The class to use the loader should implement the interface "LoaderManger.LoaderCallbacksy<>" that defines the methods for the callback. Additionally there is defined a "onCreateLoader"
method, that has to instatiate the appropriate Loader - depending on its parameter. The proper instance will always and only be requested via the LoaderManager.

The Loader subclass just has to implement the "loadInBackground" method. Parameters may be propagated to the Loader subclass via the constructor.
Schematically it looks like this:

class FooLoader extends AsyncTaskLoader {
   public FooLoader(Context context, Bundle args) {
      super(context);
      // do some initializations here
   }
   public String loadInBackground() {
      String result = "";
      // ...
      // do long running tasks here
      // ...
      return result;
   }
}

class FooLoaderClient implements LoaderManager.LoaderCallbacks {
   Activity context;
   // to be used for support library:
   // FragmentActivity context2;
   public Loader onCreateLoader(int id, Bundle args) {
      // init loader depending on id
      return new FooLoader(context, args);
   }
   public void onLoadFinished(Loader loader, String data) {
      // ...
      // update UI here
      //
   }
   public void onLoaderReset(Loader loader) {
      // ...
   }
   public void useLoader() {
      Bundle args = new Bundle();
      // ...
      // fill in args
      // ...
      Loader loader = 
         context.getLoaderManager().initLoader(0, args, this);
      // with support library: 
      // Loader loader = 
      //    context2.getSupportLoaderManager().initLoader(0, args, this);
      // call forceLoad() to start processing
      loader.forceLoad();
   }
}


Which one of the possibilities described you choose, depends on the specific context. The Loader mechanism might be inflexible in some cases, since it is intended for loading data specifically. Since it uses a general concept of Android, it will surely get more into focus in the future.