Started with billing classes

pull/14/head
Sam Bosley 14 years ago
parent eff157c9ab
commit 89767a10a4

@ -37,6 +37,9 @@
<!-- required for ics -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- required for in-app billing of premium subscriptions -->
<uses-permission android:name="com.android.vending.BILLING"/>
<!-- ============================================== Exported Permissions = -->
@ -507,7 +510,17 @@
</intent-filter>
</receiver>
<!-- files -->
<!-- premium -->
<service android:name="com.todoroo.astrid.billing.BillingService" />
<receiver android:name="com.todoroo.astrid.billing.BillingReceiver">
<intent-filter>
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
</intent-filter>
</receiver>
<activity android:name="com.todoroo.astrid.files.AACRecordingActivity"
android:configChanges="orientation|screenSize"
android:screenOrientation="portrait"

@ -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,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,92 @@
package com.todoroo.astrid.billing;
@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.example.subscriptions.CONFIRM_NOTIFICATION";
public static final String ACTION_GET_PURCHASE_INFORMATION =
"com.example.subscriptions.GET_PURCHASE_INFORMATION";
public static final String ACTION_RESTORE_TRANSACTIONS =
"com.example.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";
// 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.
REFUNDED; // User received a refund for the order.
// 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,15 @@
package com.todoroo.astrid.billing;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class BillingReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
}
}

