mirror of https://github.com/tasks/tasks
Merge and resolve conflicts from 120712_sb_in_app_billing
commit
6676eb168c
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 249 B |
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
@ -0,0 +1,79 @@
|
|||||||
|
<?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:background="#aaa"
|
||||||
|
android:orientation="vertical" >
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/premium_description_holder"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingRight="5dip"
|
||||||
|
android:paddingTop="10dip" >
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/premium_description_title"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dip"
|
||||||
|
android:gravity="left"
|
||||||
|
android:text="@string/premium_description_header"
|
||||||
|
android:textColor="#404040"
|
||||||
|
android:paddingLeft="20dip"
|
||||||
|
android:textSize="20dip"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/premium_bullets"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:textSize="20dip" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<include layout="@layout/astrid_speech_bubble"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="@android:color/white">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/premium_buy_month"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="50dip"
|
||||||
|
android:layout_marginLeft="8dip"
|
||||||
|
android:layout_marginRight="8dip"
|
||||||
|
android:layout_marginBottom="8dip"
|
||||||
|
android:layout_marginTop="8dip"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="#707070"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:text="@string/premium_buy_month"
|
||||||
|
android:gravity="center"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/premium_buy_year"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="50dip"
|
||||||
|
android:layout_marginLeft="8dip"
|
||||||
|
android:layout_marginRight="8dip"
|
||||||
|
android:layout_marginBottom="8dip"
|
||||||
|
android:layout_marginTop="8dip"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="?attr/asThemeTextColor"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/premium_buy_year"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 com.android.vending.billing;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
interface IMarketBillingService {
|
||||||
|
/** Given the arguments in bundle form, returns a bundle for results. */
|
||||||
|
Bundle sendBillingRequest(in Bundle bundle);
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.timsu.astrid.R;
|
||||||
|
import com.todoroo.andlib.service.Autowired;
|
||||||
|
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||||
|
import com.todoroo.andlib.utility.DialogUtilities;
|
||||||
|
import com.todoroo.andlib.utility.Preferences;
|
||||||
|
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
|
||||||
|
import com.todoroo.astrid.actfm.sync.ActFmSyncService;
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
|
||||||
|
import com.todoroo.astrid.billing.BillingService.RequestPurchase;
|
||||||
|
import com.todoroo.astrid.billing.BillingService.RestoreTransactions;
|
||||||
|
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
public abstract class AstridPurchaseObserver extends PurchaseObserver {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ActFmSyncService actFmSyncService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ActFmPreferenceService actFmPreferenceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link PurchaseObserver} is used to get callbacks when Android Market sends
|
||||||
|
* messages to this application so that we can update the UI.
|
||||||
|
*/
|
||||||
|
public AstridPurchaseObserver(Activity activity, Handler handler) {
|
||||||
|
super(activity, handler);
|
||||||
|
DependencyInjectionService.getInstance().inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBillingSupported(boolean supported, String type) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "supported: " + supported);
|
||||||
|
}
|
||||||
|
if (type != null && type.equals(BillingConstants.ITEM_TYPE_SUBSCRIPTION)) {
|
||||||
|
if (supported) {
|
||||||
|
billingSupportedCallback();
|
||||||
|
} else {
|
||||||
|
billingNotSupportedCallback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subscriptionsNotSupportedCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void billingSupportedCallback();
|
||||||
|
|
||||||
|
protected abstract void billingNotSupportedCallback();
|
||||||
|
|
||||||
|
protected abstract void subscriptionsNotSupportedCallback();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPurchaseStateChange(PurchaseState purchaseState, final String itemId,
|
||||||
|
int quantity, long purchaseTime, String developerPayload, final String purchaseToken) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "onPurchaseStateChange() itemId: " + itemId + " " + purchaseState);
|
||||||
|
}
|
||||||
|
|
||||||
|
Preferences.setString(BillingConstants.PREF_PRODUCT_ID, itemId);
|
||||||
|
Preferences.setString(BillingConstants.PREF_PURCHASE_TOKEN, purchaseToken);
|
||||||
|
|
||||||
|
if (purchaseState == PurchaseState.PURCHASED) {
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Preferences.setBoolean(ActFmPreferenceService.PREF_LOCAL_PREMIUM, true);
|
||||||
|
if (actFmPreferenceService.isLoggedIn()) {
|
||||||
|
actFmSyncService.updateUserSubscriptionStatus(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Preferences.setBoolean(ActFmPreferenceService.PREF_PREMIUM, true);
|
||||||
|
mActivity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
DialogUtilities.okDialog(mActivity, mActivity.getString(R.string.DLG_information_title),
|
||||||
|
0, mActivity.getString(R.string.premium_success), new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
mActivity.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mActivity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
DialogUtilities.okDialog(mActivity, mActivity.getString(R.string.DLG_information_title),
|
||||||
|
0, mActivity.getString(R.string.premium_success_with_server_error), new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
mActivity.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Preferences.setBoolean(BillingConstants.PREF_NEEDS_SERVER_UPDATE, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
} else if (purchaseState == PurchaseState.REFUNDED) {
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Preferences.setBoolean(ActFmPreferenceService.PREF_LOCAL_PREMIUM, false);
|
||||||
|
if (actFmPreferenceService.isLoggedIn())
|
||||||
|
actFmSyncService.updateUserSubscriptionStatus(null, null);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPurchaseResponse(RequestPurchase request,
|
||||||
|
ResponseCode responseCode) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, request.mProductId + ": " + responseCode);
|
||||||
|
}
|
||||||
|
if (responseCode == ResponseCode.RESULT_OK) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "purchase was successfully sent to server");
|
||||||
|
}
|
||||||
|
} else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "user canceled purchase");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "purchase failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRestoreTransactionsResponse(RestoreTransactions request,
|
||||||
|
ResponseCode responseCode) {
|
||||||
|
if (responseCode == ResponseCode.RESULT_OK) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, "completed RestoreTransactions request");
|
||||||
|
}
|
||||||
|
// Update the shared preferences so that we don't perform
|
||||||
|
// a RestoreTransactions again.
|
||||||
|
Preferences.setBoolean(BillingConstants.PREF_TRANSACTIONS_INITIALIZED, true);
|
||||||
|
} else {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, "RestoreTransactions error: " + responseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,571 @@
|
|||||||
|
// Portions copyright 2002, Google, Inc.
|
||||||
|
//
|
||||||
|
// 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 com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
// This code was converted from code at http://iharder.sourceforge.net/base64/
|
||||||
|
// Lots of extraneous features were removed.
|
||||||
|
/* The original code said:
|
||||||
|
* <p>
|
||||||
|
* I am placing this code in the Public Domain. Do with it as you will.
|
||||||
|
* This software comes with no guarantees or warranties but with
|
||||||
|
* plenty of well-wishing instead!
|
||||||
|
* Please visit
|
||||||
|
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
||||||
|
* periodically to check for updates or to contribute improvements.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Robert Harder
|
||||||
|
* @author rharder@usa.net
|
||||||
|
* @version 1.3
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 converter class. This code is not a complete MIME encoder;
|
||||||
|
* it simply converts binary data to base64 data and back.
|
||||||
|
*
|
||||||
|
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
||||||
|
* class.
|
||||||
|
*/
|
||||||
|
public class Base64 {
|
||||||
|
/** Specify encoding (value is {@code true}). */
|
||||||
|
public final static boolean ENCODE = true;
|
||||||
|
|
||||||
|
/** Specify decoding (value is {@code false}). */
|
||||||
|
public final static boolean DECODE = false;
|
||||||
|
|
||||||
|
/** The equals sign (=) as a byte. */
|
||||||
|
private final static byte EQUALS_SIGN = (byte) '=';
|
||||||
|
|
||||||
|
/** The new line character (\n) as a byte. */
|
||||||
|
private final static byte NEW_LINE = (byte) '\n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 64 valid Base64 values.
|
||||||
|
*/
|
||||||
|
private final static byte[] ALPHABET =
|
||||||
|
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||||
|
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||||
|
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||||
|
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||||
|
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||||
|
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||||
|
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||||
|
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||||
|
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||||
|
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||||
|
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||||
|
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||||
|
(byte) '9', (byte) '+', (byte) '/'};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 64 valid web safe Base64 values.
|
||||||
|
*/
|
||||||
|
private final static byte[] WEBSAFE_ALPHABET =
|
||||||
|
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||||
|
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||||
|
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||||
|
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||||
|
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||||
|
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||||
|
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||||
|
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||||
|
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||||
|
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||||
|
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||||
|
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||||
|
(byte) '9', (byte) '-', (byte) '_'};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a Base64 value to either its 6-bit reconstruction value
|
||||||
|
* or a negative number indicating some other meaning.
|
||||||
|
**/
|
||||||
|
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||||
|
-5, -5, // Whitespace: Tab and Linefeed
|
||||||
|
-9, -9, // Decimal 11 - 12
|
||||||
|
-5, // Whitespace: Carriage Return
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||||
|
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||||
|
-5, // Whitespace: Space
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
||||||
|
62, // Plus sign at decimal 43
|
||||||
|
-9, -9, -9, // Decimal 44 - 46
|
||||||
|
63, // Slash at decimal 47
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||||
|
-9, -9, -9, // Decimal 58 - 60
|
||||||
|
-1, // Equals sign at decimal 61
|
||||||
|
-9, -9, -9, // Decimal 62 - 64
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||||
|
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||||
|
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
||||||
|
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||||
|
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||||
|
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||||
|
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The web safe decodabet */
|
||||||
|
private final static byte[] WEBSAFE_DECODABET =
|
||||||
|
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||||
|
-5, -5, // Whitespace: Tab and Linefeed
|
||||||
|
-9, -9, // Decimal 11 - 12
|
||||||
|
-5, // Whitespace: Carriage Return
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||||
|
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||||
|
-5, // Whitespace: Space
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
||||||
|
62, // Dash '-' sign at decimal 45
|
||||||
|
-9, -9, // Decimal 46-47
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||||
|
-9, -9, -9, // Decimal 58 - 60
|
||||||
|
-1, // Equals sign at decimal 61
|
||||||
|
-9, -9, -9, // Decimal 62 - 64
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||||
|
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||||
|
-9, -9, -9, -9, // Decimal 91-94
|
||||||
|
63, // Underscore '_' at decimal 95
|
||||||
|
-9, // Decimal 96
|
||||||
|
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||||
|
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||||
|
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||||
|
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||||
|
};
|
||||||
|
|
||||||
|
// Indicates white space in encoding
|
||||||
|
private final static byte WHITE_SPACE_ENC = -5;
|
||||||
|
// Indicates equals sign in encoding
|
||||||
|
private final static byte EQUALS_SIGN_ENC = -1;
|
||||||
|
|
||||||
|
/** Defeats instantiation. */
|
||||||
|
private Base64() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ******** E N C O D I N G M E T H O D S ******** */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes up to three bytes of the array <var>source</var>
|
||||||
|
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
||||||
|
* The source and destination arrays can be manipulated
|
||||||
|
* anywhere along their length by specifying
|
||||||
|
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||||
|
* This method does not check to make sure your arrays
|
||||||
|
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
||||||
|
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
||||||
|
* the <var>destination</var> array.
|
||||||
|
* The actual number of significant bytes in your array is
|
||||||
|
* given by <var>numSigBytes</var>.
|
||||||
|
*
|
||||||
|
* @param source the array to convert
|
||||||
|
* @param srcOffset the index where conversion begins
|
||||||
|
* @param numSigBytes the number of significant bytes in your array
|
||||||
|
* @param destination the array to hold the conversion
|
||||||
|
* @param destOffset the index where output will be put
|
||||||
|
* @param alphabet is the encoding alphabet
|
||||||
|
* @return the <var>destination</var> array
|
||||||
|
* @since 1.3
|
||||||
|
*/
|
||||||
|
private static byte[] encode3to4(byte[] source, int srcOffset,
|
||||||
|
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
||||||
|
// 1 2 3
|
||||||
|
// 01234567890123456789012345678901 Bit position
|
||||||
|
// --------000000001111111122222222 Array position from threeBytes
|
||||||
|
// --------| || || || | Six bit groups to index alphabet
|
||||||
|
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
||||||
|
// 0x3f 0x3f 0x3f Additional AND
|
||||||
|
|
||||||
|
// Create buffer with zero-padding if there are only one or two
|
||||||
|
// significant bytes passed in the array.
|
||||||
|
// We have to shift left 24 in order to flush out the 1's that appear
|
||||||
|
// when Java treats a value as negative that is cast from a byte to an int.
|
||||||
|
int inBuff =
|
||||||
|
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
|
||||||
|
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
|
||||||
|
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
||||||
|
|
||||||
|
switch (numSigBytes) {
|
||||||
|
case 3:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
|
||||||
|
return destination;
|
||||||
|
case 2:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
destination[destOffset + 3] = EQUALS_SIGN;
|
||||||
|
return destination;
|
||||||
|
case 1:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = EQUALS_SIGN;
|
||||||
|
destination[destOffset + 3] = EQUALS_SIGN;
|
||||||
|
return destination;
|
||||||
|
default:
|
||||||
|
return destination;
|
||||||
|
} // end switch
|
||||||
|
} // end encode3to4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
* Equivalent to calling
|
||||||
|
* {@code encodeBytes(source, 0, source.length)}
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static String encode(byte[] source) {
|
||||||
|
return encode(source, 0, source.length, ALPHABET, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into web safe Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @param doPadding is {@code true} to pad result with '=' chars
|
||||||
|
* if it does not fall on 3 byte boundaries
|
||||||
|
*/
|
||||||
|
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
||||||
|
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source the data to convert
|
||||||
|
* @param off offset in array where conversion should begin
|
||||||
|
* @param len length of data to convert
|
||||||
|
* @param alphabet the encoding alphabet
|
||||||
|
* @param doPadding is {@code true} to pad result with '=' chars
|
||||||
|
* if it does not fall on 3 byte boundaries
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
||||||
|
boolean doPadding) {
|
||||||
|
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
||||||
|
int outLen = outBuff.length;
|
||||||
|
|
||||||
|
// If doPadding is false, set length to truncate '='
|
||||||
|
// padding characters
|
||||||
|
while (doPadding == false && outLen > 0) {
|
||||||
|
if (outBuff[outLen - 1] != '=') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
outLen -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(outBuff, 0, outLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source the data to convert
|
||||||
|
* @param off offset in array where conversion should begin
|
||||||
|
* @param len length of data to convert
|
||||||
|
* @param alphabet is the encoding alphabet
|
||||||
|
* @param maxLineLength maximum length of one line.
|
||||||
|
* @return the BASE64-encoded byte array
|
||||||
|
*/
|
||||||
|
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
||||||
|
int maxLineLength) {
|
||||||
|
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
||||||
|
int len43 = lenDiv3 * 4;
|
||||||
|
byte[] outBuff = new byte[len43 // Main 4:3
|
||||||
|
+ (len43 / maxLineLength)]; // New lines
|
||||||
|
|
||||||
|
int d = 0;
|
||||||
|
int e = 0;
|
||||||
|
int len2 = len - 2;
|
||||||
|
int lineLength = 0;
|
||||||
|
for (; d < len2; d += 3, e += 4) {
|
||||||
|
|
||||||
|
// The following block of code is the same as
|
||||||
|
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
||||||
|
// but inlined for faster encoding (~20% improvement)
|
||||||
|
int inBuff =
|
||||||
|
((source[d + off] << 24) >>> 8)
|
||||||
|
| ((source[d + 1 + off] << 24) >>> 16)
|
||||||
|
| ((source[d + 2 + off] << 24) >>> 24);
|
||||||
|
outBuff[e] = alphabet[(inBuff >>> 18)];
|
||||||
|
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
|
||||||
|
|
||||||
|
lineLength += 4;
|
||||||
|
if (lineLength == maxLineLength) {
|
||||||
|
outBuff[e + 4] = NEW_LINE;
|
||||||
|
e++;
|
||||||
|
lineLength = 0;
|
||||||
|
} // end if: end of line
|
||||||
|
} // end for: each piece of array
|
||||||
|
|
||||||
|
if (d < len) {
|
||||||
|
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
||||||
|
|
||||||
|
lineLength += 4;
|
||||||
|
if (lineLength == maxLineLength) {
|
||||||
|
// Add a last newline
|
||||||
|
outBuff[e + 4] = NEW_LINE;
|
||||||
|
e++;
|
||||||
|
}
|
||||||
|
e += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert (e == outBuff.length);
|
||||||
|
return outBuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ******** D E C O D I N G M E T H O D S ******** */
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes four bytes from array <var>source</var>
|
||||||
|
* and writes the resulting bytes (up to three of them)
|
||||||
|
* to <var>destination</var>.
|
||||||
|
* The source and destination arrays can be manipulated
|
||||||
|
* anywhere along their length by specifying
|
||||||
|
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||||
|
* This method does not check to make sure your arrays
|
||||||
|
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
||||||
|
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
||||||
|
* the <var>destination</var> array.
|
||||||
|
* This method returns the actual number of bytes that
|
||||||
|
* were converted from the Base64 encoding.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param source the array to convert
|
||||||
|
* @param srcOffset the index where conversion begins
|
||||||
|
* @param destination the array to hold the conversion
|
||||||
|
* @param destOffset the index where output will be put
|
||||||
|
* @param decodabet the decodabet for decoding Base64 content
|
||||||
|
* @return the number of decoded bytes converted
|
||||||
|
* @since 1.3
|
||||||
|
*/
|
||||||
|
private static int decode4to3(byte[] source, int srcOffset,
|
||||||
|
byte[] destination, int destOffset, byte[] decodabet) {
|
||||||
|
// Example: Dk==
|
||||||
|
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||||
|
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||||
|
return 1;
|
||||||
|
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
||||||
|
// Example: DkL=
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||||
|
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||||
|
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||||
|
destination[destOffset + 1] = (byte) (outBuff >>> 8);
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
// Example: DkLE
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||||
|
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||||
|
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
|
||||||
|
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte) (outBuff >> 16);
|
||||||
|
destination[destOffset + 1] = (byte) (outBuff >> 8);
|
||||||
|
destination[destOffset + 2] = (byte) (outBuff);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
} // end decodeToBytes
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes data from Base64 notation.
|
||||||
|
*
|
||||||
|
* @param s the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static byte[] decode(String s) throws Base64DecoderException {
|
||||||
|
byte[] bytes = s.getBytes();
|
||||||
|
return decode(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes data from web safe Base64 notation.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param s the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
||||||
|
byte[] bytes = s.getBytes();
|
||||||
|
return decodeWebSafe(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source The Base64 encoded data
|
||||||
|
* @return decoded data
|
||||||
|
* @since 1.3
|
||||||
|
* @throws Base64DecoderException
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
||||||
|
return decode(source, 0, source.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes web safe Base64 content in byte array format and returns
|
||||||
|
* the decoded data.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param source the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(byte[] source)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decodeWebSafe(source, 0, source.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source the Base64 encoded data
|
||||||
|
* @param off the offset of where to begin decoding
|
||||||
|
* @param len the length of characters to decode
|
||||||
|
* @return decoded data
|
||||||
|
* @since 1.3
|
||||||
|
* @throws Base64DecoderException
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source, int off, int len)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decode(source, off, len, DECODABET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes web safe Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param source the Base64 encoded data
|
||||||
|
* @param off the offset of where to begin decoding
|
||||||
|
* @param len the length of characters to decode
|
||||||
|
* @return decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decode(source, off, len, WEBSAFE_DECODABET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content using the supplied decodabet and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source the Base64 encoded data
|
||||||
|
* @param off the offset of where to begin decoding
|
||||||
|
* @param len the length of characters to decode
|
||||||
|
* @param decodabet the decodabet for decoding Base64 content
|
||||||
|
* @return decoded data
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
int len34 = len * 3 / 4;
|
||||||
|
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
||||||
|
int outBuffPosn = 0;
|
||||||
|
|
||||||
|
byte[] b4 = new byte[4];
|
||||||
|
int b4Posn = 0;
|
||||||
|
int i = 0;
|
||||||
|
byte sbiCrop = 0;
|
||||||
|
byte sbiDecode = 0;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
|
||||||
|
sbiDecode = decodabet[sbiCrop];
|
||||||
|
|
||||||
|
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
||||||
|
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
||||||
|
// An equals sign (for padding) must not occur at position 0 or 1
|
||||||
|
// and must be the last byte[s] in the encoded value
|
||||||
|
if (sbiCrop == EQUALS_SIGN) {
|
||||||
|
int bytesLeft = len - i;
|
||||||
|
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
|
||||||
|
if (b4Posn == 0 || b4Posn == 1) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"invalid padding byte '=' at byte offset " + i);
|
||||||
|
} else if ((b4Posn == 3 && bytesLeft > 2)
|
||||||
|
|| (b4Posn == 4 && bytesLeft > 1)) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"padding byte '=' falsely signals end of encoded value "
|
||||||
|
+ "at offset " + i);
|
||||||
|
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"encoded value has invalid trailing byte");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
b4[b4Posn++] = sbiCrop;
|
||||||
|
if (b4Posn == 4) {
|
||||||
|
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||||
|
b4Posn = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Base64DecoderException("Bad Base64 input character at " + i
|
||||||
|
+ ": " + source[i + off] + "(decimal)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because web safe encoding allows non padding base64 encodes, we
|
||||||
|
// need to pad the rest of the b4 buffer with equal signs when
|
||||||
|
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
||||||
|
// four characters, so the b4 buffer must have two or three
|
||||||
|
// characters. This also catches the case where the input is
|
||||||
|
// padded with EQUALS_SIGN
|
||||||
|
if (b4Posn != 0) {
|
||||||
|
if (b4Posn == 1) {
|
||||||
|
throw new Base64DecoderException("single trailing character at offset "
|
||||||
|
+ (len - 1));
|
||||||
|
}
|
||||||
|
b4[b4Posn++] = EQUALS_SIGN;
|
||||||
|
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] out = new byte[outBuffPosn];
|
||||||
|
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2002, Google, Inc.
|
||||||
|
//
|
||||||
|
// 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 com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when encountering an invalid Base64 input character.
|
||||||
|
*
|
||||||
|
* @author nelson
|
||||||
|
*/
|
||||||
|
public class Base64DecoderException extends Exception {
|
||||||
|
public Base64DecoderException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Base64DecoderException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
@ -0,0 +1,277 @@
|
|||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.support.v4.app.ActionBar;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ImageView.ScaleType;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.timsu.astrid.R;
|
||||||
|
import com.todoroo.andlib.service.Autowired;
|
||||||
|
import com.todoroo.andlib.service.DependencyInjectionService;
|
||||||
|
import com.todoroo.andlib.utility.DialogUtilities;
|
||||||
|
import com.todoroo.andlib.utility.Preferences;
|
||||||
|
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
|
||||||
|
import com.todoroo.astrid.service.ThemeService;
|
||||||
|
|
||||||
|
public class BillingActivity extends FragmentActivity {
|
||||||
|
|
||||||
|
private static final int DIALOG_CANNOT_CONNECT_ID = 1;
|
||||||
|
private static final int DIALOG_BILLING_NOT_SUPPORTED_ID = 2;
|
||||||
|
private static final int DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID = 3;
|
||||||
|
|
||||||
|
private Handler handler;
|
||||||
|
private BillingService billingService;
|
||||||
|
private AstridPurchaseObserver purchaseObserver;
|
||||||
|
private Button buyMonth;
|
||||||
|
private Button buyYear;
|
||||||
|
|
||||||
|
@Autowired private ActFmPreferenceService actFmPreferenceService;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
ThemeService.applyTheme(this);
|
||||||
|
DependencyInjectionService.getInstance().inject(this);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.billing_activity);
|
||||||
|
|
||||||
|
setupActionBar();
|
||||||
|
|
||||||
|
setupButtons();
|
||||||
|
|
||||||
|
setupText();
|
||||||
|
|
||||||
|
handler = new Handler();
|
||||||
|
billingService = new BillingService();
|
||||||
|
billingService.setContext(this);
|
||||||
|
purchaseObserver = new AstridPurchaseObserver(this, handler) {
|
||||||
|
@Override
|
||||||
|
protected void billingSupportedCallback() {
|
||||||
|
restoreTransactions();
|
||||||
|
buyMonth.setEnabled(true);
|
||||||
|
buyYear.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void billingNotSupportedCallback() {
|
||||||
|
showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void subscriptionsNotSupportedCallback() {
|
||||||
|
showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupActionBar() {
|
||||||
|
ActionBar actionBar = getSupportActionBar();
|
||||||
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
actionBar.setDisplayShowTitleEnabled(false);
|
||||||
|
|
||||||
|
actionBar.setDisplayShowCustomEnabled(true);
|
||||||
|
actionBar.setCustomView(R.layout.header_title_view);
|
||||||
|
((TextView) actionBar.getCustomView().findViewById(R.id.title)).setText(R.string.premium_billing_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
ResponseHandler.register(purchaseObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
ResponseHandler.unregister(purchaseObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
billingService.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (!actFmPreferenceService.isLoggedIn()) {
|
||||||
|
// Prompt to log in, but this shouldn't happen anyways since we hide the entry path to this screen when not logged in
|
||||||
|
DialogUtilities.okDialog(this, getString(R.string.premium_login_prompt),
|
||||||
|
new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!billingService.checkBillingSupported(BillingConstants.ITEM_TYPE_SUBSCRIPTION)) {
|
||||||
|
showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
|
||||||
|
} else if (ActFmPreferenceService.isPremiumUser()) {
|
||||||
|
DialogUtilities.okDialog(this, getString(R.string.premium_already_subscribed), new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupButtons() {
|
||||||
|
buyMonth = (Button) findViewById(R.id.premium_buy_month);
|
||||||
|
buyYear = (Button) findViewById(R.id.premium_buy_year);
|
||||||
|
|
||||||
|
buyMonth.setEnabled(false);
|
||||||
|
buyYear.setEnabled(false);
|
||||||
|
|
||||||
|
buyMonth.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (!billingService.requestPurchase(BillingConstants.PRODUCT_ID_MONTHLY,
|
||||||
|
BillingConstants.ITEM_TYPE_SUBSCRIPTION, null)) {
|
||||||
|
showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buyYear.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (!billingService.requestPurchase(BillingConstants.PRODUCT_ID_YEARLY,
|
||||||
|
BillingConstants.ITEM_TYPE_SUBSCRIPTION, null)) {
|
||||||
|
showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
private void setupText() {
|
||||||
|
int[] bullets = new int[] { R.string.premium_description_1, R.string.premium_description_2, R.string.premium_description_3,
|
||||||
|
R.string.premium_description_4, R.string.premium_description_5, R.string.premium_description_6
|
||||||
|
};
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder("<html><style type=\"text/css\">li { padding-bottom: 13px } </style><body><ul>");
|
||||||
|
|
||||||
|
for (int i = 0; i < bullets.length; i++) {
|
||||||
|
String curr = getString(bullets[i]);
|
||||||
|
if (curr.contains("\n"))
|
||||||
|
curr = curr.replace("\n", "<br>");
|
||||||
|
builder.append("<li><font style='color=#404040; font-size: 18px'>").append(curr);
|
||||||
|
|
||||||
|
builder.append("</font></li>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append("</ul></body></html>");
|
||||||
|
|
||||||
|
WebView list = (WebView) findViewById(R.id.premium_bullets);
|
||||||
|
list.loadDataWithBaseURL("file:///android_asset/", builder.toString(), "text/html", "utf-8", null);
|
||||||
|
list.setBackgroundColor(0);
|
||||||
|
|
||||||
|
View speechBubbleBackground = findViewById(R.id.speech_bubble_container);
|
||||||
|
speechBubbleBackground.setBackgroundColor(0);
|
||||||
|
|
||||||
|
DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
|
ImageView icon = (ImageView) findViewById(R.id.astridIcon);
|
||||||
|
|
||||||
|
int dim = (int) (80 * metrics.density);
|
||||||
|
icon.setLayoutParams(new LinearLayout.LayoutParams(dim, dim));
|
||||||
|
icon.setScaleType(ScaleType.FIT_CENTER);
|
||||||
|
|
||||||
|
TextView speechBubble = (TextView) findViewById(R.id.reminder_message);
|
||||||
|
|
||||||
|
// Construct speech bubble text
|
||||||
|
String html = String.format("%s <font color=\"#%s\">%s</font>",
|
||||||
|
getString(R.string.premium_speech_bubble_1),
|
||||||
|
Integer.toHexString(getResources().getColor(R.color.red_theme_color) - 0xff000000),
|
||||||
|
getString(R.string.premium_speech_bubble_2));
|
||||||
|
Spanned spanned = Html.fromHtml(html);
|
||||||
|
speechBubble.setText(spanned);
|
||||||
|
speechBubble.setTextSize(17);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the language and/or country of the device into the given string.
|
||||||
|
* The pattern "%lang%" will be replaced by the device's language code and
|
||||||
|
* the pattern "%region%" will be replaced with the device's country code.
|
||||||
|
*
|
||||||
|
* @param str the string to replace the language/country within
|
||||||
|
* @return a string containing the local language and region codes
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
private String replaceLanguageAndRegion(String str) {
|
||||||
|
// Substitute language and or region if present in string
|
||||||
|
if (str.contains("%lang%") || str.contains("%region%")) {
|
||||||
|
Locale locale = Locale.getDefault();
|
||||||
|
str = str.replace("%lang%", locale.getLanguage().toLowerCase());
|
||||||
|
str = str.replace("%region%", locale.getCountry().toLowerCase());
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Dialog onCreateDialog(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case DIALOG_CANNOT_CONNECT_ID:
|
||||||
|
return createDialog(R.string.cannot_connect_title,
|
||||||
|
R.string.cannot_connect_message);
|
||||||
|
case DIALOG_BILLING_NOT_SUPPORTED_ID:
|
||||||
|
return createDialog(R.string.billing_not_supported_title,
|
||||||
|
R.string.billing_not_supported_message);
|
||||||
|
case DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID:
|
||||||
|
return createDialog(R.string.subscriptions_not_supported_title,
|
||||||
|
R.string.subscriptions_not_supported_message);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dialog createDialog(int titleId, int messageId) {
|
||||||
|
String helpUrl = replaceLanguageAndRegion(getString(R.string.subscriptions_help_url));
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i("billing-activity-url", helpUrl); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
final Uri helpUri = Uri.parse(helpUrl);
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setTitle(titleId)
|
||||||
|
.setIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
.setMessage(messageId)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(R.string.subscriptions_learn_more, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, helpUri);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return builder.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreTransactions() {
|
||||||
|
boolean initialized = Preferences.getBoolean(BillingConstants.PREF_TRANSACTIONS_INITIALIZED, false);
|
||||||
|
if (!initialized) {
|
||||||
|
billingService.restoreTransactions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
|
||||||
|
import com.todoroo.astrid.utility.Constants;
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
public class BillingConstants {
|
||||||
|
|
||||||
|
/** This is the action we use to bind to the MarketBillingService. */
|
||||||
|
public static final String MARKET_BILLING_SERVICE_ACTION = "com.android.vending.billing.MarketBillingService.BIND";
|
||||||
|
|
||||||
|
// Intent actions that we send from the BillingReceiver to the
|
||||||
|
// BillingService. Defined by this application.
|
||||||
|
public static final String ACTION_CONFIRM_NOTIFICATION = "com.timsu.astrid.subscriptions.CONFIRM_NOTIFICATION";
|
||||||
|
public static final String ACTION_GET_PURCHASE_INFORMATION = "com.timsu.astrid.subscriptions.GET_PURCHASE_INFORMATION";
|
||||||
|
public static final String ACTION_RESTORE_TRANSACTIONS = "com.timsu.astrid.subscriptions.RESTORE_TRANSACTIONS";
|
||||||
|
|
||||||
|
// Intent actions that we receive in the BillingReceiver from Market.
|
||||||
|
// These are defined by Market and cannot be changed.
|
||||||
|
public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY";
|
||||||
|
public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE";
|
||||||
|
public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED";
|
||||||
|
|
||||||
|
// These are the names of the extras that are passed in an intent from
|
||||||
|
// Market to this application and cannot be changed.
|
||||||
|
public static final String NOTIFICATION_ID = "notification_id";
|
||||||
|
public static final String INAPP_SIGNED_DATA = "inapp_signed_data";
|
||||||
|
public static final String INAPP_SIGNATURE = "inapp_signature";
|
||||||
|
public static final String INAPP_REQUEST_ID = "request_id";
|
||||||
|
public static final String INAPP_RESPONSE_CODE = "response_code";
|
||||||
|
|
||||||
|
// These are the names of the fields in the request bundle.
|
||||||
|
public static final String BILLING_REQUEST_METHOD = "BILLING_REQUEST";
|
||||||
|
public static final String BILLING_REQUEST_API_VERSION = "API_VERSION";
|
||||||
|
public static final String BILLING_REQUEST_PACKAGE_NAME = "PACKAGE_NAME";
|
||||||
|
public static final String BILLING_REQUEST_ITEM_ID = "ITEM_ID";
|
||||||
|
public static final String BILLING_REQUEST_ITEM_TYPE = "ITEM_TYPE";
|
||||||
|
public static final String BILLING_REQUEST_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD";
|
||||||
|
public static final String BILLING_REQUEST_NOTIFY_IDS = "NOTIFY_IDS";
|
||||||
|
public static final String BILLING_REQUEST_NONCE = "NONCE";
|
||||||
|
|
||||||
|
public static final String BILLING_RESPONSE_RESPONSE_CODE = "RESPONSE_CODE";
|
||||||
|
public static final String BILLING_RESPONSE_PURCHASE_INTENT = "PURCHASE_INTENT";
|
||||||
|
public static final String BILLING_RESPONSE_REQUEST_ID = "REQUEST_ID";
|
||||||
|
public static final long BILLING_RESPONSE_INVALID_REQUEST_ID = -1;
|
||||||
|
|
||||||
|
// These are the types supported in the IAB v2
|
||||||
|
public static final String ITEM_TYPE_INAPP = "inapp";
|
||||||
|
public static final String ITEM_TYPE_SUBSCRIPTION = "subs";
|
||||||
|
|
||||||
|
|
||||||
|
public static final String PRODUCT_ID_MONTHLY = "com.timsu.astrid.premium_monthly";
|
||||||
|
public static final String PRODUCT_ID_YEARLY = "com.timsu.astrid.premium_yearly";
|
||||||
|
|
||||||
|
public static final String PREF_PRODUCT_ID = ActFmPreferenceService.IDENTIFIER + "_inapp_product_id";
|
||||||
|
public static final String PREF_PURCHASE_TOKEN = ActFmPreferenceService.IDENTIFIER + "_inapp_purchase_token";
|
||||||
|
public static final String PREF_NEEDS_SERVER_UPDATE = ActFmPreferenceService.IDENTIFIER + "_inapp_needs_server_update";
|
||||||
|
public static final String PREF_TRANSACTIONS_INITIALIZED = "premium_transactions_initialized"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
public static final char PUB_KEY_OBFUSCATION_CHAR = '!';
|
||||||
|
public static final char PUB_KEY_REPLACE_CHAR = 'B';
|
||||||
|
public static final String PUB_KEY_OBFUSCATED = "pubkey";
|
||||||
|
|
||||||
|
public static final boolean DEBUG = false || Constants.DEBUG;
|
||||||
|
|
||||||
|
// The response codes for a request, defined by Android Market.
|
||||||
|
public enum ResponseCode {
|
||||||
|
RESULT_OK,
|
||||||
|
RESULT_USER_CANCELED,
|
||||||
|
RESULT_SERVICE_UNAVAILABLE,
|
||||||
|
RESULT_BILLING_UNAVAILABLE,
|
||||||
|
RESULT_ITEM_UNAVAILABLE,
|
||||||
|
RESULT_DEVELOPER_ERROR,
|
||||||
|
RESULT_ERROR;
|
||||||
|
|
||||||
|
// Converts from an ordinal value to the ResponseCode
|
||||||
|
public static ResponseCode valueOf(int index) {
|
||||||
|
ResponseCode[] values = ResponseCode.values();
|
||||||
|
if (index < 0 || index >= values.length) {
|
||||||
|
return RESULT_ERROR;
|
||||||
|
}
|
||||||
|
return values[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The possible states of an in-app purchase, as defined by Android Market.
|
||||||
|
public enum PurchaseState {
|
||||||
|
// Responses to requestPurchase or restoreTransactions.
|
||||||
|
PURCHASED, // User was charged for the order.
|
||||||
|
CANCELED, // The charge failed on the server. (NOT THE SAME AS CANCELING A SUBSCRIPTION)
|
||||||
|
REFUNDED, // User received a refund for the order.
|
||||||
|
EXPIRED; // Subscription expired due to non-payment or cancellation
|
||||||
|
|
||||||
|
// Converts from an ordinal value to the PurchaseState
|
||||||
|
public static PurchaseState valueOf(int index) {
|
||||||
|
PurchaseState[] values = PurchaseState.values();
|
||||||
|
if (index < 0 || index >= values.length) {
|
||||||
|
return CANCELED;
|
||||||
|
}
|
||||||
|
return values[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
|
||||||
|
|
||||||
|
public class BillingReceiver extends BroadcastReceiver {
|
||||||
|
private static final String TAG = "billing-receiver"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the entry point for all asynchronous messages sent from Android Market to
|
||||||
|
* the application. This method forwards the messages on to the
|
||||||
|
* {@link BillingService}, which handles the communication back to Android Market.
|
||||||
|
* The {@link BillingService} also reports state changes back to the application through
|
||||||
|
* the {@link ResponseHandler}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (BillingConstants.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
|
||||||
|
String signedData = intent.getStringExtra(BillingConstants.INAPP_SIGNED_DATA);
|
||||||
|
String signature = intent.getStringExtra(BillingConstants.INAPP_SIGNATURE);
|
||||||
|
purchaseStateChanged(context, signedData, signature);
|
||||||
|
} else if (BillingConstants.ACTION_NOTIFY.equals(action)) {
|
||||||
|
String notifyId = intent.getStringExtra(BillingConstants.NOTIFICATION_ID);
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "notifyId: " + notifyId); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
notify(context, notifyId);
|
||||||
|
} else if (BillingConstants.ACTION_RESPONSE_CODE.equals(action)) {
|
||||||
|
long requestId = intent.getLongExtra(BillingConstants.INAPP_REQUEST_ID, -1);
|
||||||
|
int responseCodeIndex = intent.getIntExtra(BillingConstants.INAPP_RESPONSE_CODE,
|
||||||
|
ResponseCode.RESULT_ERROR.ordinal());
|
||||||
|
checkResponseCode(context, requestId, responseCodeIndex);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "unexpected action: " + action); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when Android Market sends information about a purchase state
|
||||||
|
* change. The signedData parameter is a plaintext JSON string that is
|
||||||
|
* signed by the server with the developer's private key. The signature
|
||||||
|
* for the signed data is passed in the signature parameter.
|
||||||
|
* @param context the context
|
||||||
|
* @param signedData the (unencrypted) JSON string
|
||||||
|
* @param signature the signature for the signedData
|
||||||
|
*/
|
||||||
|
private void purchaseStateChanged(Context context, String signedData, String signature) {
|
||||||
|
Intent intent = new Intent(BillingConstants.ACTION_PURCHASE_STATE_CHANGED);
|
||||||
|
intent.setClass(context, BillingService.class);
|
||||||
|
intent.putExtra(BillingConstants.INAPP_SIGNED_DATA, signedData);
|
||||||
|
intent.putExtra(BillingConstants.INAPP_SIGNATURE, signature);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when Android Market sends a "notify" message indicating that transaction
|
||||||
|
* information is available. The request includes a nonce (random number used once) that
|
||||||
|
* we generate and Android Market signs and sends back to us with the purchase state and
|
||||||
|
* other transaction details. This BroadcastReceiver cannot bind to the
|
||||||
|
* MarketBillingService directly so it starts the {@link BillingService}, which does the
|
||||||
|
* actual work of sending the message.
|
||||||
|
*
|
||||||
|
* @param context the context
|
||||||
|
* @param notifyId the notification ID
|
||||||
|
*/
|
||||||
|
private void notify(Context context, String notifyId) {
|
||||||
|
Intent intent = new Intent(BillingConstants.ACTION_GET_PURCHASE_INFORMATION);
|
||||||
|
intent.setClass(context, BillingService.class);
|
||||||
|
intent.putExtra(BillingConstants.NOTIFICATION_ID, notifyId);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when Android Market sends a server response code. The BillingService can
|
||||||
|
* then report the status of the response if desired.
|
||||||
|
*
|
||||||
|
* @param context the context
|
||||||
|
* @param requestId the request ID that corresponds to a previous request
|
||||||
|
* @param responseCodeIndex the ResponseCode ordinal value for the request
|
||||||
|
*/
|
||||||
|
private void checkResponseCode(Context context, long requestId, int responseCodeIndex) {
|
||||||
|
Intent intent = new Intent(BillingConstants.ACTION_RESPONSE_CODE);
|
||||||
|
intent.setClass(context, BillingService.class);
|
||||||
|
intent.putExtra(BillingConstants.INAPP_REQUEST_ID, requestId);
|
||||||
|
intent.putExtra(BillingConstants.INAPP_RESPONSE_CODE, responseCodeIndex);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,611 @@
|
|||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.vending.billing.IMarketBillingService;
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
|
||||||
|
import com.todoroo.astrid.billing.Security.VerifiedPurchase;
|
||||||
|
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
public class BillingService extends Service implements ServiceConnection {
|
||||||
|
private static final String TAG = "billing-service";
|
||||||
|
|
||||||
|
/** The service connection to the remote MarketBillingService. */
|
||||||
|
private static IMarketBillingService mService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of requests that are pending while we are waiting for the
|
||||||
|
* connection to the MarketBillingService to be established.
|
||||||
|
*/
|
||||||
|
private static LinkedList<BillingRequest> mPendingRequests = new LinkedList<BillingRequest>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of requests that we have sent to Android Market but for which we have
|
||||||
|
* not yet received a response code. The HashMap is indexed by the
|
||||||
|
* request Id that each request receives when it executes.
|
||||||
|
*/
|
||||||
|
private static HashMap<Long, BillingRequest> mSentRequests =
|
||||||
|
new HashMap<Long, BillingRequest>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base class for all requests that use the MarketBillingService.
|
||||||
|
* Each derived class overrides the run() method to call the appropriate
|
||||||
|
* service interface. If we are already connected to the MarketBillingService,
|
||||||
|
* then we call the run() method directly. Otherwise, we bind
|
||||||
|
* to the service and save the request on a queue to be run later when
|
||||||
|
* the service is connected.
|
||||||
|
*/
|
||||||
|
abstract class BillingRequest {
|
||||||
|
private final int mStartId;
|
||||||
|
protected long mRequestId;
|
||||||
|
|
||||||
|
public BillingRequest(int startId) {
|
||||||
|
mStartId = startId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStartId() {
|
||||||
|
return mStartId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the request, starting the connection if necessary.
|
||||||
|
* @return true if the request was executed or queued; false if there
|
||||||
|
* was an error starting the connection
|
||||||
|
*/
|
||||||
|
public boolean runRequest() {
|
||||||
|
if (runIfConnected()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindToMarketBillingService()) {
|
||||||
|
// Add a pending request to run when the service is connected.
|
||||||
|
mPendingRequests.add(this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try running the request directly if the service is already connected.
|
||||||
|
* @return true if the request ran successfully; false if the service
|
||||||
|
* is not connected or there was an error when trying to use it
|
||||||
|
*/
|
||||||
|
public boolean runIfConnected() {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
if (mService != null) {
|
||||||
|
try {
|
||||||
|
mRequestId = run();
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, "request id: " + mRequestId);
|
||||||
|
}
|
||||||
|
if (mRequestId >= 0) {
|
||||||
|
mSentRequests.put(mRequestId, this);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
onRemoteException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a remote exception occurs while trying to execute the
|
||||||
|
* {@link #run()} method. The derived class can override this to
|
||||||
|
* execute exception-handling code.
|
||||||
|
* @param e the exception
|
||||||
|
*/
|
||||||
|
protected void onRemoteException(RemoteException e) {
|
||||||
|
Log.w(TAG, "remote billing service crashed");
|
||||||
|
mService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The derived class must implement this method.
|
||||||
|
* @throws RemoteException
|
||||||
|
*/
|
||||||
|
abstract protected long run() throws RemoteException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when Android Market sends a response code for this
|
||||||
|
* request.
|
||||||
|
* @param responseCode the response code
|
||||||
|
*/
|
||||||
|
protected void responseCodeReceived(ResponseCode responseCode) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Bundle makeRequestBundle(String method) {
|
||||||
|
Bundle request = new Bundle();
|
||||||
|
request.putString(BillingConstants.BILLING_REQUEST_METHOD, method);
|
||||||
|
request.putInt(BillingConstants.BILLING_REQUEST_API_VERSION, 2);
|
||||||
|
request.putString(BillingConstants.BILLING_REQUEST_PACKAGE_NAME, getPackageName());
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void logResponseCode(String method, Bundle response) {
|
||||||
|
ResponseCode responseCode = ResponseCode.valueOf(
|
||||||
|
response.getInt(BillingConstants.BILLING_RESPONSE_RESPONSE_CODE));
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.e(TAG, method + " received " + responseCode.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class that checks if in-app billing is supported.
|
||||||
|
*
|
||||||
|
* Note: Support for subscriptions implies support for one-time purchases. However, the opposite
|
||||||
|
* is not true.
|
||||||
|
*
|
||||||
|
* Developers may want to perform two checks if both one-time and subscription products are
|
||||||
|
* available.
|
||||||
|
*/
|
||||||
|
class CheckBillingSupported extends BillingRequest {
|
||||||
|
public String mProductType = null;
|
||||||
|
|
||||||
|
/** Constructor
|
||||||
|
*
|
||||||
|
* Note: Support for subscriptions implies support for one-time purchases. However, the
|
||||||
|
* opposite is not true.
|
||||||
|
*
|
||||||
|
* Developers may want to perform two checks if both one-time and subscription products are
|
||||||
|
* available.
|
||||||
|
*
|
||||||
|
* @pram itemType Either BillingConstants.ITEM_TYPE_INAPP or BillingConstants.ITEM_TYPE_SUBSCRIPTION, indicating
|
||||||
|
* the type of item support is being checked for.
|
||||||
|
*/
|
||||||
|
public CheckBillingSupported(String itemType) {
|
||||||
|
super(-1);
|
||||||
|
mProductType = itemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long run() throws RemoteException {
|
||||||
|
Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED");
|
||||||
|
if (mProductType != null) {
|
||||||
|
request.putString(BillingConstants.BILLING_REQUEST_ITEM_TYPE, mProductType);
|
||||||
|
}
|
||||||
|
Bundle response = mService.sendBillingRequest(request);
|
||||||
|
int responseCode = response.getInt(BillingConstants.BILLING_RESPONSE_RESPONSE_CODE);
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "CheckBillingSupported response code: " +
|
||||||
|
ResponseCode.valueOf(responseCode));
|
||||||
|
}
|
||||||
|
boolean billingSupported = (responseCode == ResponseCode.RESULT_OK.ordinal());
|
||||||
|
ResponseHandler.checkBillingSupportedResponse(billingSupported, mProductType);
|
||||||
|
return BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class that requests a purchase.
|
||||||
|
*/
|
||||||
|
class RequestPurchase extends BillingRequest {
|
||||||
|
public final String mProductId;
|
||||||
|
public final String mDeveloperPayload;
|
||||||
|
public final String mProductType;
|
||||||
|
|
||||||
|
/** Constructor
|
||||||
|
*
|
||||||
|
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
|
||||||
|
* purchase.
|
||||||
|
* @param itemType Either BillingConstants.ITEM_TYPE_INAPP or BillingConstants.ITEM_TYPE_SUBSCRIPTION,
|
||||||
|
* indicating the type of item type support is being checked for.
|
||||||
|
* @param developerPayload Optional data.
|
||||||
|
*/
|
||||||
|
public RequestPurchase(String itemId, String itemType, String developerPayload) {
|
||||||
|
// This object is never created as a side effect of starting this
|
||||||
|
// service so we pass -1 as the startId to indicate that we should
|
||||||
|
// not stop this service after executing this request.
|
||||||
|
super(-1);
|
||||||
|
mProductId = itemId;
|
||||||
|
mDeveloperPayload = developerPayload;
|
||||||
|
mProductType = itemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long run() throws RemoteException {
|
||||||
|
Bundle request = makeRequestBundle("REQUEST_PURCHASE");
|
||||||
|
request.putString(BillingConstants.BILLING_REQUEST_ITEM_ID, mProductId);
|
||||||
|
request.putString(BillingConstants.BILLING_REQUEST_ITEM_TYPE, mProductType);
|
||||||
|
// Note that the developer payload is optional.
|
||||||
|
if (mDeveloperPayload != null) {
|
||||||
|
request.putString(BillingConstants.BILLING_REQUEST_DEVELOPER_PAYLOAD, mDeveloperPayload);
|
||||||
|
}
|
||||||
|
Bundle response = mService.sendBillingRequest(request);
|
||||||
|
PendingIntent pendingIntent
|
||||||
|
= response.getParcelable(BillingConstants.BILLING_RESPONSE_PURCHASE_INTENT);
|
||||||
|
if (pendingIntent == null) {
|
||||||
|
Log.e(TAG, "Error with requestPurchase");
|
||||||
|
return BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = new Intent();
|
||||||
|
ResponseHandler.buyPageIntentResponse(pendingIntent, intent);
|
||||||
|
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
|
||||||
|
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void responseCodeReceived(ResponseCode responseCode) {
|
||||||
|
ResponseHandler.responseCodeReceived(BillingService.this, this, responseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class that confirms a list of notifications to the server.
|
||||||
|
*/
|
||||||
|
class ConfirmNotifications extends BillingRequest {
|
||||||
|
final String[] mNotifyIds;
|
||||||
|
|
||||||
|
public ConfirmNotifications(int startId, String[] notifyIds) {
|
||||||
|
super(startId);
|
||||||
|
mNotifyIds = notifyIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long run() throws RemoteException {
|
||||||
|
Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS");
|
||||||
|
request.putStringArray(BillingConstants.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
|
||||||
|
Bundle response = mService.sendBillingRequest(request);
|
||||||
|
logResponseCode("confirmNotifications", response);
|
||||||
|
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
|
||||||
|
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class that sends a GET_PURCHASE_INFORMATION message to the server.
|
||||||
|
*/
|
||||||
|
class GetPurchaseInformation extends BillingRequest {
|
||||||
|
long mNonce;
|
||||||
|
final String[] mNotifyIds;
|
||||||
|
|
||||||
|
public GetPurchaseInformation(int startId, String[] notifyIds) {
|
||||||
|
super(startId);
|
||||||
|
mNotifyIds = notifyIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long run() throws RemoteException {
|
||||||
|
mNonce = Security.generateNonce();
|
||||||
|
|
||||||
|
Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION");
|
||||||
|
request.putLong(BillingConstants.BILLING_REQUEST_NONCE, mNonce);
|
||||||
|
request.putStringArray(BillingConstants.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
|
||||||
|
Bundle response = mService.sendBillingRequest(request);
|
||||||
|
logResponseCode("getPurchaseInformation", response);
|
||||||
|
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
|
||||||
|
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRemoteException(RemoteException e) {
|
||||||
|
super.onRemoteException(e);
|
||||||
|
Security.removeNonce(mNonce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class that sends a RESTORE_TRANSACTIONS message to the server.
|
||||||
|
*/
|
||||||
|
class RestoreTransactions extends BillingRequest {
|
||||||
|
long mNonce;
|
||||||
|
|
||||||
|
public RestoreTransactions() {
|
||||||
|
// This object is never created as a side effect of starting this
|
||||||
|
// service so we pass -1 as the startId to indicate that we should
|
||||||
|
// not stop this service after executing this request.
|
||||||
|
super(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long run() throws RemoteException {
|
||||||
|
mNonce = Security.generateNonce();
|
||||||
|
|
||||||
|
Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS");
|
||||||
|
request.putLong(BillingConstants.BILLING_REQUEST_NONCE, mNonce);
|
||||||
|
Bundle response = mService.sendBillingRequest(request);
|
||||||
|
logResponseCode("restoreTransactions", response);
|
||||||
|
return response.getLong(BillingConstants.BILLING_RESPONSE_REQUEST_ID,
|
||||||
|
BillingConstants.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRemoteException(RemoteException e) {
|
||||||
|
super.onRemoteException(e);
|
||||||
|
Security.removeNonce(mNonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void responseCodeReceived(ResponseCode responseCode) {
|
||||||
|
ResponseHandler.responseCodeReceived(BillingService.this, this, responseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BillingService() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContext(Context context) {
|
||||||
|
attachBaseContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We don't support binding to this service, only starting the service.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart(Intent intent, int startId) {
|
||||||
|
handleCommand(intent, startId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link BillingReceiver} sends messages to this service using intents.
|
||||||
|
* Each intent has an action and some extra arguments specific to that action.
|
||||||
|
* @param intent the intent containing one of the supported actions
|
||||||
|
* @param startId an identifier for the invocation instance of this service
|
||||||
|
*/
|
||||||
|
public void handleCommand(Intent intent, int startId) {
|
||||||
|
if (intent == null)
|
||||||
|
return;
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "handleCommand() action: " + action);
|
||||||
|
}
|
||||||
|
if (BillingConstants.ACTION_CONFIRM_NOTIFICATION.equals(action)) {
|
||||||
|
String[] notifyIds = intent.getStringArrayExtra(BillingConstants.NOTIFICATION_ID);
|
||||||
|
confirmNotifications(startId, notifyIds);
|
||||||
|
} else if (BillingConstants.ACTION_GET_PURCHASE_INFORMATION.equals(action)) {
|
||||||
|
String notifyId = intent.getStringExtra(BillingConstants.NOTIFICATION_ID);
|
||||||
|
getPurchaseInformation(startId, new String[] { notifyId });
|
||||||
|
} else if (BillingConstants.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
|
||||||
|
String signedData = intent.getStringExtra(BillingConstants.INAPP_SIGNED_DATA);
|
||||||
|
String signature = intent.getStringExtra(BillingConstants.INAPP_SIGNATURE);
|
||||||
|
purchaseStateChanged(startId, signedData, signature);
|
||||||
|
} else if (BillingConstants.ACTION_RESPONSE_CODE.equals(action)) {
|
||||||
|
long requestId = intent.getLongExtra(BillingConstants.INAPP_REQUEST_ID, -1);
|
||||||
|
int responseCodeIndex = intent.getIntExtra(BillingConstants.INAPP_RESPONSE_CODE,
|
||||||
|
ResponseCode.RESULT_ERROR.ordinal());
|
||||||
|
ResponseCode responseCode = ResponseCode.valueOf(responseCodeIndex);
|
||||||
|
checkResponseCode(requestId, responseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds to the MarketBillingService and returns true if the bind
|
||||||
|
* succeeded.
|
||||||
|
* @return true if the bind succeeded; false otherwise
|
||||||
|
*/
|
||||||
|
private boolean bindToMarketBillingService() {
|
||||||
|
try {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "binding to Market billing service");
|
||||||
|
}
|
||||||
|
boolean bindResult = bindService(
|
||||||
|
new Intent(BillingConstants.MARKET_BILLING_SERVICE_ACTION),
|
||||||
|
this, // ServiceConnection.
|
||||||
|
Context.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
if (bindResult) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Could not bind to service.");
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Log.e(TAG, "Security exception: " + e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if in-app billing is supported.
|
||||||
|
* @pram itemType Either BillingConstants.ITEM_TYPE_INAPP or BillingConstants.ITEM_TYPE_SUBSCRIPTION, indicating the
|
||||||
|
* type of item support is being checked for.
|
||||||
|
* @return true if supported; false otherwise
|
||||||
|
*/
|
||||||
|
public boolean checkBillingSupported(String itemType) {
|
||||||
|
return new CheckBillingSupported(itemType).runRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests that the given item be offered to the user for purchase. When
|
||||||
|
* the purchase succeeds (or is canceled) the {@link BillingReceiver}
|
||||||
|
* receives an intent with the action {@link BillingConstants#ACTION_NOTIFY}.
|
||||||
|
* Returns false if there was an error trying to connect to Android Market.
|
||||||
|
* @param productId an identifier for the item being offered for purchase
|
||||||
|
* @param itemType Either BillingConstants.ITEM_TYPE_INAPP or BillingConstants.ITEM_TYPE_SUBSCRIPTION, indicating
|
||||||
|
* the type of item type support is being checked for.
|
||||||
|
* @param developerPayload a payload that is associated with a given
|
||||||
|
* purchase, if null, no payload is sent
|
||||||
|
* @return false if there was an error connecting to Android Market
|
||||||
|
*/
|
||||||
|
public boolean requestPurchase(String productId, String itemType, String developerPayload) {
|
||||||
|
return new RequestPurchase(productId, itemType, developerPayload).runRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests transaction information for all managed items. Call this only when the
|
||||||
|
* application is first installed or after a database wipe. Do NOT call this
|
||||||
|
* every time the application starts up.
|
||||||
|
* @return false if there was an error connecting to Android Market
|
||||||
|
*/
|
||||||
|
public boolean restoreTransactions() {
|
||||||
|
return new RestoreTransactions().runRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms receipt of a purchase state change. Each {@code notifyId} is
|
||||||
|
* an opaque identifier that came from the server. This method sends those
|
||||||
|
* identifiers back to the MarketBillingService, which ACKs them to the
|
||||||
|
* server. Returns false if there was an error trying to connect to the
|
||||||
|
* MarketBillingService.
|
||||||
|
* @param startId an identifier for the invocation instance of this service
|
||||||
|
* @param notifyIds a list of opaque identifiers associated with purchase
|
||||||
|
* state changes.
|
||||||
|
* @return false if there was an error connecting to Market
|
||||||
|
*/
|
||||||
|
private boolean confirmNotifications(int startId, String[] notifyIds) {
|
||||||
|
return new ConfirmNotifications(startId, notifyIds).runRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the purchase information. This message includes a list of
|
||||||
|
* notification IDs sent to us by Android Market, which we include in
|
||||||
|
* our request. The server responds with the purchase information,
|
||||||
|
* encoded as a JSON string, and sends that to the {@link BillingReceiver}
|
||||||
|
* in an intent with the action {@link BillingConstants#ACTION_PURCHASE_STATE_CHANGED}.
|
||||||
|
* Returns false if there was an error trying to connect to the MarketBillingService.
|
||||||
|
*
|
||||||
|
* @param startId an identifier for the invocation instance of this service
|
||||||
|
* @param notifyIds a list of opaque identifiers associated with purchase
|
||||||
|
* state changes
|
||||||
|
* @return false if there was an error connecting to Android Market
|
||||||
|
*/
|
||||||
|
private boolean getPurchaseInformation(int startId, String[] notifyIds) {
|
||||||
|
return new GetPurchaseInformation(startId, notifyIds).runRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the data was signed with the given signature, and calls
|
||||||
|
* {@link ResponseHandler#purchaseResponse(Context, PurchaseState, String, String, long)}
|
||||||
|
* for each verified purchase.
|
||||||
|
* @param startId an identifier for the invocation instance of this service
|
||||||
|
* @param signedData the signed JSON string (signed, not encrypted)
|
||||||
|
* @param signature the signature for the data, signed with the private key
|
||||||
|
*/
|
||||||
|
private void purchaseStateChanged(int startId, String signedData, String signature) {
|
||||||
|
ArrayList<Security.VerifiedPurchase> purchases;
|
||||||
|
purchases = Security.verifyPurchase(signedData, signature);
|
||||||
|
if (purchases == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> notifyList = new ArrayList<String>();
|
||||||
|
for (VerifiedPurchase vp : purchases) {
|
||||||
|
if (vp.notificationId != null) {
|
||||||
|
notifyList.add(vp.notificationId);
|
||||||
|
}
|
||||||
|
ResponseHandler.purchaseResponse(this, vp.purchaseState, vp.productId,
|
||||||
|
vp.orderId, vp.purchaseTime, vp.developerPayload, vp.purchaseToken);
|
||||||
|
}
|
||||||
|
if (!notifyList.isEmpty()) {
|
||||||
|
String[] notifyIds = notifyList.toArray(new String[notifyList.size()]);
|
||||||
|
confirmNotifications(startId, notifyIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when we receive a response code from Android Market for a request
|
||||||
|
* that we made. This is used for reporting various errors and for
|
||||||
|
* acknowledging that an order was sent to the server. This is NOT used
|
||||||
|
* for any purchase state changes. All purchase state changes are received
|
||||||
|
* in the {@link BillingReceiver} and passed to this service, where they are
|
||||||
|
* handled in {@link #purchaseStateChanged(int, String, String)}.
|
||||||
|
* @param requestId a number that identifies a request, assigned at the
|
||||||
|
* time the request was made to Android Market
|
||||||
|
* @param responseCode a response code from Android Market to indicate the state
|
||||||
|
* of the request
|
||||||
|
*/
|
||||||
|
private void checkResponseCode(long requestId, ResponseCode responseCode) {
|
||||||
|
BillingRequest request = mSentRequests.get(requestId);
|
||||||
|
if (request != null) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, request.getClass().getSimpleName() + ": " + responseCode);
|
||||||
|
}
|
||||||
|
request.responseCodeReceived(responseCode);
|
||||||
|
}
|
||||||
|
mSentRequests.remove(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs any pending requests that are waiting for a connection to the
|
||||||
|
* service to be established. This runs in the main UI thread.
|
||||||
|
*/
|
||||||
|
private void runPendingRequests() {
|
||||||
|
int maxStartId = -1;
|
||||||
|
BillingRequest request;
|
||||||
|
while ((request = mPendingRequests.peek()) != null) {
|
||||||
|
if (request.runIfConnected()) {
|
||||||
|
// Remove the request
|
||||||
|
mPendingRequests.remove();
|
||||||
|
|
||||||
|
// Remember the largest startId, which is the most recent
|
||||||
|
// request to start this service.
|
||||||
|
if (maxStartId < request.getStartId()) {
|
||||||
|
maxStartId = request.getStartId();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The service crashed, so restart it. Note that this leaves
|
||||||
|
// the current request on the queue.
|
||||||
|
bindToMarketBillingService();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here then all the requests ran successfully. If maxStartId
|
||||||
|
// is not -1, then one of the requests started the service, so we can
|
||||||
|
// stop it now.
|
||||||
|
if (maxStartId >= 0) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.i(TAG, "stopping service, startId: " + maxStartId);
|
||||||
|
}
|
||||||
|
stopSelf(maxStartId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when we are connected to the MarketBillingService.
|
||||||
|
* This runs in the main UI thread.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, "Billing service connected");
|
||||||
|
}
|
||||||
|
mService = IMarketBillingService.Stub.asInterface(service);
|
||||||
|
runPendingRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when we are disconnected from the MarketBillingService.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
Log.w(TAG, "Billing service disconnected");
|
||||||
|
mService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unbinds from the MarketBillingService. Call this when the application
|
||||||
|
* terminates to avoid leaking a ServiceConnection.
|
||||||
|
*/
|
||||||
|
public void unbind() {
|
||||||
|
try {
|
||||||
|
unbindService(this);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This might happen if the service was disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.PendingIntent.CanceledException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentSender;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
|
||||||
|
import com.todoroo.astrid.billing.BillingService.RequestPurchase;
|
||||||
|
import com.todoroo.astrid.billing.BillingService.RestoreTransactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for observing changes related to purchases. The main application
|
||||||
|
* extends this class and registers an instance of that derived class with
|
||||||
|
* {@link ResponseHandler}. The main application implements the callbacks
|
||||||
|
* {@link #onBillingSupported(boolean)} and
|
||||||
|
* {@link #onPurchaseStateChange(PurchaseState, String, int, long)}. These methods
|
||||||
|
* are used to update the UI.
|
||||||
|
*/
|
||||||
|
public abstract class PurchaseObserver {
|
||||||
|
protected static final String TAG = "purchase-observer"; //$NON-NLS-1$
|
||||||
|
protected final Activity mActivity;
|
||||||
|
private final Handler mHandler;
|
||||||
|
private Method mStartIntentSender;
|
||||||
|
private final Object[] mStartIntentSenderArgs = new Object[5];
|
||||||
|
private static final Class<?>[] START_INTENT_SENDER_SIG = new Class[] {
|
||||||
|
IntentSender.class, Intent.class, int.class, int.class, int.class
|
||||||
|
};
|
||||||
|
|
||||||
|
public PurchaseObserver(Activity activity, Handler handler) {
|
||||||
|
mActivity = activity;
|
||||||
|
mHandler = handler;
|
||||||
|
initCompatibilityLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the callback that is invoked when Android Market responds to the
|
||||||
|
* {@link BillingService#checkBillingSupported()} request.
|
||||||
|
* @param supported true if in-app billing is supported.
|
||||||
|
*/
|
||||||
|
public abstract void onBillingSupported(boolean supported, String type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the callback that is invoked when an item is purchased,
|
||||||
|
* refunded, or canceled. It is the callback invoked in response to
|
||||||
|
* calling {@link BillingService#requestPurchase(String)}. It may also
|
||||||
|
* be invoked asynchronously when a purchase is made on another device
|
||||||
|
* (if the purchase was for a Market-managed item), or if the purchase
|
||||||
|
* was refunded, or the charge was canceled. This handles the UI
|
||||||
|
* update. The database update is handled in
|
||||||
|
* {@link ResponseHandler#purchaseResponse(Context, PurchaseState,
|
||||||
|
* String, String, long)}.
|
||||||
|
* @param purchaseState the purchase state of the item
|
||||||
|
* @param itemId a string identifying the item (the "SKU")
|
||||||
|
* @param quantity the current quantity of this item after the purchase
|
||||||
|
* @param purchaseTime the time the product was purchased, in
|
||||||
|
* milliseconds since the epoch (Jan 1, 1970)
|
||||||
|
*/
|
||||||
|
public abstract void onPurchaseStateChange(PurchaseState purchaseState,
|
||||||
|
String itemId, int quantity, long purchaseTime, String developerPayload, String purchaseToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when we receive a response code from Market for a
|
||||||
|
* RequestPurchase request that we made. This is NOT used for any
|
||||||
|
* purchase state changes. All purchase state changes are received in
|
||||||
|
* {@link #onPurchaseStateChange(PurchaseState, String, int, long)}.
|
||||||
|
* This is used for reporting various errors, or if the user backed out
|
||||||
|
* and didn't purchase the item. The possible response codes are:
|
||||||
|
* RESULT_OK means that the order was sent successfully to the server.
|
||||||
|
* The onPurchaseStateChange() will be invoked later (with a
|
||||||
|
* purchase state of PURCHASED or CANCELED) when the order is
|
||||||
|
* charged or canceled. This response code can also happen if an
|
||||||
|
* order for a Market-managed item was already sent to the server.
|
||||||
|
* RESULT_USER_CANCELED means that the user didn't buy the item.
|
||||||
|
* RESULT_SERVICE_UNAVAILABLE means that we couldn't connect to the
|
||||||
|
* Android Market server (for example if the data connection is down).
|
||||||
|
* RESULT_BILLING_UNAVAILABLE means that in-app billing is not
|
||||||
|
* supported yet.
|
||||||
|
* RESULT_ITEM_UNAVAILABLE means that the item this app offered for
|
||||||
|
* sale does not exist (or is not published) in the server-side
|
||||||
|
* catalog.
|
||||||
|
* RESULT_ERROR is used for any other errors (such as a server error).
|
||||||
|
*/
|
||||||
|
public abstract void onRequestPurchaseResponse(RequestPurchase request,
|
||||||
|
ResponseCode responseCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when we receive a response code from Android Market for a
|
||||||
|
* RestoreTransactions request that we made. A response code of
|
||||||
|
* RESULT_OK means that the request was successfully sent to the server.
|
||||||
|
*/
|
||||||
|
public abstract void onRestoreTransactionsResponse(RestoreTransactions request,
|
||||||
|
ResponseCode responseCode);
|
||||||
|
|
||||||
|
private void initCompatibilityLayer() {
|
||||||
|
try {
|
||||||
|
mStartIntentSender = mActivity.getClass().getMethod("startIntentSender", //$NON-NLS-1$
|
||||||
|
START_INTENT_SENDER_SIG);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
mStartIntentSender = null;
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
mStartIntentSender = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void startBuyPageActivity(PendingIntent pendingIntent, Intent intent) {
|
||||||
|
if (mStartIntentSender != null) {
|
||||||
|
// This is on Android 2.0 and beyond. The in-app buy page activity
|
||||||
|
// must be on the activity stack of the application.
|
||||||
|
try {
|
||||||
|
// This implements the method call:
|
||||||
|
// mActivity.startIntentSender(pendingIntent.getIntentSender(),
|
||||||
|
// intent, 0, 0, 0);
|
||||||
|
mStartIntentSenderArgs[0] = pendingIntent.getIntentSender();
|
||||||
|
mStartIntentSenderArgs[1] = intent;
|
||||||
|
mStartIntentSenderArgs[2] = Integer.valueOf(0);
|
||||||
|
mStartIntentSenderArgs[3] = Integer.valueOf(0);
|
||||||
|
mStartIntentSenderArgs[4] = Integer.valueOf(0);
|
||||||
|
mStartIntentSender.invoke(mActivity, mStartIntentSenderArgs);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "error starting activity", e); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is on Android version 1.6. The in-app buy page activity must be on its
|
||||||
|
// own separate activity stack instead of on the activity stack of
|
||||||
|
// the application.
|
||||||
|
try {
|
||||||
|
pendingIntent.send(mActivity, 0 /* code */, intent);
|
||||||
|
} catch (CanceledException e) {
|
||||||
|
Log.e(TAG, "error starting activity", e); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the UI after the database has been updated. This method runs
|
||||||
|
* in a background thread so it has to post a Runnable to run on the UI
|
||||||
|
* thread.
|
||||||
|
* @param purchaseState the purchase state of the item
|
||||||
|
* @param itemId a string identifying the item
|
||||||
|
* @param quantity the quantity of items in this purchase
|
||||||
|
*/
|
||||||
|
void postPurchaseStateChange(final PurchaseState purchaseState, final String itemId,
|
||||||
|
final int quantity, final long purchaseTime, final String developerPayload, final String purchaseToken) {
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
onPurchaseStateChange(
|
||||||
|
purchaseState, itemId, quantity, purchaseTime, developerPayload, purchaseToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.ResponseCode;
|
||||||
|
import com.todoroo.astrid.billing.BillingService.RequestPurchase;
|
||||||
|
import com.todoroo.astrid.billing.BillingService.RestoreTransactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains the methods that handle responses from Android Market. The
|
||||||
|
* implementation of these methods is specific to a particular application.
|
||||||
|
* The methods in this example update the database and, if the main application
|
||||||
|
* has registered a {@llink PurchaseObserver}, will also update the UI. An
|
||||||
|
* application might also want to forward some responses on to its own server,
|
||||||
|
* and that could be done here (in a background thread) but this example does
|
||||||
|
* not do that.
|
||||||
|
*
|
||||||
|
* You should modify and obfuscate this code before using it.
|
||||||
|
*/
|
||||||
|
public class ResponseHandler {
|
||||||
|
private static final String TAG = "response-handler"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a static instance of {@link PurchaseObserver} that the
|
||||||
|
* application creates and registers with this class. The PurchaseObserver
|
||||||
|
* is used for updating the UI if the UI is visible.
|
||||||
|
*/
|
||||||
|
private static PurchaseObserver sPurchaseObserver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an observer that updates the UI.
|
||||||
|
* @param observer the observer to register
|
||||||
|
*/
|
||||||
|
public static synchronized void register(PurchaseObserver observer) {
|
||||||
|
sPurchaseObserver = observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a previously registered observer.
|
||||||
|
* @param observer the previously registered observer.
|
||||||
|
*/
|
||||||
|
public static synchronized void unregister(PurchaseObserver observer) {
|
||||||
|
sPurchaseObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the application of the availability of the MarketBillingService.
|
||||||
|
* This method is called in response to the application calling
|
||||||
|
* {@link BillingService#checkBillingSupported()}.
|
||||||
|
* @param supported true if in-app billing is supported.
|
||||||
|
*/
|
||||||
|
public static void checkBillingSupportedResponse(boolean supported, String type) {
|
||||||
|
if (sPurchaseObserver != null) {
|
||||||
|
sPurchaseObserver.onBillingSupported(supported, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new activity for the user to buy an item for sale. This method
|
||||||
|
* forwards the intent on to the PurchaseObserver (if it exists) because
|
||||||
|
* we need to start the activity on the activity stack of the application.
|
||||||
|
*
|
||||||
|
* @param pendingIntent a PendingIntent that we received from Android Market that
|
||||||
|
* will create the new buy page activity
|
||||||
|
* @param intent an intent containing a request id in an extra field that
|
||||||
|
* will be passed to the buy page activity when it is created
|
||||||
|
*/
|
||||||
|
public static void buyPageIntentResponse(PendingIntent pendingIntent, Intent intent) {
|
||||||
|
if (sPurchaseObserver == null) {
|
||||||
|
if (BillingConstants.DEBUG) {
|
||||||
|
Log.d(TAG, "UI is not running"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sPurchaseObserver.startBuyPageActivity(pendingIntent, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the application of purchase state changes. The application
|
||||||
|
* can offer an item for sale to the user via
|
||||||
|
* {@link BillingService#requestPurchase(String)}. The BillingService
|
||||||
|
* calls this method after it gets the response. Another way this method
|
||||||
|
* can be called is if the user bought something on another device running
|
||||||
|
* this same app. Then Android Market notifies the other devices that
|
||||||
|
* the user has purchased an item, in which case the BillingService will
|
||||||
|
* also call this method. Finally, this method can be called if the item
|
||||||
|
* was refunded.
|
||||||
|
* @param purchaseState the state of the purchase request (PURCHASED,
|
||||||
|
* CANCELED, or REFUNDED)
|
||||||
|
* @param productId a string identifying a product for sale
|
||||||
|
* @param orderId a string identifying the order
|
||||||
|
* @param purchaseTime the time the product was purchased, in milliseconds
|
||||||
|
* since the epoch (Jan 1, 1970)
|
||||||
|
* @param developerPayload the developer provided "payload" associated with
|
||||||
|
* the order
|
||||||
|
*/
|
||||||
|
public static void purchaseResponse(
|
||||||
|
final Context context, final PurchaseState purchaseState, final String productId,
|
||||||
|
final String orderId, final long purchaseTime, final String developerPayload, final String purchaseToken) {
|
||||||
|
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
// This needs to be synchronized because the UI thread can change the
|
||||||
|
// value of sPurchaseObserver.
|
||||||
|
synchronized(ResponseHandler.class) {
|
||||||
|
if (sPurchaseObserver != null) {
|
||||||
|
sPurchaseObserver.postPurchaseStateChange(
|
||||||
|
purchaseState, productId, 1, purchaseTime, developerPayload, purchaseToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when we receive a response code from Android Market for a
|
||||||
|
* RequestPurchase request that we made. This is used for reporting various
|
||||||
|
* errors and also for acknowledging that an order was sent successfully to
|
||||||
|
* the server. This is NOT used for any purchase state changes. All
|
||||||
|
* purchase state changes are received in the {@link BillingReceiver} and
|
||||||
|
* are handled in {@link Security#verifyPurchase(String, String)}.
|
||||||
|
* @param context the context
|
||||||
|
* @param request the RequestPurchase request for which we received a
|
||||||
|
* response code
|
||||||
|
* @param responseCode a response code from Market to indicate the state
|
||||||
|
* of the request
|
||||||
|
*/
|
||||||
|
public static void responseCodeReceived(Context context, RequestPurchase request,
|
||||||
|
ResponseCode responseCode) {
|
||||||
|
if (sPurchaseObserver != null) {
|
||||||
|
sPurchaseObserver.onRequestPurchaseResponse(request, responseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when we receive a response code from Android Market for a
|
||||||
|
* RestoreTransactions request.
|
||||||
|
* @param context the context
|
||||||
|
* @param request the RestoreTransactions request for which we received a
|
||||||
|
* response code
|
||||||
|
* @param responseCode a response code from Market to indicate the state
|
||||||
|
* of the request
|
||||||
|
*/
|
||||||
|
public static void responseCodeReceived(Context context, RestoreTransactions request,
|
||||||
|
ResponseCode responseCode) {
|
||||||
|
if (sPurchaseObserver != null) {
|
||||||
|
sPurchaseObserver.onRestoreTransactionsResponse(request, responseCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.todoroo.astrid.billing;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a stub class
|
||||||
|
* @author Sam
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("nls")
|
||||||
|
public class Security {
|
||||||
|
private static final String TAG = "Security";
|
||||||
|
|
||||||
|
private static final String KEY_FACTORY_ALGORITHM = "RSA";
|
||||||
|
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
private static HashSet<Long> sKnownNonces = new HashSet<Long>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to hold the verified purchase information.
|
||||||
|
*/
|
||||||
|
public static class VerifiedPurchase {
|
||||||
|
public PurchaseState purchaseState;
|
||||||
|
public String notificationId;
|
||||||
|
public String productId;
|
||||||
|
public String orderId;
|
||||||
|
public long purchaseTime;
|
||||||
|
public String developerPayload;
|
||||||
|
public String purchaseToken;
|
||||||
|
|
||||||
|
public VerifiedPurchase(PurchaseState purchaseState, String notificationId,
|
||||||
|
String productId, String orderId, long purchaseTime, String developerPayload, String purchaseToken) {
|
||||||
|
this.purchaseState = purchaseState;
|
||||||
|
this.notificationId = notificationId;
|
||||||
|
this.productId = productId;
|
||||||
|
this.orderId = orderId;
|
||||||
|
this.purchaseTime = purchaseTime;
|
||||||
|
this.developerPayload = developerPayload;
|
||||||
|
this.purchaseToken = purchaseToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generates a nonce (a random number used once). */
|
||||||
|
public static long generateNonce() {
|
||||||
|
long nonce = RANDOM.nextLong();
|
||||||
|
sKnownNonces.add(nonce);
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeNonce(long nonce) {
|
||||||
|
sKnownNonces.remove(nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isNonceKnown(long nonce) {
|
||||||
|
return sKnownNonces.contains(nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String constructPublicKey() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PublicKey generatePublicKey(String encodedPublicKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue