スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Android アプリ内課金の実装方法

Androidでのアプリ内課金の実装方法を記述する。
すごく長くなってしまう(´・ω・`)

が、自分がアプリ内課金を実装しようと思って、いろいろネット上を探してみるが、どれも中途半端な記事しかなかったため、自分が全てを書くことにする。


ちなみに、アプリ内課金を実装する際に、参考にした参考書がこれだ

この参考書の「アプリ内課金」の部分を参考に構築したら、なんとかアプリ内課金の実装ができた。
この参考書オススメです。



■前提条件

まずアプリ内課金を使用するには、Androidのバージョンが2.3以上必要になる。
これは、アプリ内課金の処理を「Google Play Store(旧Androidマーケット)」が代行しているためである。
実はこの「Google Play Store」はAndroidのバージョン2.3からのみ対応しており、2.2以下のバージョンでは、旧Androidマーケットしか利用できないのだ。
※最初に買ったスマホXperiaは2.2までしかバージョンアップできないから困ったもんだ(´・ω・`)

しかし、2012年12月現在では、ほとんどのスマホが2.3以上になっているため、あまり問題はないと思われる。
古い端末でテストなどが出来ないことだけ注意が必要。

■ポイント
先に言ってしまうが、Androidでアプリ内課金を実装するには、多くのコーディングが必要になってくる。
しかし、一度作成したクラスなどは、別のアプリケーションでも流用できるため、1回作ってしまえば、次回からのアプリ構築は簡単なもの。

setumei1222.jpg


■開発クラス(ファイル)一覧
・パッケージ(自分の好きなパッケージでいいよ)
Base64.java
Base64DecoderException.java
BillingReceiver.java
BillingService.java
CatalogEntry.java
Consts.java
PurchaseCallback.java
PurchaseController.java
Security.java

・パッケージ「com.android.vending.billing] これは固定です。
IMarketBillingService.aidl


それではいよいよコーディングの中身に入る。
■Base64.java

Base64.javaはこのままコピペでOK。修正する部分はなし。


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
*/
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;
}
}



■Base64DecoderException.java


public class Base64DecoderException extends Exception {
public Base64DecoderException() {
super();
}

public Base64DecoderException(String s) {
super(s);
}

private static final long serialVersionUID = 1L;
}


■Consts.java
主に課金処理についての定数が定義されている。
基本下記をコピペで問題ない

public class Consts {
// MarketBillingServiceにバインドするためのインテント
public static final String MARKET_BILLING_SERVICE_ACTION =
"com.android.vending.billing.MarketBillingService.BIND";

// 課金リクエストの種類
// (BillingServiceを起動するためのインテント)
public static final String ACTION_CONFIRM_NOTIFICATION =
"montarosu.montarosu.beetles.CONFIRM_NOTIFICATION";
public static final String ACTION_GET_PURCHASE_INFORMATION =
"montarosu.montarosu.beetles.GET_PURCHASE_INFORMATION";
public static final String ACTION_RESTORE_TRANSACTIONS =
"montarosu.montarosu.beetles.RESTORE_TRANSACTIONS";

// アプリ内課金リクエストに設定するパラメータ
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_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 long BILLING_RESPONSE_INVALID_REQUEST_ID = -1;

// 非同期レスポンス
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";

// 非同期レスポンスに含まれるパラメータ
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";

// レスポンスコードの定義
public enum ResponseCode {
RESULT_OK,
RESULT_USER_CANCELED,
RESULT_SERVICE_UNAVAILABLE,
RESULT_BILLING_UNAVAILABLE,
RESULT_ITEM_UNAVAILABLE,
RESULT_DEVELOPER_ERROR,
RESULT_ERROR;

// インデックスから対応するレスポンスコードを返す
public static ResponseCode valueOf(int index) {
ResponseCode[] values = ResponseCode.values();
if (index < 0 || index >= values.length) {
return RESULT_ERROR;
}
return values[index];
}
}

// 購入状態の定義
public enum PurchaseState {
PURCHASED,
CANCELED,
REFUNDED;

// インデックスから対応するレスポンスコードを返す
public static PurchaseState valueOf(int index) {
PurchaseState[] values = PurchaseState.values();
if (index < 0 || index >= values.length) {
return CANCELED;
}
return values[index];
}
}
}


