ExampleArchiv

In meinem letzten Beitrag “Using the Android Interface Definition Language (AIDL) to make a Remote Procedure Call (RPC) in Android” habe ich die Grundlagen erläutert, wie die Kommunikation zwischen Prozessen in Android umgesetzt werden kann. Jetzt werden wir einen Blick auf ein Spezialgebiet in diesem Bereich werfen: Parcelables als Parameter einer AIDL Methode.

Wie im letzten Beitrag beschrieben, ist es möglich, entweder primitive Java Typen innerhalb einer Remote-Method Signature oder irgendeine Klasse, die das android.os.Parcelable Interface implementiert, zu benutzen. Über dies Interface ist es möglich, beliebig komplexe Datentypen zu definieren, die als Parameter oder Rückgabewerte von Methoden während eines Remote Method Aufrufs genutzt werden können.

Ich habe eine modifizierte Version des ersten Beispiel-Apps kreiert, um Ihnen ein einfaches Beispiel zu geben, das ein Parcelable Object als Methoden-Rückgabewert nutzt. Das Beispiel besteht aus einem Service, der ein Parcelable Message Object erzeugt, das die aktuelle Systemzeit sowie das Erscheinungsbild des Textes, wie Farbe, Größe und Stil, enthält. Im zweiten Teil des Beispiels finden Sie eine Activity, die mit dem AIDL IPC ein solches Message Object erzeugt, um die aktuelle Uhrzeit anzuzeigen. Die entsprechenden Projekte sind AIDLRemoteClientUsingParcelableObject für das Client-Projekt (enthält die Activity) und AIDLRemoteMessageServiceUsingParcelableObject für den Server (enthält den Service).

Stellen Sie sich eine iOS App vor, die ein HTML5 Video bereitstellt, das Sie abspielen und stoppen können, und mit dem Sie interagieren können. Das ist genau das, was wir in diesem Tutorial zusammenbauen wollen.

HTML

Da nur kleine Änderungen notwendig sind, werden wir damit beginnen, den HTML/JS-Code zu bearbeiten, der in diesem Artikel bereitgestellt wird. Zuerst verbinden wir ein JavaScript onclick-Attribut mit jedem der Elemente, die

umgeben. Dazu nehmen wir die setupItems()-Funktion:

var t = $(items[i][3]);

Beachten Sie, dass das Encoding notwendig ist, weil der String oben als Funktionsparameter gegeben ist.

Wie Sie sehen können, wird Overlay.call() mit der entsprechenden Item-ID aufgerufen, wenn Sie auf eine Box klicken. Call() arbeitet ähnlich zu der Art und Weise, wie wir windows.location in einem vorangegangenen Artikel verwendet haben, um Daten an die iOS parent app zu schicken.  Es nimmt die ID und leitet die Seite weiter zu so etwas wie ?cmd=call¶m=2. Diese Zeichenfolge kann später von der iOS-App gelesen werden.

Das ist es auch schon! Starten Sie nun Xcode und erzeugen Sie eine neue, view-based Applikation. Nennen Sie es App Solut InteractiveVideoInterface, und öffnen Sie Ihre ViewController header Datei. Fügen Sie folgenden Code ein:

@interface App_Solut_InteractiveVideoInterfaceViewController : UIViewController {

IBOutlet UIWebView *webView;
IBOutlet UILabel *label;
}
@property(retain, nonatomic) UIWebView *webView;
@property(retain, nonatomic) UILabel *label;

@end

Speichern Sie die Datei, wechseln Sie zum Interface Builder und bearbeiten Sie die xib Datei des ViewControllers. Ziehen Sie ein WebView und ein Label auf dem Bildschirm und verbinden Sie die Outlets, die soeben erzeugt wurden. Dazu ziehen Sie (während Sie die Strg-Taste gedrückt halten) aus dem WebView Objekt zum Besitzer der Dateien und wählen Sie delegate. Dann das gleiche umgekehrt: ziehen Sie (während Sie die Strg-Taste gedrückt halten, oder nehmen Sie die rechte Maustaste, wenn Sie eine Standardmaus benutzen) vom Besitzer der Dateien zum  WebView und zum Label und wählen Sie die entsprechenden Einträge aus der Liste. Sie haben nun die Verbindung zwischen Ihrem Code und Ihrem Interface geschaffen.