@ -0,0 +1,654 @@
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;
import com.todoroo.astrid.utility.Constants;
@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 (Constants.DEBUG) {
Log.d(TAG, getClass().getSimpleName());
}
if (mService != null) {
try {
mRequestId = run();
if (Constants.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 (Constants.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;
// /** Legacy contrustor
// *
// * This constructor is provided for legacy purposes. Assumes the calling application will
// * not be using any features not present in API v1, such as subscriptions.
// */
// @Deprecated
// public CheckBillingSupported() {
// // 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);
// }
/** 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 (Constants.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;
// /** Legacy constructor
// *
// * @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
// * purchase.
// */
// @Deprecated
// public RequestPurchase(String itemId) {
// this(itemId, null, null);
// }
//
// /** Legacy constructor
// *
// * @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
// * purchase.
// * @param developerPayload Optional data.
// */
// @Deprecated
// public RequestPurchase(String itemId, String developerPayload) {
// this(itemId, null, developerPayload);
// }
/** 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) {
String action = intent.getAction();
if (Constants.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 (Constants.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. Assumes this is a one-time purchase.
// *
// * @return true if supported; false otherwise
// */
// @Deprecated
// public boolean checkBillingSupported() {
// return new CheckBillingSupported().runRequest();
// }
/**
* 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);
}
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 (Constants.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 (Constants.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 (Constants.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 {
private static final String TAG = "purchase-observer"; //$NON-NLS-1$
private 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);
/**
* 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) {
mHandler.post(new Runnable() {
@Override
public void run() {
onPurchaseStateChange(
purchaseState, itemId, quantity, purchaseTime, developerPayload);
}
});
}
}

@ -0,0 +1,169 @@
// 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;
import com.todoroo.astrid.utility.Constants;
/**
* 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 (Constants.DEBUG) {
Log.d(TAG, "UI is not running");
}
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) {
// Update the database with the purchase state. We shouldn't do that
// from the main thread so we do the work in a background thread.
// We don't update the UI here. We will update the UI after we update
// the database because we need to read and update the current quantity
// first.
new Thread(new Runnable() {
@Override
public void run() {
// PurchaseDatabase db = new PurchaseDatabase(context);
// int quantity = db.updatePurchase(
// orderId, productId, purchaseState, purchaseTime, developerPayload);
// db.close();
//
// // 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, quantity, purchaseTime, developerPayload);
// }
// }
}
}).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,247 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.todoroo.astrid.billing;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashSet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.text.TextUtils;
import android.util.Log;
import com.todoroo.astrid.billing.BillingConstants.PurchaseState;
import com.todoroo.astrid.utility.Constants;
/**
* Security-related methods. For a secure implementation, all of this code
* should be implemented on a server that communicates with the
* application on the device. For the sake of simplicity and clarity of this
* example, this code is included here and is executed on the device. If you
* must verify the purchases on the phone, you should obfuscate this code to
* make it harder for an attacker to replace the code with stubs that treat all
* purchases as verified.
*/
@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();
/**
* This keeps track of the nonces that we generated and sent to the
* server. We need to keep track of these until we get back the purchase
* state and send a confirmation message back to Android Market. If we are
* killed and lose this list of nonces, it is not fatal. Android Market will
* send us a new "notify" message and we will re-generate a new nonce.
* This has to be "static" so that the {@link BillingReceiver} can
* check if a nonce exists.
*/
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 VerifiedPurchase(PurchaseState purchaseState, String notificationId,
String productId, String orderId, long purchaseTime, String developerPayload) {
this.purchaseState = purchaseState;
this.notificationId = notificationId;
this.productId = productId;
this.orderId = orderId;
this.purchaseTime = purchaseTime;
this.developerPayload = developerPayload;
}
}
/** 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);
}
/**
* Verifies that the data was signed with the given signature, and returns
* the list of verified purchases. The data is in JSON format and contains
* a nonce (number used once) that we generated and that was signed
* (as part of the whole data string) with a private key. The data also
* contains the {@link PurchaseState} and product ID of the purchase.
* In the general case, there can be an array of purchase transactions
* because there may be delays in processing the purchase on the backend
* and then several purchases can be batched together.
* @param signedData the signed JSON string (signed, not encrypted)
* @param signature the signature for the data, signed with the private key
*/
public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature) {
if (signedData == null) {
Log.e(TAG, "data is null");
return null;
}
if (Constants.DEBUG) {
Log.i(TAG, "signedData: " + signedData);
}
boolean verified = false;
if (!TextUtils.isEmpty(signature)) {
/**
* Compute your public key (that you got from the Android Market publisher site).
*
* Instead of just storing the entire literal string here embedded in the
* program, construct the key at runtime from pieces or
* use bit manipulation (for example, XOR with some other string) to hide
* the actual key. The key itself is not secret information, but we don't
* want to make it easy for an adversary to replace the public key with one
* of their own and then fake messages from the server.
*
* Generally, encryption keys / passwords should only be kept in memory
* long enough to perform the operation they need to perform.
*/
String base64EncodedPublicKey = "your public key here";
PublicKey key = Security.generatePublicKey(base64EncodedPublicKey);
verified = Security.verify(key, signedData, signature);
if (!verified) {
Log.w(TAG, "signature does not match data.");
return null;
}
}
JSONObject jObject;
JSONArray jTransactionsArray = null;
int numTransactions = 0;
long nonce = 0L;
try {
jObject = new JSONObject(signedData);
// The nonce might be null if the user backed out of the buy page.
nonce = jObject.optLong("nonce");
jTransactionsArray = jObject.optJSONArray("orders");
if (jTransactionsArray != null) {
numTransactions = jTransactionsArray.length();
}
} catch (JSONException e) {
return null;
}
if (!Security.isNonceKnown(nonce)) {
Log.w(TAG, "Nonce not found: " + nonce);
return null;
}
ArrayList<VerifiedPurchase> purchases = new ArrayList<VerifiedPurchase>();
try {
for (int i = 0; i < numTransactions; i++) {
JSONObject jElement = jTransactionsArray.getJSONObject(i);
int response = jElement.getInt("purchaseState");
PurchaseState purchaseState = PurchaseState.valueOf(response);
String productId = jElement.getString("productId");
String packageName = jElement.getString("packageName");
long purchaseTime = jElement.getLong("purchaseTime");
String orderId = jElement.optString("orderId", "");
String notifyId = null;
if (jElement.has("notificationId")) {
notifyId = jElement.getString("notificationId");
}
String developerPayload = jElement.optString("developerPayload", null);
// If the purchase state is PURCHASED, then we require a
// verified nonce.
if (purchaseState == PurchaseState.PURCHASED && !verified) {
continue;
}
purchases.add(new VerifiedPurchase(purchaseState, notifyId, productId,
orderId, purchaseTime, developerPayload));
}
} catch (JSONException e) {
Log.e(TAG, "JSON exception: ", e);
return null;
}
removeNonce(nonce);
return purchases;
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
throw new IllegalArgumentException(e);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* @param publicKey public key associated with the developer account
* @param signedData signed data from server
* @param signature server signature
* @return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
if (Constants.DEBUG) {
Log.i(TAG, "signature: " + signature);
}
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}
}
Loading…
Cancel
Save