■PurchaseCallback.java

public abstract class PurchaseCallback {
public abstract void onPurchaseComplete();
}


■PurchaseController.java

public class PurchaseController {
private static PurchaseCallback sPurchaseCallback;

public static void regist(PurchaseCallback callback) {
sPurchaseCallback = callback;
}

public static void unregist(PurchaseCallback callback) {
sPurchaseCallback = null;
}

public static void purchaseComplete() {
if (sPurchaseCallback != null) {
sPurchaseCallback.onPurchaseComplete();
}
}
}


■Security.java
ここでは、自分のアプリKeyを設定しなければならない。
設定する箇所は下記を参照していただきたい。

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 sKnownNonces = new HashSet();

/**
* 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 verifyPurchase(String signedData, String signature) {
if (signedData == null) {
logout.out(new Security().getClass(),"onCreate()", "data is null");
return null;
}
logout.out(new Security().getClass(),"onCreate()", "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 = "お主専用のKeyを設定するでおじゃる";
PublicKey key = Security.generatePublicKey(base64EncodedPublicKey);
verified = Security.verify(key, signedData, signature);
if (!verified) {
logout.out(new Security().getClass(),"onCreate()", "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)) {
logout.out(new Security().getClass(),"onCreate()", "Nonce not found: " + nonce);
return null;
}

//JSONを分解
ArrayList purchases = new ArrayList();
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) {
logout.out(new Security().getClass(),"onCreate()", "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) {
logout.out(new Security().getClass(),"onCreate()", "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
logout.out(new Security().getClass(),"onCreate()", "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) {
logout.out(new Security().getClass(),"onCreate()", "signature: " + signature);
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
logout.out(new Security().getClass(),"verify()", "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
logout.out(new Security().getClass(),"verify()", "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
logout.out(new Security().getClass(),"verify()", "Invalid key specification.");
} catch (SignatureException e) {
logout.out(new Security().getClass(),"verify()", "Signature exception.");
} catch (Base64DecoderException e) {
logout.out(new Security().getClass(),"verify()", "Base64 decoding failed.");
}
return false;
}
}


コード途中にある、変数「base64EncodedPublicKey」にアプリ毎のライセンスKeyを設定する。
※ポイント
2012年12月では、この設定するライセンスKeyがアプリごとに設定することになった。
前まではアカウントに対してライセンスKeyは一つだけだったのだが。。
ライセンスKeyの生成にはアプリをデベロッパコンソール上にアップする必要があるため、
ライセンスKeyの生成は後述する。

■CatalogEntry.java
CatalogEntryでは、実際の課金アイテムのIDなどをリスト化している。

public class CatalogEntry {
public String productId; // プロダクトID
public int nameId; // コンテンツ名
public int resId; // リソースID
public String fileName; // 保存ファイル名

public CatalogEntry(String productId, int nameId, int resId, String fileName) {
this.productId = productId;
this.nameId = nameId;
this.resId = resId;
this.fileName = fileName;
}

/***
* コンテンツリスト
*/
public static final CatalogEntry[] CATALOG = new CatalogEntry[] {
new CatalogEntry("coin100", R.string.app_name, -1, ""),
new CatalogEntry("coin400", R.string.app_name, -1, ""),
new CatalogEntry("coin1000", R.string.app_name, -1, ""),
};
}

課金アイテムを購入するために、Google PlayをCallするときに渡すアイテムIDが、「productId」となる。
なので、ここでリスト化するproductIdは、実際にGooglePlay上にも登録する。
ほかに「nameId、resId、fileName」とあるが、参考にしたファイルをそのまま使用していた名残であり、実際には必要ない。productIdのみあればとりあえずOK。
登録は後述する