In Ihrer Implementierungs-Datei führen Sie nun ein synthesize für WebView und Label durch:

@synthesize webView;
@synthesize label;

So wie beim WebView Interface-Code fügen wir die folgende Funktion hinzu, um alle Webpage Requests abzufangen.

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = [[request URL] absoluteString];
NSArray *urlArray = [url componentsSeparatedByString:@"?"];
NSString *cmd = @"";
NSMutableArray *paramsToPass = nil;
if([urlArray count] > 1) {
NSString *paramsString = [urlArray objectAtIndex:1];
NSArray *urlParamsArray = [paramsString componentsSeparatedByString:@"&"];
cmd = [[[urlParamsArray objectAtIndex:0] componentsSeparatedByString:@"="] objectAtIndex:1];
int numCommands = [urlParamsArray count];
paramsToPass = [[NSMutableArray alloc] initWithCapacity:numCommands-1];
for(int i = 1; i < numCommands; i++){
NSString *aParam = [[[urlParamsArray objectAtIndex:i] componentsSeparatedByString:@"="] objectAtIndex:1];
[paramsToPass addObject:aParam];
}
}
if([cmd compare:@"call"] == NSOrderedSame) {
NSString *message = [[paramsToPass objectAtIndex:0]stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
[label setText:[NSString stringWithFormat:@"You just clicked button #%@", message]];
}
/* Only load the page if it is not an appcall */
NSRange aSubStringRange = [url rangeOfString:@"appcall"];
if(aSubStringRange.length != 0){
NSLog(@"App call found: request cancelled");
return NO;
} else {
return YES;
}
}

Sie sehen, dass der Wert des param Parameters mit einem String verbunden ist, der als Text des Labels benutzt wird. In diesem Szenario schicken alle Video-Overlays ihre eigene ID zur App, wenn auf sie geklickt wird.

Von diesem Punkt an ist Ihre Fantasie frei. Sie können auf den Benutzerinput reagieren und tun, was Sie wollen.

Beispielprojekt herunterladen

Sie finden das Beispielprojekt bei GitHub: InteractiveVideoInterface-Example.

Ein WebView-Element ist eine schnelle und einfache Art, sowohl plattformunabhängige als auch dynamische Inhalte in eine gewöhnliche iOS App zu integrieren. Nun ist es nicht schwierig, das WebView-Element zu benutzen, um Webseiten anzuzeigen, aber die Dinge werden komplizierter, wenn eine Interaktion zwischen der Parent App und der Webseite erforderlich ist. Im Moment gibt es keinen gangbaren Weg für eine Webseite, Nachrichten an den WebView Controller zu senden. Dieser Artikel zeigt, wie man trotz dieser fehlenden Funktion eine Zwei-Wege-Kommunikation implementieren kann. Am Ende des Artikels finden Sie einen Download-Link zu einem vollständigen iOS Beispielprojekt.

JavaScript Code von Ihrer App aus aufrufen

Dieser Teil ist einfach: Nehmen wir an, in Ihrem Projekt benutzen Sie ein WebView-Element für die Anzeige der Seiten. Es gibt eine Methode namens stringByEvaluatingJavaScriptFromString, die die gewünschte Funktionalität bereitstellt. Beispiel:

[myWebView stringByEvaluatingJavaScriptFromString:@"alert('Hello World!');"];

Diese Methode – Sie ahnen es – zeigt eine Warnmeldung, die von Ihrer Webseite geschickt wurde. Auf diese Weise können Sie alle JavaScript-Funktionen aufrufen, die Ihrer Webseite von außen zur Verfügung stehen. Damit ist es ein Kinderspiel, Seiten ohne großen Aufwand zu ändern.

In Ihrer App Funktionen vom WebView-Element aus aufrufen

Der umgekehrte Weg ist weitaus komplizierter. Wir müssen eine Methode finden, wie eine Webseite mit einer App kommunizieren kann. Glücklicherweise bietet das WebView-Element einige hilfreiche Attribute und Ereignisse. Zunächst einmal wird ein Ereignis namens shouldStartLoadWithRequest ausgelöst, wenn eine Seite angefordert wird. Hier ist ein Beispiel für die Implementierung:

-(BOOL)myWebView:(UIWebView*)myWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNaviagtionType)navigationType {
   [...]
}

Was diese Funktion so interessant macht ist, dass sie aufgerufen wird, bevor die Seite tatsächlich geladen wird, und dass sie einen booleschen Wert als Rückgabe erwartet. Der Rückgabewert YES veranlasst das WebView-Element, die gewünschte Seite zu laden, während der Rückgabewert NO keine Seite lädt. Der Plan ist wie folgt: Eine JavaScript-Funktion lädt eine fiktive Seite einschließlich einiger GET-Parameter über window.location. Durch das spezielle URL Stichwort ist die App in der Lage herauszufinden, ob eine echte Seite geladen werden soll oder ob eine In-App Funktion aufgerufen wird (App-Call). An diesem Punkt kann die Funktion entweder den Aufruf stoppen und die Aktion durchführen oder die Seite laden. Die Funktion erledigt also zwei Dinge:

  • Sie prüft, ob ein normaler Link oder ein App-Aufruf angefordert wird und gibt den entsprechenden booleschen Wert zurück.
  • Sie holt die gegebenen Parameter und überprüft, ob sie zu den vordefinierten Aktionen passen.

Basierend auf dem Beispiel von tetontech ist dies der Code, über den wir sprechen:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
 NSString *url = [[request URL] absoluteString];
 NSLog(@"Requesting: %@",url);
 NSArray *urlArray = [url componentsSeparatedByString:@"?"];
 NSString *cmd = @"";
 NSMutableArray *paramsToPass = nil;
 // isolate command and parameters
 if([urlArray count] < 1){
 NSString *paramsString = [urlArray objectAtIndex:1];
 NSArray *urlParamsArray = [paramsString componentsSeparatedByString:@"&"];
 cmd = [[[urlParamsArray objectAtIndex:0] componentsSeparatedByString:@"="] objectAtIndex:1];
 int numCommands = [urlParamsArray count];
 paramsToPass = [[NSMutableArray alloc] initWithCapacity:numCommands-1];
 for(int i = 1; i < numCommands; i++){
 NSString *aParam = [[[urlParamsArray objectAtIndex:i] componentsSeparatedByString:@"="] objectAtIndex:1];
 [paramsToPass addObject:aParam];
 }
 }
 if([cmd compare:@"toggleWorking"] == NSOrderedSame){
 NSLog(@"Turning working indicator...");
 if([UIApplication sharedApplication].networkActivityIndicatorVisible == NO) {
 [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
 NSLog(@"...on");
 } else {
 [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
 NSLog(@"...off");
 }
 } else if([cmd compare:@"logMessage"] == NSOrderedSame) {
 NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingOccurrencesOfString:@"%20" withString:@" "];
 NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
 NSLog(@"Received JS message: %@",message);
 }
 // only load the page if it is the initial index.html file
 NSRange aSubStringRange = [url rangeOfString:@"index.html"];
 if(aSubStringRange.length != 0){
 return YES;
 } else {
 NSLog(@"App call found: request cancelled");
 return NO;
 }
}

Wie schon gesagt, extrahiert und isoliert diese Funktion jeden gegebenen Parameter und Befehl und überprüft mit einigen if-Anweisungen, ob ein Befehl erkannt wird. Wenn das der Fall ist, können zusätzliche Parameter verwendet werden, um eine Aktion auszuführen. In diesem Beispiel gibt es nur zwei mögliche Aktionen:

  • Mit der Anforderung ?cmd=toggleWorking wird in der Tab-Leiste des iPad - abhängig von seinem gegenwärtigen Zustand - ein sich drehendes Rädchen ein- oder ausgeschaltet.
  • Die Aktion logMessage kann aufgerufen werden mit: ?cmd=logMessage¶m=Hello%20World. Es wird eine Nachricht als log-Datei an die Debug-Konsole übermittelt.
