2010-08-30

Ein bischen Apps-Arithmetik

Als Bummler zwischen den Welten lese ich selbstverständlich auch ein Weblog rund um das Apple-Ökosystem: theAppleBlog. Dort ist zu lesen, dass Cupertinos AppStore derzeit eine Viertel Millionen Apps anbietet – eine ohne Zweifel beeindruckende Zahl. Der Weblog-Beitrag bezieht sich dabei übrigens auf Auswertungen der Site 148Apps.biz. Nun kann man dem Beitrag entnehmen, dass derzeit etwa 50.000 Entwickler knapp 630 Anwendungen pro Tag hochladen. Rein rechnerisch ergibt das 0,0126 Apps pro Entwickler und Tag. Nochmal umgerechnet vergehen also etwa 80 Tage (1 / 0,0126), bis derselbe Entwickler wieder eine Anwendung heraus bringt. Was halten Sie davon? Wie lange würden Sie für den Bau einer App veranschlagen? Schreiben Sie mir…

2010-08-28

AccountManager: dem Geheimnis auf der Spur

Lange Zeit hatten Anwendungsentwickler das Problem, vom Benutzer Dinge abfragen zu müssen, die dem System (seinem Handy) eigentlich bekannt sind: zum Beispiel die Daten zu seinem Google Konto. Schon seit Eclair gibt es deshalb die Klasse AccountManager, die als zentrale Speicherstelle der vom Benutzer angemeldeten Onlinekonten fungiert. Die Idee ist, sich vom AccountManager einfach ein so genanntes Token geben zu lassen, um dann mit diesem Token auf einen Webservice zuzugreifen. Damit müsste es dann eigentlich ganz einfach sein, zum Beispiel mit Google Contacts oder Google Calendar zu reden. …oder…?

Wer sich mal den Spaß gemacht hat, das Web daraufhin abzusuchen, wird wahrscheinlich zu dem Schluss kommen, dass es so ohne weiteres offenbar doch nicht geht. …geht ja wohl! Wie üblich bei mir, zunächst der Quelltext:

package com.thomaskuenneth;

import java.io.InputStream;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class TKSimpleTasks extends Activity {

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

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    AccountManager am = AccountManager.get(this);
    Account[] accounts = am.getAccountsByType("com.google");
    if (accounts.length < 1) {
      finish();
    }
    String authTokenType = "cl";
    Bundle options = null;
    am.getAuthToken(accounts[0], authTokenType, options, this,
        new AccountManagerCallback<Bundle>() {

          public void run(AccountManagerFuture<Bundle> arg0) {
            try {
              Bundle b = arg0.getResult();
              String token = b
                  .getString(AccountManager.KEY_AUTHTOKEN);
              Log.d(TAG, token);

              HttpGet get = new HttpGet(
                  "https://www.google.com/calendar/feeds/default/owncalendars/full");
              get.addHeader("Authorization", "GoogleLogin auth="
                  + token);

              HttpClient httpclient = new DefaultHttpClient();
              HttpResponse r = httpclient.execute(get);
              InputStream is = r.getEntity().getContent();
              StringBuilder sb = new StringBuilder();
              int ch;
              while ((ch = is.read()) != -1) {
                sb.append((char) ch);
              }

              ((TextView) findViewById(R.id.tv)).setText(sb
                  .toString());

            } catch (Throwable thr) {
              Log.e(TAG, thr.getMessage(), thr);
            }
          }
        }, null);
  }
}

Zunächst holt man sich eine Instanz des AccountManagers:

AccountManager am = AccountManager.get(this);

Anschließend sucht man nach geeigneten Konten:

Account[] accounts = am.getAccountsByType("com.google");

Das in der Einleitung angesprochene Token erhalten Sie durch Aufruf der Methode getAuthToken. Der eigentlich spannende Teil diesbezüglich steht hier:

Bundle b = arg0.getResult();
String token = b.getString(AccountManager.KEY_AUTHTOKEN);

Sie werden jetzt vielleicht sagen, dass Sie ähnliche Quelltextfragmente recht oft finden. Das stimmt. Aber ich behaupte, kaum jemand schreibt, was es mit dem ominösen authTokenType auf sich hat. Damit steuern Sie, auf welchen Teil oder Bereich eines Google Kontos Sie Zugriff haben möchten. Das Beispiel liest den Kalender aus. Das eigentliche Lesen ist hier nur skizzenhaft ausprogrammiert. Ganz wichtig – und das werden Sie sehr selten finden – ist das Setzen des Tokens im Header des HTTP-GETs.

Bitte beachten Sie noch, dass Ihre Anwendung die Rechte GET_ACCOUNTS, USE_CREDENTIALS und INTERNET braucht. Haben Sie Fragen, Anregungen oder Kommentare? Schreiben Sie mir.

2010-08-22

Netzwerk-Status abfragen

Android bietet eine ganze Reihe von Auskunftsfunktionen. Heute zeige ich Ihnen, wie Sie den Netzwerk-Status ermitteln. Wie üblich, zunächst das – diesmal extrem kurze – Progrämmchen:

package com.thomaskuenneth;

import android.app.Activity;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.widget.TextView;

public class TestActivity extends Activity {

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    ConnectivityManager connMgr =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

    NetworkInfo mobile = connMgr
        .getNetworkInfo(ConnectivityManager.TYPE_MOBILE);

    String roaming =
"roaming ist " + (mobile.isRoaming() ? "ein" : "aus");

    ((TextView) findViewById(R.id.view)).setText(roaming);
  }
}

Das Ermitteln einer ConnectivityManager-Instanz verläuft nach dem Ihnen bereits gut bekannten Muster. Mit getSystemService wird ein bestimmter Service abgefragt und das Ergebnis auf den gewünschten Typ gecastet. Die Methode getNetworkInfo liefert Informationen zu Netzwerktypen. Im konkreten Fall (ConnectivityManager.TYPE_MOBILE) möchte ich wissen, ob im aktuell verwendeten Netz geroamt wird. Hierfür gibt es eine eigene Methode, isRoaming. Damit das Ganze auch funktioniert, muss die App das Recht android.permission.ACCESS_NETWORK_STATE anfordern.

2010-08-20

Schon gesehen? - Zauberhaftes Unboxing

In meinem Bericht über das iPad hatte ich geschrieben, dass ich auf ein Unboxing verzichte, weil es das tausendfach im Netz gibt. Heute bin ich auf eine besonders charmante Variante gestoßen. Aber sehen Sie selbst...

2010-08-19

Lichtgestalten – wieder Spaß mit Sensoren

Wenn ich es sogar mit dem folgenden Screenshot schaffe, Ihre Aufmerksamkeit zu erregen, kann ich mich wirklich glücklich schätzen… :-)

Screenshot des Testprogramms für den Helligkeitssensor  
Screenshot des Testprogramms für den Helligkeitssensor

In dieser Folge der Reihe Spaß mit Sensoren zeige ich Ihnen, wie Sie den Helligkeitssensor von Androiden auslesen. Zunächst der Quelltext der Anwendung:

package com.thomaskuenneth;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;

public class Test extends Activity {

  private TextView view;

  private SensorManager manager;
  private SensorEventListener listener;
  private Sensor sensor;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    view = (TextView) findViewById(R.id.view);

    manager = (SensorManager) getSystemService(SENSOR_SERVICE);
    listener = new SensorEventListener() {

      public void onAccuracyChanged(Sensor sensor, int accuracy) {
      }

      public void onSensorChanged(SensorEvent event) {
        if (event.values.length > 0) {
          float light = event.values[0];
          view.setText(Float.toString(light));
        }
      }

    };
   