■BillingReceiver.java
課金サービス用のレシーバ機能一式

public class BillingReceiver extends BroadcastReceiver {
/***
* 非同期応答メッセージ(ブロードキャストインテント)を受信する
*/
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
logout.out(this.getClass(),"onReceive()","action:"+action);
if (Consts.ACTION_NOTIFY.equals(action)) {
// トランザクション情報の通知IDを取得する
String notifyId = intent.getStringExtra(Consts.NOTIFICATION_ID);

// トランザクション情報を取得する
notify(context, notifyId);

} else if (Consts.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
// 署名済みトランザクション情報と署名を取得する
String signedData = intent.getStringExtra(Consts.INAPP_SIGNED_DATA);
String signature = intent.getStringExtra(Consts.INAPP_SIGNATURE);

// トランザクション情報を処理する
purchaseStateChanged(context, signedData, signature);

} else if (Consts.ACTION_RESPONSE_CODE.equals(action)) {
// リクエストIDとレスポンスコードを取得する
long requestId = intent.getLongExtra(Consts.INAPP_REQUEST_ID, -1);
int responseCodeIndex = intent.getIntExtra(Consts.INAPP_RESPONSE_CODE, ResponseCode.RESULT_ERROR.ordinal());

// レスポンスを処理する
checkResponseCode(context, requestId, responseCodeIndex);

}
}

/***
* トランザクション情報を取得する
* (BillingServiceにGET_PURCHASE_INFORMATIONリクエストを送信させる)
*/
private void notify(Context context, String notifyId) {
logout.out(this.getClass(),"notify()","notifyId:"+notifyId);
Intent intent = new Intent(Consts.ACTION_GET_PURCHASE_INFORMATION);
intent.setClass(context, BillingService.class);
intent.putExtra(Consts.NOTIFICATION_ID, notifyId);
context.startService(intent);
}

/***
* トランザクション情報を処理する
* (BillingServiceに署名済みトランザクション情報と署名を渡して処理させる)
*/
private void purchaseStateChanged(Context context, String signedData, String signature) {
logout.out(this.getClass(),"purchaseStateChanged()","signedData:"+signedData);
logout.out(this.getClass(),"purchaseStateChanged()","signature:"+signature);
Intent intent = new Intent(Consts.ACTION_PURCHASE_STATE_CHANGED);
intent.setClass(context, BillingService.class);
intent.putExtra(Consts.INAPP_SIGNED_DATA, signedData);
intent.putExtra(Consts.INAPP_SIGNATURE, signature);
context.startService(intent);
}

/***
* レスポンスを処理する
* (BillingServiceにリクエストIDとレスポンスコードを渡してを処理させる)
*/
private void checkResponseCode(Context context, long requestId, int responseCodeIndex) {
logout.out(this.getClass(),"checkResponseCode()","requestId:"+requestId);
logout.out(this.getClass(),"checkResponseCode()","responseCodeIndex:"+responseCodeIndex);
Intent intent = new Intent(Consts.ACTION_RESPONSE_CODE);
intent.setClass(context, BillingService.class);
intent.putExtra(Consts.INAPP_REQUEST_ID, requestId);
intent.putExtra(Consts.INAPP_RESPONSE_CODE, responseCodeIndex);
context.startService(intent);
}
}



■BillingService.java
これが一番重要なクラス。アプリ内課金処理がgoogle Playで実行された後に、呼び出されるクラス。