NSRange aSubStringRange = [url rangeOfString:@"index.html"];
if(aSubStringRange.length != 0){

Die letzte Überprüfung ist sehr wichtig! Angenommen, Ihre Hauptseite heißt index.html, wird dadurch sicher gestellt, dass die Seite einmal beim Start geladen wird. Weitere Anforderungen sind im Moment blockiert, aber Sie kennen sicherlich andere Ansätze, um sicherzustellen, dass App-Aufrufe nicht geladen werden.

Beispielprojekt herunterladen

Gehen Sie zu GitHub und laden sie das WebViewInterface-Example herunter. Quelle:

Es gibt verschiedene Möglichkeiten, mit einem Service zu kommunizieren. Ein häufig gebrauchter Ansatz ist die Verwendung von Intents, wobei der Service entsprechend der durchgeführten Intent-Action reagieren kann. Dies ist einfach zu realisieren, aber wenn der Service viele verschiedene Operationen definiert, kann der resultierende Code sehr komplex und dadurch nur schwer wartbar werden. Wenn Intents verwendet werden, muss der Entwickler sich außerdem immer darum kümmern, die Parameter zu dem Intent hinzuzufügen; und nachdem das Ergebnis empfangen wurde, müssen die Ergebnis-Parameter aus dem Intent extrahiert werden. Dies führt zu einem erhöhten Programmier-Overhead innerhalb des Clients wann immer eine Remote-Funktionalität aufgerufen wird, was zu fehleranfälligem Code führen kann.

Zur Vermeidung dieser Nachteile kann der integrierten Remote Procedure Call (RPC) Mechanismus von Android verwendet werden. Zur Demonstration der Verwendung von RPCs in Android habe ich ein einfaches Beispiel erstellt. Dieses Beispiel besteht aus zwei Apps, wobei das erste einen Service und das zweite eine Activity enthält. Die Activity verbindet sich mit dem Service mit Hilfe des Android-RPC-Mechanismus und fordert einen string vom Service an.

Bei der Entwicklung von Anwendungen für das Android OS kommt es vor, dass allgemeine Activities oder Services mit wenig Änderungen in weiteren Anwendungen wieder verwendet werden könnten. Da Sie diese Klassen nicht in jedes einzelnen Projekt kopieren möchten – was zu einem kaum handhabbarem Code führen würde – wäre es besser, einen Weg zu finden, den Quellcode dieser Klassen aus weiteren Projekten heraus zu referenzieren. Für Nicht-Android-Anwendungen kann man das Problem lösen, indem man den Code in mehrere Bibliotheken aufteilt, so dass die gewünschte Funktionalität von mehreren Projekten referenziert werden kann. Solange keine Interaktion mit externen Ressourcen nötig ist, ist das auch in Android Projekten ohne weiteres möglich (erstellen Sie ein jar und referenzieren Sie es in den Projekten). Aber wenn Sie mit externen Ressourcen arbeiten, können diese Klassen nicht innerhalb einer Bibliothek verwendet werden, weil das Android SDK keine passenden IDs innerhalb der “R”-Klasse für externe Ressourcen erzeugt, die in einer referenzierten jar-Datei enthalten sind.

In diesem Beitrag möchte ich Ihnen verschiedene Lösungen für dieses Problem zeigen, wodurch Sie vermeiden können, den Quellcode in die Projekte zu kopieren.

Today we are going to take a look at custom entries for a Spinner or ListView. Android allows the developers to create custom layouts for the entries in a Spinner or ListView. Using this mechanism it’s possible to implement highly customized designs for the default GUI elements such as the spinner. For example it’s possible to implement a spinner with entries consisting of an image and of multiple lines of text. In this post I will explain the three steps required to implement this functionality using the Query Contacts App from my previous post. In this App I’ve implemented a Spinner with custom entries to display the contact photo together with the contact name embedded in a single entry. Furthermore each element of the ListView which is used to display the details of the contact contains two lines of text.
Because the implementation of a custom ListView element is very similar to implementing a custom Spinner element I will only explain the Spinner element and mention the differences when implementing a custom entry for a ListView. Of course you can browse or download the source code of the example App at our SVN repository at Google Code (http://code.google.com/p/android-contacts-contract-example/) or use svn to check the project out and your allowed to reuse portions of this code as you wish.

Defining the Layout

The first step when using a custom layout for a Spinner entry is to define a layout xml file. The file must be located in the “res/layout” folder of your project. In our example I’ve created the “res/layout/ spinner_entry_with_icon.xml” layout file which contains a TableLayout with an ImageView and a TextView element.

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TableRow android:id="@+id/tableRow1" android:layout_width="wrap_content" android:layout_height="wrap_content">
<ImageView android:layout_width="32sp" android:src="@drawable/icon" android:id="@+id/spinnerEntryContactPhoto" android:layout_height="32sp" />
<TextView android:textSize="22sp" android:textStyle="bold" android:layout_width="fill_parent" android:id="@+id/spinnerEntryContactName" android:layout_height="fill_parent" android:paddingLeft="5sp" />
</TableRow>
</TableLayout>

Implementing the Adapter

Implementing the Adapter is the next and only step which requires some coding. There are two interfaces available which can be implemented one for Spinner “android.widget.SpinnerAdapter“ and the other for ListView “android.widget.ListAdapter”. But we don’t have to implement all methods defined in this interfaces because there exists an abstract super class which we can use. The “android.widget.BaseAdapter” class provides all core methods required by the two interfaces mentioned above we only have to take care of four abstract methods.

  1. public int getCount()

    This method should return the amount of entries this adapter provides.

  2. public Object getItem(int position)

    This method should return the object associated with the given position.

  3. public long getItemId(int position)

    This method should return the row id for the given position.

  4. public View getView(int position, View convertView, ViewGroup parent)

    This method should return a View which represents this position. We will use our custom layout here in this method.

For the Spinner Adapter in our example I’ve created the class “ContactsSpinnerAdapter” defined as shown below.

public class ContactsSpinnerAdapater extends BaseAdapter implements
SpinnerAdapter {
private final List<SpinnerEntry> content;
private final Activity activity;

public ContactsSpinnerAdapater(List<SpinnerEntry> content,
Activity activity) {
super();
this.content = content;
this.activity = activity;
}
…
}

If you want to implement a custom layout for a ListView just implement the ListAdapter interface instead of the SpinnerAdapter interface.

The data which this Adapter provides is stored in the filed variable content as a default java list. The reference to this list together with the reference to the Activity is provided in the constructor. The reference to the Activity is required later when creating the custom View. The first three methods are very simple to implement. The method getCount will just return the size of our list, getItem returns the item in the list at the given position and getItemId returns the same row number as the given position.

public int getCount() {
return content.size();
}

public SpinnerEntry getItem(int position) {
return content.get(position);
}

public long getItemId(int position) {
return position;
}

The important method is the getView method. I’ve used the default layout inflater to parse the layout file and to make the GUI elements available in the code.

public View getView(int position, View convertView, ViewGroup parent) {
final LayoutInflater inflater = activity.getLayoutInflater();
final View spinnerEntry = inflater.inflate(
R.layout.spinner_entry_with_icon, null);	// initialize the layout from xml

Now we can access the View spinnerEntry in the same way as we are used to from Activitys. Using the findViewById Method we can get the GUI child elements such as the ImageView and the TextView. Furthermore, according to the given position we can get the data from the content of the adapter and use the information stored there to set the data in the GUI elements (setText and setImageBitmap). In the last step we return the spinnerEntry View.

	final TextView contactName = (TextView) spinnerEntry
.findViewById(R.id.spinnerEntryContactName);
final ImageView contactImage = (ImageView) spinnerEntry
.findViewById(R.id.spinnerEntryContactPhoto);
final SpinnerEntry currentEntry = content.get(position);
contactName.setText(currentEntry.getContactName());
contactImage.setImageBitmap(currentEntry.getContactPhoto());
return spinnerEntry;
}

Adding the Adapter to the View

Adding the custom adapter to a Spinner or ListView is the same as with default Adapters like the ArrayAdapter. In our example we add the adapter to our Spinner in the MainActivity:

contactSpinner = (Spinner)findViewById(R.id.contactsSpinner);
final List<SpinnerEntry> spinnerContent = new LinkedList<SpinnerEntry>();
final ContactsSpinnerAdapater adapter = new ContactsSpinnerAdapater(spinnerContent, this);
contactSpinner.setAdapter(adapter);

Result and Summary

The resulting Spinner entries will look like this:

Custom Spinner Entries

You can easily adapt the presented approach to your own needs and thus create custom Spinner and ListView elements that match the needs of your App.