    sensor = manager.getDefaultSensor(Sensor.TYPE_LIGHT);
    if (sensor != null) {
      manager.registerListener(listener, sensor,
          SensorManager.SENSOR_DELAY_NORMAL);
    } else {
      view.setText("keinen Helligkeitssensor gefunden");
    }
  }

  @Override
  protected void onDestroy() {
    if (sensor != null) {
      manager.unregisterListener(listener);
    }
    super.onDestroy();
  }
}
Java2html

Ein Großteil des Listings ist bereits bekannt; zunächst wird eine Referenz auf einen SensorManager besorgt und im Anschluss ein SensorEventListener registriert. Der einzige Unterschied zum letzten Beispiel besteht darin, dass ich die Methode getDefaultSensor aufrufe. Das ist einerseits praktisch, weil ich nicht wie bei getSensorList über die Liste iterieren muss. Um zu prüfen, ob ein entsprechender Sensor (im Beispiel ist dies Sensor.TYPE_LIGHT) vorhanden ist, reicht also eine Abfrage auf null. Allerdings sagt die Doku, dass es sich bei dem gelieferten Sensor um einen zusammengesetzten oder kombinierten Sensor handeln kann, dessen Daten möglicherweise gemittelt oder gefiltert wurden. Ob das in der Praxis tatsächlich Auswirkungen hat, habe ich beim Helligkeitssensor meines Nexus One nicht weiter überprüft.

Übrigens hat Wikipedia eine schöne Übersicht, die Werte auf Lichtverhältnisse abbildet. Erweitern Sie doch mein Progrämmchen und machen es zu einer hübschen Helligkeitssensor-App (sofern es die nicht schon ohnehin gibt :-)).

2010-08-10

Rasender Reporter

Heute möchte ich Ihnen zeigen, wie leicht sich Androiden in mobile Diktiergeräte verwandeln lassen. Dies bedeutet:

  • Aufnehmen von Geräuschen mit dem eingebauten Mikrofon
  • Abspielen der Aufnahme

Der erste Teil, das Aufnehmen, ist sehr schnell erledigt:

protected void recordToFile() {
  recorder = new MediaRecorder();
  recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
  recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
  recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
  File f = new File(Environment.getExternalStorageDirectory(),
      "recording.3gp");
  try {
    f.createNewFile();
    recorder.setOutputFile(filename = f.getAbsolutePath());
    recorder.prepare();
    recorder.start();
  } catch (IOException e) {
    Log.d(TAG, "could not start recording", e);
  }
}
Java2html

Ruft man diese Methode auf, wird das durch das Mikrofon aufgenommene Audiosignal in eine Datei auf der externen Speicherkarte geschrieben.

Stellen Sie sich eine Anwendung mit zwei Schaltflächen zum Starten und Stoppen der Aufnahme vor. Dann könnte das reagieren auf Klicks auf den Start-Knopf so aussehen:

startRecording.setOnClickListener(new View.OnClickListener() {
  public void onClick(View v) {
    startRecording.setEnabled(false);
    stopRecording.setEnabled(true);
    stopRecording.requestFocus();
    recordToFile();
  }
});
Java2html

Das Anklicken des Stopp-Knopfs beendet die Aufnahme und startet die Wiedergabe. Dies widerum könnte man so realisieren:

stopRecording.setOnClickListener(new View.OnClickListener() {
  public void onClick(View v) {
    stopRecording.setEnabled(false);
    // die Aufnahme stoppen
    recorder.stop();
    recorder.release();
    // die Datei abspielen
    playAudioFile(filename);
  }

});
Java2html

Bleibt eigentlich nur noch ein Blick auf die Abspielroutine:

protected void playAudioFile(String filename) {
  final MediaPlayer player = new MediaPlayer();
  player.setOnCompletionListener(new OnCompletionListener() {

    public void onCompletion(MediaPlayer player) {
      player.release();
      startRecording.setEnabled(true);
      startRecording.requestFocus();
    }
  });
  try {
    player.setDataSource(filename);
    player.prepare();
    player.start();
  } catch (Throwable thr) { // IllegalArgumentException,
    // IllegalStateException, IOException
    Log.d(TAG, "could not play audio", thr);
  }
}
Java2html

Diese Methode bringt insofern ein klein wenig Würze in die doch sehr einfachen Tätigkeiten, als sie einen Listener registriert, der nach dem Ende des Abspielvorgangs in Aktion tritt. Er gibt die nun nicht mehr benötigten Ressourcen frei und reaktiviert eine Schaltfläche – die Starttaste sollte natürlich während einer Wiedergabe nicht angeklickt werden können.

2010-08-08

Hands on - iPad

Dieses Weblog dreht sich vor allem um Java und Android. Gelegentlich müssen Sie, liebe Leserin, lieber Leser, aber auch mit Posts zu anderen mobilen Geräten rechnen. Heute ist so ein Tag. :-)

Dieser Beitrag handelt nämlich von meinem neuesten Gadget, ein iPad 32 GB, nur W-LAN. Meine Freunde und Kollegen wissen, dass ich so einen Kauf nie kategorisch ausgeschlossen habe, insofern sollte der Schock nicht allzu groß ausfallen. :-) Die Frage, warum man sich als Android-Freak ein iPad holt, nagt vielleicht aber doch. Dabei ist die Antwort hierauf vergleichsweise einfach. Nichts ist hinderlicher für gute Arbeit als Scheuklappen. Um die Konzepte der "eigenen" Plattform wirklich zu verstehen, muss man auch andere kennen. Und - Apple liefert in vielen Punkten verdammt überzeugende Konzepte.

So, das obligatorische Unboxing wurde unzählige Male dokumentiert, so dass dieser Post gerne darauf verzichten kann. Ich möchte mich vielmehr darauf konzentrieren, Ihnen die ersten Eindrücke zu vermitteln. Die Apple-typisch sehr schöne Verpackung enthält nur ganz wenige Teile - das iPad, ein USB-Kabel, ein Netzteil und ein kleines Mäppchen mit Apple-Stickern und gaaaaaaanz wenig Dokumentation. Nach dem Anschließen an den Rechner möchte das Gerät aktiviert und registriert werden, wobei die Registrierung mit einem "unbekannten Fehler" scheiterte. ...naja, dann eben keine Werbe-Mails... Nach dem fast schon obligatorischen Firmware-Update hat mein Mac meine iTunes-Bibliothek auf das iPad geschaufelt.

Da ich bisher kein iOS-Gerät benutzt habe, war das Entdecken des Systems richtig aufregend und spannend. Es macht Spass, sozusagen einen Deckel aufzumachen, um zu sehen, was sich in einer Kiste befindet. Das Einstellen des iPads, das Anpassen an die eigenen Bedürfnisse, das Konfigurieren von Browser und E-Mail-Client gehen Apple-typisch flott und elegant von statten. Nach wenigen Minuten bin ich mit dem heimischen Funknetz verbunden und lese die Nachrichten meines Google Mail-Kontos. Auch der Erstkontakt mit den mobilen Versionen der Apple-Software (iTunes, App-Store, ...) verläuft freundlich. Und dann die ersten Apps - Google Earth, Kindle for iPad, ein Autorennen, das Wort-Knobelspiel Bookworm - was ich dem iPad auch vorsetze, es geht souverän damit um. Die Reaktionsgeschwindigkeit ist hoch, das System antwortet prompt, Animationen sind praktisch immer butterweich.

Die Handhabung... Flach auf den Tisch legen macht definitiv keinen Spaß, ein Dock oder ein Ständer stand noch nicht zur Verfügung. Also musste ich das gute Stück in der Hand halten. Das fühlt sich an sich ganz gut an, das Gewicht des Geräts zieht aber nach einer geraumen Zeit doch merklich "nach unten". Die Konsequenz ist, dass man regelmäßig die Haltung wechselt, mal hat man das iPad in der linken, dann wieder in der rechten Hand. Tolles Display hin oder her, auf diese Weise ein Buch lesen möchte ich definitiv nicht. Da ist mir mein - sorry Apple - Kindle merklich lieber.

Ich habe mir das iPad als komfortables Surfbrett für zuhause geholt. Hier macht es einen exzellenten Job. Auch als "Monster iPod" hat es ohne Frage Klasse, aber fürs Video Gucken muss in jedem Fall ein Dock her. Ist das iPad ein Gerät für draußen? Ganz ehrlich, ich würde es nicht ständig mit mir herum tragen wollen.

2010-08-06

Hands on – UI Stencils für Android

Vor einiger Zeit hatte ich in meinem Eintrag Scribbeln in seiner schönsten Form über Zeichenschablonen für das UI-Prototyping von iPhone-Anwendungen berichtet. Einer meiner Leser hatte mich vor kurzem (leider nur per E-Mail :-)) darauf aufmerksam gemacht, dass es diese Schablonen nun auch für Android gibt. Da ich neugierig bin, habe ich mir das Android Stencil Kit aus den USA kommen lassen. Zunächst das obligatorische Unboxing:

Die noch ungeöffnete Verpackung
Die noch ungeöffnete Verpackung

Der Inhalt des Umschlags
Der Inhalt des Umschlags

Schablone, Bleistift und Sticker
Schablone, Bleistift und Sticker

Bestellt habe ich das Ganze am 27. Juli. Die Zeit, die die Schablone über den großen Teich gebraucht hat, ist also recht kurz gewesen. Auch der Zustellprozess war absolut reibungslos – kein Gang zum Zollamt nötig.

Die Schablone selbst macht einen wertigen Eindruck, ist allerdings recht dünn. Ob sie eine raue Behandlung während des Transports schadlos überstanden hätte, vermag ich nicht zu sagen. Für die tägliche Arbeit sollte sie aber gut gerüstet sein. Auch der mitgelieferte Druckbleistift mit Radierer (ZEBRA Modell #2) gefällt. Seine Optik ist einem echten Bleistift nachempfunden – ich war ehrlich gesagt für einen winzigen Augenblick drauf und dran, ihn anzuspitzen. :-) Seine Mine ist 0,7 mm dick/dünn.

Ein erster Versuch
Ein erster Versuch

Das Bild oben zeigt meine erste Skizze. Offen gesagt musste ich etwas üben, bis die Mine des Stifts zielsicher die Aussparungen in der Schablone traf – wobei ich dies vor allem meinem zeichnerischen Nicht-Talent zuschreibe. :-) Nach etwas Übung sollte das Scribbeln aber flott von der Hand gehen. Empfehlen möchte ich, sich von der Homepage des Herstellers eine Vorlage in Gestalt eines PDFs herunter zu laden, dass schemenhaft ein fiktives Gerät vorgibt und so das Zeichnen wesentlich erleichtert.

Ausgedruckte Vorlage
Ausgedruckte Vorlage

Die Vorlage enthält Felder, um beispielsweise Projektnamen und Datum zu erfassen.

Die Schablone “simuliert” einen Bildschirm mit 480 mal 800 Punkten auf etwa 7,3cm mal 11cm. Einige Bildschirmsymbole, beispielsweise die Zoom-Schaltflächen werden durch mehrere Aussparungen auf der Schablone zusammengesetzt. Alles in allem macht die Arbeit mit diesem Hilfsmittel sehr großen Spaß; die Ergebnisse wirken, zumindest aus der Hand des Laien, wesentlich ästhetischer als reine Von-Hand-Zeichnungen. Seinen Preis ist das Paket meiner Meinung nach in jedem Fall wert.