public class BillingService extends Service implements ServiceConnection {
private static IMarketBillingService mService;
Context context = null;
// 処理待ちのリクエスト
private static BillingRequest mPendingRequests = null;

public void setContext(Context context) {
attachBaseContext(context);
this.context = context;
//billingHandler = new Handler(billBack);
}



/***
* すべてのリクエストに共通する処理を定義したスーパークラス
*/
abstract class BillingRequest {
// リクエストID
protected long mRequestId;

// リクエストを処理するスレッド
abstract protected long run() throws RemoteException;

// リクエストを処理する
public boolean runRequest() {
// MarketBillingServiceに接続済みならそのままリクエストを処理する
if (runIfConnected()) {
return true;
}

// MarketBillingServiceに接続されていないので接続する
if (bindToMarketBillingService()) {
// 接続が成功したらリクエストをPendingRequestsに追加する
mPendingRequests = this;
return true;
}

return false;
}

// MarketBillingServiceに接続済みならリクエストを処理する
public boolean runIfConnected() {
if (mService != null) {
try {
// リクエストを処理する
mRequestId = run();

// 送信したリクエストのリクエストIDをログに表示する
logout.out(this.getClass(),"runIfConnected()","sent request (requestId:"+mRequestId+")");

return true;
} catch (RemoteException e) {
onRemoteException(e);
}
}
return false;
}

protected void onRemoteException(RemoteException e) {
mService = null;
}

// 基本Bundleを生成する
protected Bundle makeRequestBundle(String method) {
logout.out(this.getClass(),"makeRequestBundle()","method:"+method);
logout.out(this.getClass(),"makeRequestBundle()","getPackageName:"+getPackageName());
Bundle request = new Bundle();
request.putString(Consts.BILLING_REQUEST_METHOD, method);
request.putInt(Consts.BILLING_REQUEST_API_VERSION, 2);
request.putString(Consts.BILLING_REQUEST_PACKAGE_NAME, getPackageName());
return request;
}
}

/***
* MarketBillingServiceにバインドする
*/
private boolean bindToMarketBillingService() {
logout.out(this.getClass(),"bindToMarketBillingService()","MarketBillingServiceにバインドする");
// MarketBillingServiceにバインドする
boolean bindResult = bindService(new Intent(
Consts.MARKET_BILLING_SERVICE_ACTION), this,
Context.BIND_AUTO_CREATE);

if (bindResult) {
return true;
}
return false;
}

/***
* REQUEST_PURCHASEリクエストを送信する
*/
class RequestPurchase extends BillingRequest {
public final String mProductId;

public RequestPurchase(String itemId) {
logout.out(this.getClass(),"RequestPurchase()","REQUEST_PURCHASEリクエストを送信する:"+itemId);
mProductId = itemId;
}

@Override
protected long run() throws RemoteException {
logout.out(this.getClass(),"run()","run()");
// 基本Bundleを作成する
Bundle request = makeRequestBundle("REQUEST_PURCHASE");

// BundleにプロダクトIDを追加する
request.putString(Consts.BILLING_REQUEST_ITEM_ID, mProductId);
logout.out(this.getClass(),"run()","mProductId:"+mProductId);

// MarketServiceに送信する
Bundle response = mService.sendBillingRequest(request);

// レスポンスに含まれているPendingIntentを取得する
PendingIntent pendingIntent = response.getParcelable(Consts.BILLING_RESPONSE_PURCHASE_INTENT);
logout.out(this.getClass(),"run()","pendingIntent:"+(pendingIntent == null ? "null!!!":""));
if (pendingIntent == null) {
return Consts.BILLING_RESPONSE_INVALID_REQUEST_ID;
}

// 取得したPendingIntentを使ってチェックアウト画面を起動する
Intent intent = new Intent();
startBuyPageActivity(pendingIntent, intent);

return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
}

/***
* GET_PURCHASE_INFORMATIONリクエストを送信する
*/
class GetPurchaseInformation extends BillingRequest {
long mNonce;
final String[] mNotifyIds;

public GetPurchaseInformation(String[] notifyIds) {
mNotifyIds = notifyIds;
}

@Override
protected long run() throws RemoteException {
logout.out(this.getClass(),"run()","ノンスを生成する");
// ノンスを生成する
mNonce = Security.generateNonce();

logout.out(this.getClass(),"run()","mNonce:"+mNonce);
logout.out(this.getClass(),"run()","mNotifyIds:"+mNotifyIds);
// GET_PURCHASE_INFORMATIONリクエストにノンスと通知IDを設定して送信する
Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION");
request.putLong(Consts.BILLING_REQUEST_NONCE, mNonce);
request.putStringArray(Consts.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
Bundle response = mService.sendBillingRequest(request);

return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
}

@Override
protected void onRemoteException(RemoteException e) {
super.onRemoteException(e);

// 例外が発生したらノンスを削除
Security.removeNonce(mNonce);
}
}

/***
* CONFIRM_NOTIFICATIONSリクエストを送信する
*/
class ConfirmNotifications extends BillingRequest {
final String[] mNotifyIds;

public ConfirmNotifications(String[] notifyIds) {
mNotifyIds = notifyIds;
}

@Override
protected long run() throws RemoteException {
logout.out(this.getClass(),"run()","mNotifyIds:"+mNotifyIds);
Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS");
request.putStringArray(Consts.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
Bundle response = mService.sendBillingRequest(request);

return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
}
}

// GET_PURCHASE_INFORMATIONリクエストを処理する
public boolean requestPurchase(String productId) {
return new RequestPurchase(productId).runRequest();
}

// PURCHASE_STATE_CHANGEDリクエストを処理する
private boolean getPurchaseInformation(String[] notifyIds) {
return new GetPurchaseInformation(notifyIds).runRequest();
}

// CONFIRM_NOTIFICATIONリクエストを処理する
private boolean confirmNotifications(String[] notifyIds) {
return new ConfirmNotifications(notifyIds).runRequest();
}

@Override
public void onStart(Intent intent, int startId) {
String action = intent.getAction();
logout.out(this.getClass(),"onStart()","action:"+action);
// GET_PURCHASE_INFORMATIONリクエストを処理する
if (Consts.ACTION_GET_PURCHASE_INFORMATION.equals(action)) {
String notifyId = intent.getStringExtra(Consts.NOTIFICATION_ID);
getPurchaseInformation(new String[] { notifyId });

// PURCHASE_STATE_CHANGEDリクエストを処理する
} else if (Consts.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
String signedData = intent.getStringExtra(Consts.INAPP_SIGNED_DATA);
String signature = intent.getStringExtra(Consts.INAPP_SIGNATURE);

purchaseStateChanged(signedData, signature);

// CONFIRM_NOTIFICATIONリクエストを処理する
} else if (Consts.ACTION_CONFIRM_NOTIFICATION.equals(action)) {
String[] notifyIds = intent.getStringArrayExtra(Consts.NOTIFICATION_ID);
confirmNotifications(notifyIds);

// レスポンスを処理する
} else if (Consts.ACTION_RESPONSE_CODE.equals(action)) {
// RequestIDとResponseCodeをログに表示する
long requestId = intent.getLongExtra(Consts.INAPP_REQUEST_ID, -1);
int responseCodeIndex = intent.getIntExtra(Consts.INAPP_RESPONSE_CODE, ResponseCode.RESULT_ERROR.ordinal());
ResponseCode responseCode = ResponseCode.valueOf(responseCodeIndex);
logout.out(this.getClass(),"onStart()","requestId:"+requestId+" / "+"responseCode:"+responseCode);
}
}



private void purchaseStateChanged(String signedData, String signature) {

logout.out(this.getClass(),"purchaseStateChanged()","☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆");
ArrayList<Security.VerifiedPurchase> purchases;

// 受信したトランザクション情報の整合性を検証する
purchases = Security.verifyPurchase(signedData, signature);
if (purchases == null) {
return;
}

logout.out(this.getClass(),"purchaseStateChanged()","サイズ:"+purchases.size());
// トランザクション情報を処理する
ArrayList<String> notifyList = new ArrayList<String>();
for (VerifiedPurchase vp : purchases) {
if (vp.notificationId != null) {
// トランザクションごとに通知IDを保持する
notifyList.add(vp.notificationId);
}
logout.out(this.getClass(),"purchaseStateChanged()","vp.productId:"+vp.productId);

// トランザクションごとに対象のコンテンツをコピーする
for (CatalogEntry catalog : CatalogEntry.CATALOG) {
if (catalog.productId.equals(vp.productId)) {
//Utils.copyMusic(this, catalog.resId, catalog.fileName);
if(vp.productId.equals("coin100")){
                            なにかしらの処理を記述
}
if(vp.productId.equals("coin400")){
                            なにかしらの処理を記述
}
if(vp.productId.equals("coin1000")){
                            なにかしらの処理を記述
}
}
}
}

// トランザクション情報を取得できた場合は、CONFIRM_NOTIFICATIONSリクエストを送信する
if (!notifyList.isEmpty()) {
String[] notifyIds = notifyList.toArray(new String[notifyList.size()]);
confirmNotifications(notifyIds);
}

}

Handler.Callback msgCall = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Toast.makeText(context, msg.obj.toString(), Toast.LENGTH_LONG).show();
return false;
}
};

/***
* MarketBillingService接続時に処理できなかったリクエストを処理する
*/
private void runPendingRequests() {
if (mPendingRequests != null) {
if (mPendingRequests.runIfConnected()) {
mPendingRequests = null;
} else {
bindToMarketBillingService();
return;
}
}
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

public void onServiceConnected(ComponentName name, IBinder service) {
// IMarketBillingServiceインタフェースへの参照を作成する
mService = IMarketBillingService.Stub.asInterface(service);

// 接続されたタイミングでリクエストを処理する
runPendingRequests();
}

public void onServiceDisconnected(ComponentName name) {
mService = null;
}

public void unbind() {
unbindService(this);
}

/***
* チェックアウト画面を表示する
*/
void startBuyPageActivity(PendingIntent pendingIntent, Intent intent) {
try {
this.startIntentSender(pendingIntent.getIntentSender(), intent, 0, 0, 0);
} catch (Exception e) {
//
}
}
}

基本上記をコピペすればよい。

メインとなる、課金処理が完了された後に呼ばれるのが
「purchaseStateChanged」メソッドとなる。
記述内の「vp.productId」に購入したproductIdが格納されてくるため、CatalogEntry と比較を行うことで、どの課金アイテムが購入されたかがわかる。
あとは、そのアイテムごとにどのように処理をするのかを記述すればよい。

※APIバージョンについて
Consts.BILLING_REQUEST_API_VERSIONにて設定している「API_VERSION」について
2012年12月10日からバージョン3まで存在している。

ライセンスKeyがアプリ毎に設定する使用になってから、このAPI_VERSIONが2、もしくは3ではないと上手く動作しないようだ。

調べてみると、ライセンスKeyを新しいアプリ毎のKeyを設定し、API_VERSIONが2で、上手く動作しているらしい。


■IMarketBillingService.aidlの作成
パッケージ「com.android.vending.billing」を作成し、その配下に「IMarketBillingService.aidl」というファイルを作成する
内容は以下

/*
* 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);
}

ファイルを作成すると、自動でgenフォルダに
パッケージ:com.android.vending.billing
ファイル:IMarketBillingService.java
が作成される。
aidlファイルについての詳細は省略する


以上がアプリ内課金に必要なclassとなる。

■アプリ内課金の実行ロジックについて


では実際にアプリ内課金を実行する方法を記述する

//課金サービスの生成とチェック
BillingService mBillingService = new BillingService();
mBillingService.setContext(this);
//アプリ内課金処理開始
mBillingService.requestPurchase(CatalogEntry.CATALOG[0].productId)


アプリ内課金の実行は簡単。
作成した「BillingService」クラスを生成して、メソッド「requestPurchase」をCallする際に、購入したアイテム用のproductIdを渡してあげるだけ。

メソッド「requestPurchase」をCallすることで、GooglePlayStoreが起動し、決済画面が表示される。
決済が完了すると、「BillingService」クラスの、メソッド「purchaseStateChanged」がCallされるというわけだ。
このメソッドに渡されるproductIdが、Callする際に引数で渡したproductIdになっているというわけです。


以上が、アプリ内課金の実装とサービスをCallするためのコードとなる。

次にマニフェストファイルの設定について

■マニフェストファイル

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.android.vending.BILLING" />


<service android:name="billing.montarosu.montarosu.billings.BillingService" />
<receiver android:name="billing.montarosu.montarosu.billings.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>



■ライセンスKeyの生成

Security.javaで必要なライセンスKeyの生成について記述する。
デベロッパコンソールにアプリをアップする必要があるため、とりあえずデベロッパコンソールにアップする。
ステータスを「非公開」にすれば誰にも見られないため、とりあえずapkファイルを作成してみる。
apkファイルの作成は省略する。

デベロッパコンソールにアプリをアップし、詳細画面の左側「サービスと API」を開と、
右側にライセンスKeyが表示されるので、そのKeyをSecurityクラスの変数「base64EncodedPublicKey」に追記すればよい。

setumei21.jpg


■アプリ内課金用のアイテムをデベロッパコンソールへ登録する

さー残りもわずか。
肝心の購入用アイテムをデベロッパコンソール上に登録する。

ライセンスKeyの画面で、左側「アプリ内サービス」をクリックし、「新しいサービスを追加」をクリックする。
まず、サービスのタイプには複数ある
1.管理対象の商品
2.管理対象外の商品
3.定期購入

3の定期購入はまだやったことが無いからスルー。
1、2についてだが、
1の「管理対象」をいうのは、1回だけ購入できるというもの。
例でいうと、RPGにある「伝説の剣」など、そのアプリ内で1回だけ購入させたいもののこと。
2の「管理対象外」というのは1の逆で、何回でも購入できるもの
例でいうと、RPGの回復系アイテムみたいなものだ。

「サービス ID 」には、プログラムで設定している「productId」を設定する。
コンソール側に設定するサービスIDとプログラム側で引数で渡すproductIdが一致することで、PlayStoreで決済が可能になるのだ。

最後に、設定を完了する際に「公開」として保存すること。
「公開」にしなければサービスが有効にならないため。
これは、本アプリ側が「非公開」でも問題ない。

本アプリを「非公開」、サービスを「公開」に設定することで、まだアプリ自体を「公開」にしなくても、デバッグができるようになるのだ。

デベロッパコンソール側での設定は以上となる。

これで、アプリ内課金の実装については完成となる。

次はデバッグ方法について。
これはまた別に書く。
疲れた。
スポンサーサイト

テーマ : android
ジャンル : コンピュータ

コメントの投稿

非公開コメント

参考になります

アプリ内課金の情報が少なく困っていたので非常に助かります。

1点質問なのですが、メソッドとして使用している「logout.out()」は独自に作成されたものなのでしょうか?

できれば「logout.out()」の情報も掲載していただけると助かります。

Re: 参考になります

コメントありがとうございます。

すいません。logoutは独自メソッドです
コードが汚くてすいません。
public class logout {
public static String PROJECT_NAME = "";

public static void out(Object cl,String method,String message){
try{
Class c = (Class)cl;
android.util.Log.d(PROJECT_NAME,c.getName()+" \t"+method+" \t"+message);
}catch(Exception ex){
android.util.Log.d(PROJECT_NAME,method+" \t"+message);
}finally{
}
}
}
ただ、情報を付加してログを出力しているだけです。


ちなみに、最近出たVersion3では、アプリ内課金の搭載が非常に簡単になっているらしいです。
そっちの搭載が出来たら、またブログにアップしたいと思います。
おすすめアプリ
カテゴリ
最新記事
リンク
アクセスカウンター
アクセス解析
imobile
i-mobile
i-mobile
i-mobile
i-mobile
i-mobile
検索フォーム
RSSリンクの表示
リンク
ブロとも申請フォーム

この人とブロともになる

QRコード
QR
Amazon
Androidお勧め参考書
EC studio
商品
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。