/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.robotmedia.billing;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.android.vending.billing.IMarketBillingService;
import net.robotmedia.billing.utils.Compatibility;
import java.util.LinkedList;
import static net.robotmedia.billing.BillingRequest.*;
public class BillingService extends Service implements ServiceConnection {
private static enum Action {
CHECK_BILLING_SUPPORTED, CHECK_SUBSCRIPTION_SUPPORTED, CONFIRM_NOTIFICATIONS, GET_PURCHASE_INFORMATION, REQUEST_PURCHASE, REQUEST_SUBSCRIPTION, RESTORE_TRANSACTIONS
}
private static final String ACTION_MARKET_BILLING_SERVICE = "com.android.vending.billing.MarketBillingService.BIND";
private static final String EXTRA_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD";
private static final String EXTRA_ITEM_ID = "ITEM_ID";
private static final String EXTRA_NONCE = "EXTRA_NONCE";
private static final String EXTRA_NOTIFY_IDS = "NOTIFY_IDS";
private static LinkedList<BillingRequest> mPendingRequests = new LinkedList<BillingRequest>();
private static IMarketBillingService mService;
public static void checkBillingSupported(Context context) {
final Intent intent = createIntent(context, Action.CHECK_BILLING_SUPPORTED);
context.startService(intent);
}
public static void checkSubscriptionSupported(Context context) {
final Intent intent = createIntent(context, Action.CHECK_SUBSCRIPTION_SUPPORTED);
context.startService(intent);
}
public static void confirmNotifications(Context context, String[] notifyIds) {
final Intent intent = createIntent(context, Action.CONFIRM_NOTIFICATIONS);
intent.putExtra(EXTRA_NOTIFY_IDS, notifyIds);
context.startService(intent);
}
private static Intent createIntent(Context context, Action action) {
final String actionString = getActionForIntent(context, action);
final Intent intent = new Intent(actionString);
intent.setClass(context, BillingService.class);
return intent;
}
private static final String getActionForIntent(Context context, Action action) {
return context.getPackageName() + "." + action.toString();
}
public static void getPurchaseInformation(Context context, String[] notifyIds, long nonce) {
final Intent intent = createIntent(context, Action.GET_PURCHASE_INFORMATION);
intent.putExtra(EXTRA_NOTIFY_IDS, notifyIds);
intent.putExtra(EXTRA_NONCE, nonce);
context.startService(intent);
}
public static void requestPurchase(Context context, String itemId, String developerPayload) {
final Intent intent = createIntent(context, Action.REQUEST_PURCHASE);
intent.putExtra(EXTRA_ITEM_ID, itemId);
intent.putExtra(EXTRA_DEVELOPER_PAYLOAD, developerPayload);
context.startService(intent);
}
public static void requestSubscription(Context context, String itemId, String developerPayload) {
final Intent intent = createIntent(context, Action.REQUEST_SUBSCRIPTION);
intent.putExtra(EXTRA_ITEM_ID, itemId);
intent.putExtra(EXTRA_DEVELOPER_PAYLOAD, developerPayload);
context.startService(intent);
}
public static void restoreTransations(Context context, long nonce) {
final Intent intent = createIntent(context, Action.RESTORE_TRANSACTIONS);
intent.setClass(context, BillingService.class);
intent.putExtra(EXTRA_NONCE, nonce);
context.startService(intent);
}
private void bindMarketBillingService() {
try {
final boolean bindResult = bindService(new Intent(ACTION_MARKET_BILLING_SERVICE), this, Context.BIND_AUTO_CREATE);
if (!bindResult) {
Log.e(this.getClass().getSimpleName(), "Could not bind to MarketBillingService");
}
} catch (SecurityException e) {
Log.e(this.getClass().getSimpleName(), "Could not bind to MarketBillingService", e);
}
}
private void checkBillingSupported(int startId) {
final String packageName = getPackageName();
final CheckBillingSupported request = new CheckBillingSupported(packageName, startId);
runRequestOrQueue(request);
}
private void checkSubscriptionSupported(int startId) {
final String packageName = getPackageName();
final CheckSubscriptionSupported request = new CheckSubscriptionSupported(packageName, startId);
runRequestOrQueue(request);
}
private void confirmNotifications(Intent intent, int startId) {
final String packageName = getPackageName();
final String[] notifyIds = intent.getStringArrayExtra(EXTRA_NOTIFY_IDS);
final ConfirmNotifications request = new ConfirmNotifications(packageName, startId, notifyIds);
runRequestOrQueue(request);
}
private Action getActionFromIntent(Intent intent) {
final String actionString = intent.getAction();
if (actionString == null) {
return null;
}
final String[] split = actionString.split("\\.");
if (split.length <= 0) {
return null;
}
return Action.valueOf(split[split.length - 1]);
}
private void getPurchaseInformation(Intent intent, int startId) {
final String packageName = getPackageName();
final long nonce = intent.getLongExtra(EXTRA_NONCE, 0);
final String[] notifyIds = intent.getStringArrayExtra(EXTRA_NOTIFY_IDS);
final GetPurchaseInformation request = new GetPurchaseInformation(packageName, startId, notifyIds);
request.setNonce(nonce);
runRequestOrQueue(request);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IMarketBillingService.Stub.asInterface(service);
runPendingRequests();
}
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
// This is the old onStart method that will be called on the pre-2.0
// platform. On 2.0 or later we override onStartCommand() so this
// method will not be called.
@Override
public void onStart(Intent intent, int startId) {
handleCommand(intent, startId);
}
// @Override // Avoid compile errors on pre-2.0
public int onStartCommand(Intent intent, int flags, int startId) {
handleCommand(intent, startId);
return Compatibility.START_NOT_STICKY;
}
private void handleCommand(Intent intent, int startId) {
final Action action = getActionFromIntent(intent);
if (action == null) {
return;
}
switch (action) {
case CHECK_BILLING_SUPPORTED:
checkBillingSupported(startId);
break;
case CHECK_SUBSCRIPTION_SUPPORTED:
checkSubscriptionSupported(startId);
break;
case REQUEST_PURCHASE:
requestPurchase(intent, startId);
break;
case REQUEST_SUBSCRIPTION:
requestSubscription(intent, startId);
break;
case GET_PURCHASE_INFORMATION:
getPurchaseInformation(intent, startId);
break;
case CONFIRM_NOTIFICATIONS:
confirmNotifications(intent, startId);
break;
case RESTORE_TRANSACTIONS:
restoreTransactions(intent, startId);
}
}
private void requestPurchase(Intent intent, int startId) {
final String packageName = getPackageName();
final String itemId = intent.getStringExtra(EXTRA_ITEM_ID);
final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD);
final RequestPurchase request = new RequestPurchase(packageName, startId, itemId, developerPayload);
runRequestOrQueue(request);
}
private void requestSubscription(Intent intent, int startId) {
final String packageName = getPackageName();
final String itemId = intent.getStringExtra(EXTRA_ITEM_ID);
final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD);
final RequestPurchase request = new RequestSubscription(packageName, startId, itemId, developerPayload);
runRequestOrQueue(request);
}
private void restoreTransactions(Intent intent, int startId) {
final String packageName = getPackageName();
final long nonce = intent.getLongExtra(EXTRA_NONCE, 0);
final RestoreTransactions request = new RestoreTransactions(packageName, startId);
request.setNonce(nonce);
runRequestOrQueue(request);
}
private void runPendingRequests() {
BillingRequest request;
int maxStartId = -1;
while ((request = mPendingRequests.peek()) != null) {
if (mService != null) {
runRequest(request);
mPendingRequests.remove();
if (maxStartId < request.getStartId()) {
maxStartId = request.getStartId();
}
} else {
bindMarketBillingService();
return;
}
}
if (maxStartId >= 0) {
stopSelf(maxStartId);
}
}
private void runRequest(BillingRequest request) {
try {
final long requestId = request.run(mService);
BillingController.onRequestSent(requestId, request);
} catch (RemoteException e) {
Log.w(this.getClass().getSimpleName(), "Remote billing service crashed");
// TODO: Retry?
}
}
private void runRequestOrQueue(BillingRequest request) {
mPendingRequests.add(request);
if (mService == null) {
bindMarketBillingService();
} else {
runPendingRequests();
}
}
@Override
public void onDestroy() {
super.onDestroy();
// Ensure we're not leaking Android Market billing service
if (mService != null) {
try {
unbindService(this);
} catch (IllegalArgumentException e) {
// This might happen if the service was disconnected
}
}
}
}