Android: Implementierung In-App-Käufe
Der Google Play Store stellt Android-Nutzer rund 700.000 Anwendungen zum download bereit. Die Anwendungen können dort kostenfrei oder gegen ein kleines Entgeld gekauft und installiert werden. Damit eine kostenfreie Anwendung für den Entwickler gewinnbringend ist, wird oft mit der Hilfe von Google-Adsense Werbung in die Applikation eingefügt. Durch die vom Google Play Store zur Verfügung gestellten In-App-Käufe kann der Entwickler dem Benutzer ermöglichen die oft lästige Werbung durch den Erwerb einer Premium-Version zu entfernen. Des Weiteren können die Anwendungen durch In-App-Käufe (z.B. bei einem Spiel zusätzlich Leben zu erwerben) dem Entwickler einen zusätzlichen Gewinn bringen.
Die Implementierung von In-App-Käufen ist in erster Linie recht simpel, wenn bestimmte Punkte eingehalten werden. Hält man sich nicht an diese Punkte und berücksichtigt bestimmte Tatsache nicht, kann die Implementierung von In-App-Käufen gerne sehr viel Zeit kosten. Ich selbst habe dies im Rahmen eines Kundenprojekts erfahren und möchte an dieser Stelle mit diesem Blogbeitrag andere Entwickler davor bewahren in die gleichen Fallen zu tappen.
Anforderungen
- Google Developer Konto
- Google Play Konto (Darf nicht identisch mit dem Google Developer Konto sein).
Bibliothek in das Projekt einbinden
Um den Billing-Service von Google in einem Projekt verfügbar zu machen, muss die Biliothek zunächst mit Hilfe des Android-SDK-Managers herrunter geladen werden. Das Paket nennt sich Google Play Billing Library und befindet sich unter dem Punkt „Extras“.
Die Datei IInAppBillingService.aidl befindet sich nach dem Download in folgendem Verzeichnis:
<sdk>/extras/google/play_billing/
Die Datei „IInAppBillingService.aidl“ muss jetzt dem Projekt hinzugefügt werden, wobei die Datei unter Eclipse in dem Projekt-Pfad /src in das Package com.android.vending.billing kopiert wird. Ist das Package nicht vorhanden, muss dieses erstellt werden. Unter einem Nicht-Eclipse-Projekt wird die Datei in das Verzeichnis /src/com/android/vending/billing kopiert. Auch hier müssen die Ordner ggf. erstellt werden. Nachdem die Datei kopiert wurde, muss der Build-Vorgang des Projekts einmal druchgeführt werden. Als Nächstes muss dem Projekt die Berechtigung zur Verwendung der IInAppBillingService.aidl zugeteilt werden. Dazu wird in der AndroidManifest.xml folgende Zeile eingefügt:
<uses-permission android:name="com.android.vending.BILLING" />
Das Projekt ist jetzt bereit einen Request an den Google Play Store zu senden und eine Antwort zu erhalten.
Hilfsklassen
Das Beispiel-Projekt der Google Play Billing Library verfügt über Hilfsklassen, welche uns die Implementierung der In-App-Käufe erleichtern. Die 9 Klassen, welche unter folgendem Pfad zu finden sind, kopieren wir in unser Projekt in ein dafür angelegtes Package.
PFAD_SDK/extras/google/play_billing/samples/TrivialDrive/src/com/xample/android/trivialdrivesample/util
Anlegen der Produkte
Damit die Produkte gekauft werden können, müssen diese zunächst der Anwendung in der Google Developer Konsole hinzugefügt werden. Dazu muss die Anwendung in dem Developer Konto erstellt werden (falls noch nicht geschehen). Nach der Auswahl der Anwendung können unter dem Punkt In-App-Produkte neue Produkte für die Anwendung erstellt werden. Bei der Vergabe der Produkt-ID ist darauf zu achten, das diese zu einem späteren Zeitpunkt nicht mehr geändert werden kann. In der API Version 3 stellt Google nur noch ein verwaltetes Produkt und ein Abo zur Verfügung. Bei einem Abo handelt es sich um eine automatisch wiederkehrende Zahlung, welche für normale In-App-Produkte wie z.B. eine Premium-Version oder zusätzliche Leben nicht in Frage kommt. Die API Version 3 unterscheidet bei verwaltet Produkten in verbrauche und nicht verbrauchbare Produkte. Nicht verbrauchbare Produkte können nur einmalig gekauft werden, wobei dessen Erwerb im Google Play Store Konto gespeichert wird. Verbrauchbare Produkte verhalten sich im ersten Moment wie ein Nicht-Verbrauchbares-Produkt bis zu dem Zeitpunkt wo sie verbraucht werden. Nachdem sie verbraucht wurden können die verbrauchbaren Produkte erneut gekauft werden. Wird ein Produkt verbraucht, kann der Erwerb dieses Produkts jedoch nicht mehr nachgewiesen werden. Über diesen Fakt sollte man sich an dieser Stelle im Klaren sein und seine Anwendung dementsprechend anpassen, da Produkte, welche der Benutzer gekauft und nicht verbraucht hat, nach einer Neuinstallation oder Installation auf einem anderen Device wieder zur Verfügung gestellt werden müssen.
Testzugriff auf das Developer Konto
Um über ein Google-Play-Konto den Erwerb der erstellten Produkte zu testen, muss diesem Google-Play-Konto Testzugriff auf das Developer Konto gewährt werden. Die Email-Adresse des für den Testzugang kann im Google Developer Konto unter Einstellungen – Gmail-Konten mit Testzugriff eingegeben werden.
Billing Service Klasse erstellen
Zurück in der Anwendung erstellen wir eine Klasse (Im folgenden Beispiel MyBillingService, welche die Schnittstelle zum Google Play Store darstellt.
Wichtig<br>
Produkt (SKU) Informationen können nur von bereits gekauften Produkte angefragt werden. Da bis dato keine Produkte gekauft wurden erhält man die Information:<br><br>
Querying SKU details.<br>
queryPrices: nothing to do because there are no SKUs.<br><br>
Produkt-Informationen können erst nach dem Erwerb gelistet werden.
In der folgenden Klasse sind notwendige Anpassungen durch Kommentare gekennzeichnet.
public class MyBillingService {
// Debug-Tag for logging
public static final String TAG = MyBillingService.class.getSimpleName();
//Konstante, um festzulegen ob die gekauften Produkte des Benutzers beim ersten
//Start der Anwendung abgefragt wurden
public static final String KEY_CHECK_ITEMS = "check_items";
//Konstante zum Lesen und Speichern via SharedPreferences
public static final String KEY_LIFE_COUNT = "life_count";
private static MyBillingService instance = null;
private Activity activity;
// SKUs der Produkte im App-Store definieren (ID)
public static final String SKU_ONE = "ID Produkt 1";
public static final String SKU_TWO = "ID Produkt 2";
//(arbitrary) Request Code
static final int RC_REQUEST = 10001;
//Status der Produkte
//Jedes Produkt besitzt den Status gekauft oder nicht gekauft
private boolean skuOne;
private boolean skuTwo;
private HashMap<String, Boolean> status;
// Atrribut, welches durch In-App-Produkte beeinlusst wird.
// Zum Beispiel Anzahl der Leben in einem Spiel int maxLife;
// Helper Objekt
private IabHelper mHelper;
public static MyBillingService getInstance(Activity activity) {
if(instance == null) {
instance = new MyBillingService(activity);
}
return instance;
}
private MyBillingService(Activity activity) {
//Status setzen this.activity = activity;
status = new HashMap<>();
status.put(SKU_ONE, false);
status.put(SKU_TWO, false);
//Muss auf das jeweilige Projekt angepasst werden.
maxLife = Integer.valueOf(MyApplication.getInstance().readFromPreferences(KEY_LIFE_COUNT, "2"));
//Befindet sich im Developer Konto nach der Auswahl der Anwendung unter Dienste & APIs
String base64EncodedPublicKey = "API KEY HIER EINFÜGEN";
// Erstellen des Helpers
Log.d(TAG, "Creating IAB helper.");
mHelper = new IabHelper(this.activity, base64EncodedPublicKey);
//Schaltet das Debuggen ein. In einer Release-Version sollte hier false übergeben werden.
mHelper.enableDebugLogging(true);
Log.d(TAG, "Starting setup.");
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
Log.d(TAG, "Setup finished.");
if (!result.isSuccess()) {
// Problemfall
complain("Problem setting up in-app billing: " + result);
return;
}
// Wurde das Helper-Objekt bereits zerstört, beenden
if (mHelper == null) return;
// IAB wurde initialisiert. Anfragen welche Produkte der Benutzer besitzt.
Log.d(TAG, "Setup successful. Querying inventory.");
mHelper.queryInventoryAsync(mGotInventoryListener);
}
});
}
// Listener, welcher ausgerufen wird, wenn die Liste mit gekauften Produkten erfolgreich angefragt wurde
private IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
Log.d(TAG, "Query inventory finished.");
if (mHelper == null) return;
// Meldung im Fehlerfall
if (result.isFailure()) {
complain("Failed to query inventory: " + result);
return;
}
Log.d(TAG, "Query inventory was successful.");
// Welche Produkte hat der Benutzer bereits gekauft?
Purchase purchase;
for(Map.Entry<String, Boolean> product: status.entrySet()) {
purchase = inventory.getPurchase(product.getKey());
status.put(product.getKey(), (purchase != null && verifyDeveloperPayload(purchase)));
Log.d(TAG, "User has " + product.getKey() + (product.getValue() ? " bought" : " not bought"));
}
//Muss angepasst werden: Beim ersten Start der Anwendung
//müssen alle gekauften Artikel mit ihren Auswirkungen berücksichtigt werden
if(!Boolean.valueOf(MyApplication.getInstance().readFromPreferences(KEY_CHECK_ITEMS, "false")))
{
for(Map.Entry product: status.entrySet()) {
switch (product.getKey()) {
case SKU_ONE:
if(product.getValue()) {
//TODO
}
}
}
saveData();
Log.i(TAG, "Initialzie in-app-purchase");
MyApplication.getInstance().saveToPreferences(KEY_CHECK_ITEMS, String.valueOf(true));
}
Log.d(TAG, "Initial inventory query finished; enabling main UI.");
}
};
public HashMap getStatus() {
return status;
}
public void buyProdukt(String id) {
Log.d(TAG, "Buy Produkt " + id);
if (status.get(id)) {
complain("Product already bought!");
return;
}
Log.d(TAG, "Launching purchase flow for: " + id);
/* TODO: for security, generate your payload here for verification. See the comments on
* verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use
* an empty string, but on a production app you should carefully generate this. */
String payload = "";
mHelper.launchPurchaseFlow(activity, id, RC_REQUEST,
mPurchaseFinishedListener, payload);
}
//Funktion muss in der Activity aufgerufen werden in der die Klasse zuerst aufgerufen wird.
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);
if (mHelper == null) return false;
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
return false;
}
else {
Log.d(TAG, "onActivityResult handled by IABUtil.");
return true;
}
}
/** Verifizieren das es sich um einen gültigen Kauf handelt */
boolean verifyDeveloperPayload(Purchase p) {
String payload = p.getDeveloperPayload();
/*
* TODO: An dieser Stelle muss verifiziert werden, dass es sich um einen gültigen Kauf handelt.
* Der String (getDeveloperPayload) ist der gleiche, welcher beim Kauf übergeben wird.
*
* Einen zufälligen String zur Laufzeit zu generieren ist hierbei wenig zielführen,
* da sichergestellt werden muss, dass der Kauf von einem Gerät durchgeführt werden
* kann und durch ein andere verifiziert werden kann. Zu diesem Punkt wird bald ein
* weiterer Blogbeitrag folgen. Für Testzwecke reicht hier die Rückgabe von true aus
*
* Vor der Veröffentlichung im App-Store sollte diese Funktion auf jeden Fall implementiert werden
*/
return true;
}
// Listener, welcher aufgerufen wird, wenn der Kauf erfolgreich war
private IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
// if we were disposed of in the meantime, quit.
if (mHelper == null) return;
if (result.isFailure()) {
//complain("Error purchasing: " + result);
return;
}
if (!verifyDeveloperPayload(purchase)) {
complain("Error purchasing. Authenticity verification failed.");
return;
}
//Was soll passieren, wenn der Benutzer ein Produkt erworben hat?
switch (purchase.getSku()) {
case SKU_ONE:
//TODO
saveData();
break;
}
saveData();
Log.d(TAG, "Purchase successful. Bought " + purchase.getSku());
//Handelt es sich bei SKU ONE um ein verbrauchbaren Produkt?
//Wenn ja, dann dies an dieser Stelle verbraucht werden damit der
//Benutzer das Produkt erneut kaufen kann
if (purchase.getSku().equals(SKU_ONE)) {
Log.d(TAG, "Purchase is sku one. Starting sku one consumption.");
mHelper.consumeAsync(purchase, mConsumeFinishedListener);
}
}
};
// Wird aufgerufen, sobald der Verbrauch eines Produkts erfolgreich war.
private IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
public void onConsumeFinished(Purchase purchase, IabResult result) {
Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);
if (mHelper == null) return;
// An dieser Stelle wissen wir, dass es sich um SKU_ONE handelt
// da nur dieses Produkt verbraucht werden kann.
// Sind mehere verbrauchbare Produkte vorhanden, muss dies an dieser Stelle überprüft werden
if (result.isSuccess()) {
// Was soll gesehen, wenn der Benutzer SKU_ONE verbraucht hat?
Log.d(TAG, "Consumption successful. Provisioning.");
//TODO
saveData();
}
else {
complain("Error while consuming: " + result);
}
Log.d(TAG, "End consumption flow.");
}
};
// Methode muss in der StartActivity aufgerufen werden
public void destroy() {
// Sehr wichtig:
Log.d(TAG, "Destroying helper.");
if (mHelper != null) {
mHelper.dispose();
mHelper = null;
}
}
void complain(String message) {
Log.e(TAG, "**** TrivialDrive Error: " + message);
alert("Fehler: " + message);
}
void alert(String message) {
AlertDialog.Builder bld = new AlertDialog.Builder(activity);
bld.setMessage(message);
bld.setNeutralButton("OK", null);
Log.d(TAG, "Showing alert dialog: " + message);
bld.create().show();
}
void saveData() {
/*
* Speichern der Daten in der Applikation
* Eine einfache Möglcihkeit ist die Verwendung von SharedPreferences.
* Bei einer veröffentlichten App sollten diese Daten verschlüsselt werden,
* da sonst unbefugter Zugriff möglich ist.
* Mehr dazu in den Anmerkungen.
*/
}
}
Mit der Anwendung verknüpfen
Die zuvor erstellte Klasse muss in der MainActivity der Anwendung initialisiert werden. Dazu wird die folgende Codezeile im Konstruktor verwendet:
billing = MyBillingService.getInstance(this);
Die Methoden unserer Billing Klasse onDestroy() und onActivityResult() müssen jetzt mit der MainActivity verknüpft werden. Dazu wird folgender Code in die MainActivity des Projekts eingefügt.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data); if(!billing.onActivityResult(requestCode, resultCode, data)) { super.onActivityResult(requestCode, resultCode, data); } else { Log.d(TAG, "onActivityResult handled by IABUtil."); } }
@Overridee protected void onDestroy() { billing.destroy(); super.onDestroy(); }
Unsere Billing Klasse ist nun mit der Anwendung verknüpft und kann in jedem Fragment aufgerufen werden.
Produkte kaufen
Produkte können nun in einem dafür zuständigen Fragment gekauft werden. Beispielweise kann der Erwerb des ersten Produkts durch folgende Codezeile ausgelöst werden.
MyBillingService.getInstance(getActivity()).buyProdukt(MyBillingService.SKU_ONE);
Testen wir diesen Vorgang nun in unserer lokalen Testumgebung wird die Anfrage der Produkte fehlschlagen und wir erhalten folgende Meldung.
Dieser Fehler ist darauf zurück zu führen das es sich bei der APK, welche den App-Store nach Podukten anfragt, nicht von uns signiert wurde.
APK signieren
Die Signierung einer APK erfolgt unter Android Studio problemlos unter dem Menüpunkt Build und Generate signed APK. Die Generierung einer signierten APK ist auf der Google Developer Seite ausführlich beschrieben.
Hochladen der APK
Die generierte APK kann jetzt im Google Developer Konto in die Alpha- oder Beta-Phase hochgeladen werden. Wichtig dabei ist, das diese veröffentlicht und nicht nur als Entwurf gespeichert wird. Nach dem Hochladen der APK dauert es in der Regel 2-3 Stunden bis diese den Testern zur Verfügung gestellt wird. Auf dem aktuellen Stand haben wir jedoch noch keinen Alpha- Oder Beta-Test eingerichtet.
Alpha- oder Beta-Test einrichten
Damit unsere APK an Testnutzer ausgeliefert werden kann muss ein Alpha- oder Beta-Test eingerichtet werden. Die Email-Adresse unseres Google Play Kontos muss in die Liste der Tester eingetragen werden. Testbenutzer müssen die Einladung zum Alpha- oder Beta-Test über den unten stehenden Link annehmen. Direkt nach dem Hochladen der APK ist dieser Link ungütlig und erhält nach 2-3 Stunden Gültigkeit. Weitere Informationen zum Testen von APKs befinden sich auf der Google Developer Seite.
Wichtig<br>
Es muss bereits eine aktive Version der Anwendung im Release sein, da die In-App-Produkte sonst nicht aktiviert werden und diese nicht gekauft werden können. Daher empfiehlt es sich zunächst eine Version ohne In-App-Käufe zu veröffentlichen und die In-App-Käufe dann in der nächsten Version zu implementierten.
Ist bereits eine Version der Anwendung im Release wird nach 2-3 Stunden ein Update an die Benutzer, welche sich in der Liste des Alpha- oder Beta-Tests befinden, ausgeliefert. Die APK der Alpha oder Beta-Phase muss eine höhere Versionsnummer als die Release Version haben, da immer die Version mit der höchsten Versions-Nummer ausgeliefert wird.
Testen der In-App-Käufe
Nachdem die neue Version auf den Devices des Testbenutzers aufgespielt wurde, kann der In-App-Kauf getestet werden. Da wir unser Testkonto zuvor mit Testzugriff versehen haben, wird uns für den Erwerb der Produkte kein Geld in Rechnung gestellt.
Anmerkungen
- Produkt-Informationen können erst nach dem Erwerb gelistet werden
- Kauf der Produkte ist nur über eine signierte APK möglich
- Werden die Informationen über In-App-Käufe via Applikation-Preferences in der Anwendung gespeichert sollten diese verschlüsselt werden. Mehr dazu.
- API-Key sollte aus Sicherheitsgründen zur Laufzeit zusammengesetzt werden
- Produkte werden erst nach dem Release der Anwendung aktiviert
Abschluss
Wie zu sehen ist, ist die Implementierung von In-App-Käufen kein Hexenwerk, solange bestimmte Punkte beachtet werden. Bei Fehlern, wie zum Beispiel das Anfragen von Produktinformationen von nicht erworbenen Produkten, hält man sich unter Umständen sehr lange auf, wenn man über bestimmte Fakten nicht Bescheid weiß.