+ *
+ * @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.
+ *
+ *
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 source
+ * and writes the resulting four Base64 bytes to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes.
+ *
+ * @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 destination 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 source
+ * and writes the resulting bytes (up to three of them)
+ * to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination 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;
+ }
+}
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Base64DecoderException.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Base64DecoderException.java
new file mode 100644
index 0000000000..1d1685e7d4
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Base64DecoderException.java
@@ -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.onesignal.example.iap;
+
+/**
+ * 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;
+}
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabException.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabException.java
new file mode 100644
index 0000000000..5fbea93f1d
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabException.java
@@ -0,0 +1,43 @@
+/* Copyright (c) 2012 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.onesignal.example.iap;
+
+/**
+ * Exception thrown when something went wrong with in-app billing.
+ * An IabException has an associated IabResult (an error).
+ * To get the IAB result that caused this exception to be thrown,
+ * call {@link #getResult()}.
+ */
+public class IabException extends Exception {
+ IabResult mResult;
+
+ public IabException(IabResult r) {
+ this(r, null);
+ }
+ public IabException(int response, String message) {
+ this(new IabResult(response, message));
+ }
+ public IabException(IabResult r, Exception cause) {
+ super(r.getMessage(), cause);
+ mResult = r;
+ }
+ public IabException(int response, String message, Exception cause) {
+ this(new IabResult(response, message), cause);
+ }
+
+ /** Returns the IAB result (error) that this exception signals. */
+ public IabResult getResult() { return mResult; }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabHelper.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabHelper.java
new file mode 100644
index 0000000000..3d36712727
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabHelper.java
@@ -0,0 +1,1002 @@
+/* Copyright (c) 2012 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.onesignal.example.iap;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vending.billing.IInAppBillingService;
+import com.onesignal.example.iap.IabException;
+import com.onesignal.example.iap.IabResult;
+import com.onesignal.example.iap.Inventory;
+import com.onesignal.example.iap.Purchase;
+import com.onesignal.example.iap.Security;
+import com.onesignal.example.iap.SkuDetails;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides convenience methods for in-app billing. You can create one instance of this
+ * class for your application and use it to process in-app billing operations.
+ * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
+ * many common in-app billing operations, as well as automatic signature
+ * verification.
+ *
+ * After instantiating, you must perform setup in order to start using the object.
+ * To perform setup, call the {@link #startSetup} method and provide a listener;
+ * that listener will be notified when setup is complete, after which (and not before)
+ * you may call other methods.
+ *
+ * After setup is complete, you will typically want to request an inventory of owned
+ * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
+ * and related methods.
+ *
+ * When you are done with this object, don't forget to call {@link #dispose}
+ * to ensure proper cleanup. This object holds a binding to the in-app billing
+ * service, which will leak unless you dispose of it correctly. If you created
+ * the object on an Activity's onCreate method, then the recommended
+ * place to dispose of it is the Activity's onDestroy method.
+ *
+ * A note about threading: When using this object from a background thread, you may
+ * call the blocking versions of methods; when using from a UI thread, call
+ * only the asynchronous versions and handle the results via callbacks.
+ * Also, notice that you can only call one asynchronous operation at a time;
+ * attempting to start a second asynchronous operation while the first one
+ * has not yet completed will result in an exception being thrown.
+ *
+ * @author Bruno Oliveira (Google)
+ *
+ */
+public class IabHelper {
+ // Is debug logging enabled?
+ boolean mDebugLog = false;
+ String mDebugTag = "IabHelper";
+
+ // Is setup done?
+ boolean mSetupDone = false;
+
+ // Has this object been disposed of? (If so, we should ignore callbacks, etc)
+ boolean mDisposed = false;
+
+ // Are subscriptions supported?
+ boolean mSubscriptionsSupported = false;
+
+ // Is an asynchronous operation in progress?
+ // (only one at a time can be in progress)
+ boolean mAsyncInProgress = false;
+
+ // (for logging/debugging)
+ // if mAsyncInProgress == true, what asynchronous operation is in progress?
+ String mAsyncOperation = "";
+
+ // Context we were passed during initialization
+ Context mContext;
+
+ // Connection to the service
+ IInAppBillingService mService;
+ ServiceConnection mServiceConn;
+
+ // The request code used to launch purchase flow
+ int mRequestCode;
+
+ // The item type of the current purchase flow
+ String mPurchasingItemType;
+
+ // Public key for verifying signature, in base64 encoding
+ String mSignatureBase64 = null;
+
+ // Billing response codes
+ public static final int BILLING_RESPONSE_RESULT_OK = 0;
+ public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
+ public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
+ public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
+ public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
+
+ // IAB Helper error codes
+ public static final int IABHELPER_ERROR_BASE = -1000;
+ public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
+ public static final int IABHELPER_BAD_RESPONSE = -1002;
+ public static final int IABHELPER_VERIFICATION_FAILED = -1003;
+ public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
+ public static final int IABHELPER_USER_CANCELLED = -1005;
+ public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
+ public static final int IABHELPER_MISSING_TOKEN = -1007;
+ public static final int IABHELPER_UNKNOWN_ERROR = -1008;
+ public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
+ public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
+
+ // Keys for the responses from InAppBillingService
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
+ public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
+ public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
+ public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
+ public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
+ public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+
+ // Item types
+ public static final String ITEM_TYPE_INAPP = "inapp";
+ public static final String ITEM_TYPE_SUBS = "subs";
+
+ // some fields on the getSkuDetails response bundle
+ public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
+ public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
+
+ /**
+ * Creates an instance. After creation, it will not yet be ready to use. You must perform
+ * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
+ * block and is safe to call from a UI thread.
+ *
+ * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
+ * @param base64PublicKey Your application's public key, encoded in base64.
+ * This is used for verification of purchase signatures. You can find your app's base64-encoded
+ * public key in your application's page on Google Play Developer Console. Note that this
+ * is NOT your "developer public key".
+ */
+ public IabHelper(Context ctx, String base64PublicKey) {
+ mContext = ctx.getApplicationContext();
+ mSignatureBase64 = base64PublicKey;
+ logDebug("IAB helper created.");
+ }
+
+ /**
+ * Enables or disable debug logging through LogCat.
+ */
+ public void enableDebugLogging(boolean enable, String tag) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ mDebugTag = tag;
+ }
+
+ public void enableDebugLogging(boolean enable) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ }
+
+ /**
+ * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
+ * when the setup process is complete.
+ */
+ public interface OnIabSetupFinishedListener {
+ /**
+ * Called to notify that setup is complete.
+ *
+ * @param result The result of the setup process.
+ */
+ public void onIabSetupFinished(IabResult result);
+ }
+
+ /**
+ * Starts the setup process. This will start up the setup process asynchronously.
+ * You will be notified through the listener when the setup process is complete.
+ * This method is safe to call from a UI thread.
+ *
+ * @param listener The listener to notify when the setup process is complete.
+ */
+ public void startSetup(final OnIabSetupFinishedListener listener) {
+ // If already set up, can't do it again.
+ checkNotDisposed();
+ if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
+
+ // Connection to IAB service
+ logDebug("Starting in-app billing setup.");
+ mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ logDebug("Billing service disconnected.");
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (mDisposed) return;
+ logDebug("Billing service connected.");
+ mService = IInAppBillingService.Stub.asInterface(service);
+ String packageName = mContext.getPackageName();
+ try {
+ logDebug("Checking for in-app billing 3 support.");
+
+ // check for in-app billing v3 support
+ int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ if (listener != null) listener.onIabSetupFinished(new IabResult(response,
+ "Error checking for billing v3 support."));
+
+ // if in-app purchases aren't supported, neither are subscriptions.
+ mSubscriptionsSupported = false;
+ return;
+ }
+ logDebug("In-app billing version 3 supported for " + packageName);
+
+ // check for v3 subscriptions support
+ response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Subscriptions AVAILABLE.");
+ mSubscriptionsSupported = true;
+ }
+ else {
+ logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
+ }
+
+ mSetupDone = true;
+ }
+ catch (RemoteException e) {
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
+ "RemoteException while setting up in-app billing."));
+ }
+ e.printStackTrace();
+ return;
+ }
+
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
+ }
+ }
+ };
+
+ Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
+ serviceIntent.setPackage("com.android.vending");
+ if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
+ // service available to handle that Intent
+ mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ }
+ else {
+ // no service available to handle that Intent
+ if (listener != null) {
+ listener.onIabSetupFinished(
+ new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
+ "Billing service unavailable on device."));
+ }
+ }
+ }
+
+ /**
+ * Dispose of object, releasing resources. It's very important to call this
+ * method when you are done with this object. It will release any resources
+ * used by it such as service connections. Naturally, once the object is
+ * disposed of, it can't be used again.
+ */
+ public void dispose() {
+ logDebug("Disposing.");
+ mSetupDone = false;
+ Log.e("IAPHelper", "$$$$$$$$ dispose called");
+ if (mServiceConn != null) {
+ Log.e("IAPHelper", "$$$$$$$$ dispose called2");
+ logDebug("Unbinding from service.");
+ if (mContext != null) {
+ Log.e("IAPHelper", "$$$$$$$$ dispose called3");
+ mContext.unbindService(mServiceConn);
+ }
+ }
+ mDisposed = true;
+ mContext = null;
+ mServiceConn = null;
+ mService = null;
+ mPurchaseListener = null;
+ }
+
+ private void checkNotDisposed() {
+ if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
+ }
+
+ /** Returns whether subscriptions are supported. */
+ public boolean subscriptionsSupported() {
+ checkNotDisposed();
+ return mSubscriptionsSupported;
+ }
+
+
+ /**
+ * Callback that notifies when a purchase is finished.
+ */
+ public interface OnIabPurchaseFinishedListener {
+ /**
+ * Called to notify that an in-app purchase finished. If the purchase was successful,
+ * then the sku parameter specifies which item was purchased. If the purchase failed,
+ * the sku and extraData parameters may or may not be null, depending on how far the purchase
+ * process went.
+ *
+ * @param result The result of the purchase.
+ * @param info The purchase information (null if purchase failed)
+ */
+ public void onIabPurchaseFinished(IabResult result, Purchase info);
+ }
+
+ // The listener registered on launchPurchaseFlow, which we have to call back when
+ // the purchase finishes
+ OnIabPurchaseFinishedListener mPurchaseListener;
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
+ launchPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener) {
+ launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
+ }
+
+ /**
+ * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
+ * which will involve bringing up the Google Play screen. The calling activity will be paused while
+ * the user interacts with Google Play, and the result will be delivered via the activity's
+ * {@link android.app.Activity#onActivityResult} method, at which point you must call
+ * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param act The calling activity.
+ * @param sku The sku of the item to purchase.
+ * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
+ * @param requestCode A request code (to differentiate from other responses --
+ * as in {@link android.app.Activity#startActivityForResult}).
+ * @param listener The listener to notify when the purchase process finishes
+ * @param extraData Extra data (developer payload), which will be returned with the purchase data
+ * when the purchase completes. This extra data will be permanently bound to that purchase
+ * and will always be returned when the purchase is queried.
+ */
+ public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ checkNotDisposed();
+ checkSetupDone("launchPurchaseFlow");
+ flagStartAsync("launchPurchaseFlow");
+ IabResult result;
+
+ if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
+ IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
+ "Subscriptions are not available.");
+ flagEndAsync();
+ if (listener != null) listener.onIabPurchaseFinished(r, null);
+ return;
+ }
+
+ try {
+ logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
+ Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
+ int response = getResponseCodeFromBundle(buyIntentBundle);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logError("Unable to buy item, Error response: " + getResponseDesc(response));
+ flagEndAsync();
+ result = new IabResult(response, "Unable to buy item");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ return;
+ }
+
+ PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
+ logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
+ mRequestCode = requestCode;
+ mPurchaseListener = listener;
+ mPurchasingItemType = itemType;
+ act.startIntentSenderForResult(pendingIntent.getIntentSender(),
+ requestCode, new Intent(),
+ Integer.valueOf(0), Integer.valueOf(0),
+ Integer.valueOf(0));
+ }
+ catch (SendIntentException e) {
+ logError("SendIntentException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ catch (RemoteException e) {
+ logError("RemoteException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ }
+
+ /**
+ * Handles an activity result that's part of the purchase flow in in-app billing. If you
+ * are calling {@link #launchPurchaseFlow}, then you must call this method from your
+ * Activity's {@link android.app.Activity@onActivityResult} method. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param requestCode The requestCode as you received it.
+ * @param resultCode The resultCode as you received it.
+ * @param data The data (Intent) as you received it.
+ * @return Returns true if the result was related to a purchase flow and was handled;
+ * false if the result was not related to a purchase, in which case you should
+ * handle it normally.
+ */
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ IabResult result;
+ if (requestCode != mRequestCode) return false;
+
+ checkNotDisposed();
+ checkSetupDone("handleActivityResult");
+
+ // end of async purchase operation that started on launchPurchaseFlow
+ flagEndAsync();
+
+ if (data == null) {
+ logError("Null data in IAB activity result.");
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ int responseCode = getResponseCodeFromIntent(data);
+ String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
+ String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
+
+ if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successful resultcode from purchase activity.");
+ logDebug("Purchase data: " + purchaseData);
+ logDebug("Data signature: " + dataSignature);
+ logDebug("Extras: " + data.getExtras());
+ logDebug("Expected item type: " + mPurchasingItemType);
+
+ if (purchaseData == null || dataSignature == null) {
+ logError("BUG: either purchaseData or dataSignature is null.");
+ logDebug("Extras: " + data.getExtras().toString());
+ result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ Purchase purchase = null;
+ try {
+ purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
+ String sku = purchase.getSku();
+
+ // Verify signature
+ if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
+ logError("Purchase signature verification FAILED for sku " + sku);
+ result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
+ return true;
+ }
+ logDebug("Purchase signature successfully verified.");
+ }
+ catch (JSONException e) {
+ logError("Failed to parse purchase data.");
+ e.printStackTrace();
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ if (mPurchaseListener != null) {
+ mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
+ }
+ }
+ else if (resultCode == Activity.RESULT_OK) {
+ // result code was OK, but in-app billing response was not OK.
+ logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
+ if (mPurchaseListener != null) {
+ result = new IabResult(responseCode, "Problem purchashing item.");
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ }
+ else if (resultCode == Activity.RESULT_CANCELED) {
+ logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ else {
+ logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ + ". Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ return true;
+ }
+
+ public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException {
+ return queryInventory(querySkuDetails, moreSkus, null);
+ }
+
+ /**
+ * Queries the inventory. This will query all owned items from the server, as well as
+ * information on additional skus, if specified. This method may block or take long to execute.
+ * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
+ *
+ * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
+ * as purchase information.
+ * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @throws IabException if a problem occurs while refreshing the inventory.
+ */
+ public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
+ List moreSubsSkus) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ try {
+ Inventory inv = new Inventory();
+ int r = queryPurchases(inv, ITEM_TYPE_INAPP);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned items).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of items).");
+ }
+ }
+
+ // if subscriptions are supported, then also query for subscriptions
+ if (mSubscriptionsSupported) {
+ r = queryPurchases(inv, ITEM_TYPE_SUBS);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
+ }
+ }
+ }
+
+ return inv;
+ }
+ catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
+ }
+ catch (JSONException e) {
+ throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
+ }
+ }
+
+ /**
+ * Listener that notifies when an inventory query operation completes.
+ */
+ public interface QueryInventoryFinishedListener {
+ /**
+ * Called to notify that an inventory query operation completed.
+ *
+ * @param result The result of the operation.
+ * @param inv The inventory.
+ */
+ public void onQueryInventoryFinished(IabResult result, Inventory inv);
+ }
+
+
+ /**
+ * Asynchronous wrapper for inventory query. This will perform an inventory
+ * query as described in {@link #queryInventory}, but will do so asynchronously
+ * and call back the specified listener upon completion. This method is safe to
+ * call from a UI thread.
+ *
+ * @param querySkuDetails as in {@link #queryInventory}
+ * @param moreSkus as in {@link #queryInventory}
+ * @param listener The listener to notify when the refresh operation completes.
+ */
+ public void queryInventoryAsync(final boolean querySkuDetails,
+ final List moreSkus,
+ final QueryInventoryFinishedListener listener) {
+ final Handler handler = new Handler();
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ flagStartAsync("refresh inventory");
+ (new Thread(new Runnable() {
+ public void run() {
+ IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
+ Inventory inv = null;
+ try {
+ inv = queryInventory(querySkuDetails, moreSkus);
+ }
+ catch (IabException ex) {
+ result = ex.getResult();
+ }
+
+ flagEndAsync();
+
+ final IabResult result_f = result;
+ final Inventory inv_f = inv;
+ if (!mDisposed && listener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ listener.onQueryInventoryFinished(result_f, inv_f);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(true, null, listener);
+ }
+
+ public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(querySkuDetails, null, listener);
+ }
+
+
+ /**
+ * Consumes a given in-app product. Consuming can only be done on an item
+ * that's owned, and as a result of consumption, the user will no longer own it.
+ * This method may block or take long to return. Do not call from the UI thread.
+ * For that, see {@link #consumeAsync}.
+ *
+ * @param itemInfo The PurchaseInfo that represents the item to consume.
+ * @throws IabException if there is a problem during consumption.
+ */
+ void consume(Purchase itemInfo) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+
+ if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
+ throw new IabException(IABHELPER_INVALID_CONSUMPTION,
+ "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
+ }
+
+ try {
+ String token = itemInfo.getToken();
+ String sku = itemInfo.getSku();
+ if (token == null || token.equals("")) {
+ logError("Can't consume "+ sku + ". No token.");
+ throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ + sku + " " + itemInfo);
+ }
+
+ logDebug("Consuming sku: " + sku + ", token: " + token);
+ int response = mService.consumePurchase(3, mContext.getPackageName(), token);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successfully consumed sku: " + sku);
+ }
+ else {
+ logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
+ throw new IabException(response, "Error consuming sku " + sku);
+ }
+ }
+ catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
+ }
+ }
+
+ /**
+ * Callback that notifies when a consumption operation finishes.
+ */
+ public interface OnConsumeFinishedListener {
+ /**
+ * Called to notify that a consumption has finished.
+ *
+ * @param purchase The purchase that was (or was to be) consumed.
+ * @param result The result of the consumption operation.
+ */
+ public void onConsumeFinished(Purchase purchase, IabResult result);
+ }
+
+ /**
+ * Callback that notifies when a multi-item consumption operation finishes.
+ */
+ public interface OnConsumeMultiFinishedListener {
+ /**
+ * Called to notify that a consumption of multiple items has finished.
+ *
+ * @param purchases The purchases that were (or were to be) consumed.
+ * @param results The results of each consumption operation, corresponding to each
+ * sku.
+ */
+ public void onConsumeMultiFinished(List purchases, List results);
+ }
+
+ /**
+ * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
+ * performs the consumption in the background and notifies completion through
+ * the provided listener. This method is safe to call from a UI thread.
+ *
+ * @param purchase The purchase to be consumed.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ List purchases = new ArrayList();
+ purchases.add(purchase);
+ consumeAsyncInternal(purchases, listener, null);
+ }
+
+ /**
+ * Same as {@link consumeAsync}, but for multiple items at once.
+ * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ consumeAsyncInternal(purchases, null, listener);
+ }
+
+ /**
+ * Returns a human-readable description for the given response code.
+ *
+ * @param code The response code
+ * @return A human-readable string explaining the result code.
+ * It also includes the result code numerically.
+ */
+ public static String getResponseDesc(int code) {
+ String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
+ "3:Billing Unavailable/4:Item unavailable/" +
+ "5:Developer Error/6:Error/7:Item Already Owned/" +
+ "8:Item not owned").split("/");
+ String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
+ "-1002:Bad response received/" +
+ "-1003:Purchase signature verification failed/" +
+ "-1004:Send intent failed/" +
+ "-1005:User cancelled/" +
+ "-1006:Unknown purchase response/" +
+ "-1007:Missing token/" +
+ "-1008:Unknown error/" +
+ "-1009:Subscriptions not available/" +
+ "-1010:Invalid consumption attempt").split("/");
+
+ if (code <= IABHELPER_ERROR_BASE) {
+ int index = IABHELPER_ERROR_BASE - code;
+ if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
+ else return String.valueOf(code) + ":Unknown IAB Helper Error";
+ }
+ else if (code < 0 || code >= iab_msgs.length)
+ return String.valueOf(code) + ":Unknown";
+ else
+ return iab_msgs[code];
+ }
+
+
+ // Checks that setup was done; if not, throws an exception.
+ void checkSetupDone(String operation) {
+ if (!mSetupDone) {
+ logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
+ throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromBundle(Bundle b) {
+ Object o = b.get(RESPONSE_CODE);
+ if (o == null) {
+ logDebug("Bundle with null response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+ else if (o instanceof Integer) return ((Integer)o).intValue();
+ else if (o instanceof Long) return (int)((Long)o).longValue();
+ else {
+ logError("Unexpected type for bundle response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromIntent(Intent i) {
+ Object o = i.getExtras().get(RESPONSE_CODE);
+ if (o == null) {
+ logError("Intent with no response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+ else if (o instanceof Integer) return ((Integer)o).intValue();
+ else if (o instanceof Long) return (int)((Long)o).longValue();
+ else {
+ logError("Unexpected type for intent response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
+ }
+ }
+
+ void flagStartAsync(String operation) {
+ if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
+ operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
+ mAsyncOperation = operation;
+ mAsyncInProgress = true;
+ logDebug("Starting async operation: " + operation);
+ }
+
+ void flagEndAsync() {
+ logDebug("Ending async operation: " + mAsyncOperation);
+ mAsyncOperation = "";
+ mAsyncInProgress = false;
+ }
+
+
+ int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
+ // Query purchases
+ logDebug("Querying owned items, item type: " + itemType);
+ logDebug("Package name: " + mContext.getPackageName());
+ boolean verificationFailed = false;
+ String continueToken = null;
+
+ do {
+ logDebug("Calling getPurchases with continuation token: " + continueToken);
+ Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
+ itemType, continueToken);
+
+ int response = getResponseCodeFromBundle(ownedItems);
+ logDebug("Owned items response: " + String.valueOf(response));
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getPurchases() failed: " + getResponseDesc(response));
+ return response;
+ }
+ if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
+ logError("Bundle returned from getPurchases() doesn't contain required fields.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+
+ ArrayList ownedSkus = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_ITEM_LIST);
+ ArrayList purchaseDataList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_PURCHASE_DATA_LIST);
+ ArrayList signatureList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_SIGNATURE_LIST);
+
+ for (int i = 0; i < purchaseDataList.size(); ++i) {
+ String purchaseData = purchaseDataList.get(i);
+ String signature = signatureList.get(i);
+ String sku = ownedSkus.get(i);
+ if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
+ logDebug("Sku is owned: " + sku);
+ Purchase purchase = new Purchase(itemType, purchaseData, signature);
+
+ if (TextUtils.isEmpty(purchase.getToken())) {
+ logWarn("BUG: empty/null token!");
+ logDebug("Purchase data: " + purchaseData);
+ }
+
+ // Record ownership and token
+ inv.addPurchase(purchase);
+ }
+ else {
+ logWarn("Purchase signature verification **FAILED**. Not adding item.");
+ logDebug(" Purchase data: " + purchaseData);
+ logDebug(" Signature: " + signature);
+ verificationFailed = true;
+ }
+ }
+
+ continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
+ logDebug("Continuation token: " + continueToken);
+ } while (!TextUtils.isEmpty(continueToken));
+
+ return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
+ }
+
+ int querySkuDetails(String itemType, Inventory inv, List moreSkus)
+ throws RemoteException, JSONException {
+ logDebug("Querying SKU details.");
+ ArrayList skuList = new ArrayList();
+ skuList.addAll(inv.getAllOwnedSkus(itemType));
+ if (moreSkus != null) {
+ for (String sku : moreSkus) {
+ if (!skuList.contains(sku)) {
+ skuList.add(sku);
+ }
+ }
+ }
+
+ if (skuList.size() == 0) {
+ logDebug("queryPrices: nothing to do because there are no SKUs.");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
+ Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
+ itemType, querySkus);
+
+ if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
+ int response = getResponseCodeFromBundle(skuDetails);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getSkuDetails() failed: " + getResponseDesc(response));
+ return response;
+ }
+ else {
+ logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+ }
+
+ ArrayList responseList = skuDetails.getStringArrayList(
+ RESPONSE_GET_SKU_DETAILS_LIST);
+
+ for (String thisResponse : responseList) {
+ SkuDetails d = new SkuDetails(itemType, thisResponse);
+ logDebug("Got sku details: " + d);
+ inv.addSkuDetails(d);
+ }
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+
+ void consumeAsyncInternal(final List purchases,
+ final OnConsumeFinishedListener singleListener,
+ final OnConsumeMultiFinishedListener multiListener) {
+ final Handler handler = new Handler();
+ flagStartAsync("consume");
+ (new Thread(new Runnable() {
+ public void run() {
+ final List results = new ArrayList();
+ for (Purchase purchase : purchases) {
+ try {
+ consume(purchase);
+ results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
+ }
+ catch (IabException ex) {
+ results.add(ex.getResult());
+ }
+ }
+
+ flagEndAsync();
+ if (!mDisposed && singleListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ singleListener.onConsumeFinished(purchases.get(0), results.get(0));
+ }
+ });
+ }
+ if (!mDisposed && multiListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ multiListener.onConsumeMultiFinished(purchases, results);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ void logDebug(String msg) {
+ if (mDebugLog) Log.d(mDebugTag, msg);
+ }
+
+ void logError(String msg) {
+ Log.e(mDebugTag, "In-app billing error: " + msg);
+ }
+
+ void logWarn(String msg) {
+ Log.w(mDebugTag, "In-app billing warning: " + msg);
+ }
+}
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabResult.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabResult.java
new file mode 100644
index 0000000000..a16f584188
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/IabResult.java
@@ -0,0 +1,47 @@
+/* Copyright (c) 2012 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.onesignal.example.iap;
+
+import com.onesignal.example.iap.IabHelper;
+
+/**
+ * Represents the result of an in-app billing operation.
+ * A result is composed of a response code (an integer) and possibly a
+ * message (String). You can get those by calling
+ * {@link #getResponse} and {@link #getMessage()}, respectively. You
+ * can also inquire whether a result is a success or a failure by
+ * calling {@link #isSuccess()} and {@link #isFailure()}.
+ */
+public class IabResult {
+ int mResponse;
+ String mMessage;
+
+ public IabResult(int response, String message) {
+ mResponse = response;
+ if (message == null || message.trim().length() == 0) {
+ mMessage = IabHelper.getResponseDesc(response);
+ }
+ else {
+ mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
+ }
+ }
+ public int getResponse() { return mResponse; }
+ public String getMessage() { return mMessage; }
+ public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
+ public boolean isFailure() { return !isSuccess(); }
+ public String toString() { return "IabResult: " + getMessage(); }
+}
+
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Inventory.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Inventory.java
new file mode 100644
index 0000000000..bd66c3f0b6
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Inventory.java
@@ -0,0 +1,91 @@
+/* Copyright (c) 2012 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.onesignal.example.iap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a block of information about in-app items.
+ * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
+ */
+public class Inventory {
+ Map mSkuMap = new HashMap();
+ Map mPurchaseMap = new HashMap();
+
+ public Inventory() { }
+
+ /** Returns the listing details for an in-app product. */
+ public SkuDetails getSkuDetails(String sku) {
+ return mSkuMap.get(sku);
+ }
+
+ /** Returns purchase information for a given product, or null if there is no purchase. */
+ public Purchase getPurchase(String sku) {
+ return mPurchaseMap.get(sku);
+ }
+
+ /** Returns whether or not there exists a purchase of the given product. */
+ public boolean hasPurchase(String sku) {
+ return mPurchaseMap.containsKey(sku);
+ }
+
+ /** Return whether or not details about the given product are available. */
+ public boolean hasDetails(String sku) {
+ return mSkuMap.containsKey(sku);
+ }
+
+ /**
+ * Erase a purchase (locally) from the inventory, given its product ID. This just
+ * modifies the Inventory object locally and has no effect on the server! This is
+ * useful when you have an existing Inventory object which you know to be up to date,
+ * and you have just consumed an item successfully, which means that erasing its
+ * purchase data from the Inventory you already have is quicker than querying for
+ * a new Inventory.
+ */
+ public void erasePurchase(String sku) {
+ if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
+ }
+
+ /** Returns a list of all owned product IDs. */
+ List getAllOwnedSkus() {
+ return new ArrayList(mPurchaseMap.keySet());
+ }
+
+ /** Returns a list of all owned product IDs of a given type */
+ public List getAllOwnedSkus(String itemType) {
+ List result = new ArrayList();
+ for (Purchase p : mPurchaseMap.values()) {
+ if (p.getItemType().equals(itemType)) result.add(p.getSku());
+ }
+ return result;
+ }
+
+ /** Returns a list of all purchases. */
+ List getAllPurchases() {
+ return new ArrayList(mPurchaseMap.values());
+ }
+
+ public void addSkuDetails(SkuDetails d) {
+ mSkuMap.put(d.getSku(), d);
+ }
+
+ public void addPurchase(Purchase p) {
+ mPurchaseMap.put(p.getSku(), p);
+ }
+}
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Purchase.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Purchase.java
new file mode 100644
index 0000000000..996fe0e758
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Purchase.java
@@ -0,0 +1,63 @@
+/* Copyright (c) 2012 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.onesignal.example.iap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app billing purchase.
+ */
+public class Purchase {
+ public String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
+ String mOrderId;
+ String mPackageName;
+ String mSku;
+ long mPurchaseTime;
+ int mPurchaseState;
+ String mDeveloperPayload;
+ String mToken;
+ String mOriginalJson;
+ String mSignature;
+
+ public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
+ mItemType = itemType;
+ mOriginalJson = jsonPurchaseInfo;
+ JSONObject o = new JSONObject(mOriginalJson);
+ mOrderId = o.optString("orderId");
+ mPackageName = o.optString("packageName");
+ mSku = o.optString("productId");
+ mPurchaseTime = o.optLong("purchaseTime");
+ mPurchaseState = o.optInt("purchaseState");
+ mDeveloperPayload = o.optString("developerPayload");
+ mToken = o.optString("token", o.optString("purchaseToken"));
+ mSignature = signature;
+ }
+
+ public String getItemType() { return mItemType; }
+ public String getOrderId() { return mOrderId; }
+ public String getPackageName() { return mPackageName; }
+ public String getSku() { return mSku; }
+ public long getPurchaseTime() { return mPurchaseTime; }
+ public int getPurchaseState() { return mPurchaseState; }
+ public String getDeveloperPayload() { return mDeveloperPayload; }
+ public String getToken() { return mToken; }
+ public String getOriginalJson() { return mOriginalJson; }
+ public String getSignature() { return mSignature; }
+
+ @Override
+ public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
+}
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Security.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Security.java
new file mode 100644
index 0000000000..6f057a011f
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/Security.java
@@ -0,0 +1,123 @@
+/* Copyright (c) 2012 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.onesignal.example.iap;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * 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.
+ */
+public class Security {
+ private static final String TAG = "IABUtil/Security";
+
+ private static final String KEY_FACTORY_ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+ /**
+ * Verifies that the data was signed with the given signature, and returns
+ * the verified purchase. The data is in JSON format and signed
+ * with a private key. The data also contains the {@link PurchaseState}
+ * and product ID of the purchase.
+ * @param base64PublicKey the base64-encoded public key to use for verifying.
+ * @param signedData the signed JSON string (signed, not encrypted)
+ * @param signature the signature for the data, signed with the private key
+ */
+ public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
+ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
+ TextUtils.isEmpty(signature)) {
+ Log.e(TAG, "Purchase verification failed: missing data.");
+ return false;
+ }
+
+ PublicKey key = Security.generatePublicKey(base64PublicKey);
+ return Security.verify(key, signedData, signature);
+ }
+
+ /**
+ * 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) {
+ 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;
+ }
+}
diff --git a/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/SkuDetails.java b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/SkuDetails.java
new file mode 100644
index 0000000000..d363f6982c
--- /dev/null
+++ b/OneSignalSDK/app/src/main/java/com/onesignal/example/iap/SkuDetails.java
@@ -0,0 +1,60 @@
+/* Copyright (c) 2012 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.onesignal.example.iap;
+
+import com.onesignal.example.iap.IabHelper;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app product's listing details.
+ */
+public class SkuDetails {
+ String mItemType;
+ String mSku;
+ String mType;
+ String mPrice;
+ String mTitle;
+ String mDescription;
+ String mJson;
+
+ public SkuDetails(String jsonSkuDetails) throws JSONException {
+ this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
+ }
+
+ public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
+ mItemType = itemType;
+ mJson = jsonSkuDetails;
+ JSONObject o = new JSONObject(mJson);
+ mSku = o.optString("productId");
+ mType = o.optString("type");
+ mPrice = o.optString("price");
+ mTitle = o.optString("title");
+ mDescription = o.optString("description");
+ }
+
+ public String getSku() { return mSku; }
+ public String getType() { return mType; }
+ public String getPrice() { return mPrice; }
+ public String getTitle() { return mTitle; }
+ public String getDescription() { return mDescription; }
+
+ @Override
+ public String toString() {
+ return "SkuDetails:" + mJson;
+ }
+}
diff --git a/OneSignalSDK/app/src/test/java/android/net/http/AndroidHttpClient.java b/OneSignalSDK/app/src/test/java/android/net/http/AndroidHttpClient.java
new file mode 100644
index 0000000000..83d358cf35
--- /dev/null
+++ b/OneSignalSDK/app/src/test/java/android/net/http/AndroidHttpClient.java
@@ -0,0 +1,6 @@
+package android.net.http;
+
+/**
+ * FAKE class, robolectric 3.0 workaround.
+ */
+public class AndroidHttpClient {}
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java b/OneSignalSDK/app/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java
new file mode 100644
index 0000000000..0566678739
--- /dev/null
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java
@@ -0,0 +1,40 @@
+package com.onesignal;
+
+import android.os.Looper;
+
+import org.robolectric.util.Scheduler;
+
+import java.util.Map;
+
+import static org.robolectric.Shadows.shadowOf;
+
+public class OneSignalPackagePrivateHelper {
+ public static void runAllNetworkRunnable() {
+ for (Map.Entry handlerThread : OneSignalStateSynchronizer.networkHandlerThreads.entrySet()) {
+ Scheduler scheduler = shadowOf(handlerThread.getValue().getLooper()).getScheduler();
+ while (scheduler.advanceToNextPostedRunnable()) {}
+ }
+ }
+
+ public static void runFocusRunnables() {
+ Looper looper = ActivityLifecycleHandler.focusHandlerThread.getHandlerLooper();
+ if (looper == null) return;
+
+ Scheduler scheduler = shadowOf(looper).getScheduler();
+ while (scheduler.advanceToNextPostedRunnable()) {}
+ }
+
+ public static void resetRunnables() {
+ for (Map.Entry handlerThread : OneSignalStateSynchronizer.networkHandlerThreads.entrySet())
+ handlerThread.getValue().stopScheduledRunnable();
+
+ Looper looper = ActivityLifecycleHandler.focusHandlerThread.getHandlerLooper();
+ if (looper == null) return;
+
+ shadowOf(looper).reset();
+ }
+
+ public static void SyncService_onTaskRemoved() {
+ SyncService.onTaskRemoved();
+ }
+}
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGoogleCloudMessaging.java b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGoogleCloudMessaging.java
index 8bb753005e..88cb1829dc 100644
--- a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGoogleCloudMessaging.java
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGoogleCloudMessaging.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGooglePlayServicesUtil.java b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGooglePlayServicesUtil.java
index 97eca58ba2..1ceacd4024 100644
--- a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGooglePlayServicesUtil.java
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowGooglePlayServicesUtil.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowOSUtils.java b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowOSUtils.java
new file mode 100644
index 0000000000..b91a000b1e
--- /dev/null
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowOSUtils.java
@@ -0,0 +1,12 @@
+package com.onesignal;
+
+import org.robolectric.annotation.Implements;
+
+@Implements(OSUtils.class)
+public class ShadowOSUtils {
+ public static int deviceType = 1;
+
+ public int getDeviceType() {
+ return deviceType;
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowOneSignalRestClient.java b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowOneSignalRestClient.java
index 1900583519..31a9c9aa86 100644
--- a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowOneSignalRestClient.java
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowOneSignalRestClient.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
@@ -30,74 +27,99 @@
package com.onesignal;
-import android.content.Context;
-import android.util.Log;
-
-import com.loopj.android.http.JsonHttpResponseHandler;
-import com.loopj.android.http.ResponseHandlerInterface;
-
-import org.json.JSONException;
import org.json.JSONObject;
import org.robolectric.annotation.Implements;
-import java.io.UnsupportedEncodingException;
-
@Implements(OneSignalRestClient.class)
public class ShadowOneSignalRestClient {
- public static JSONObject lastPost;
- public static Thread testThread;
- public static boolean failNext;
-
- public static final String testUserId = "a2f7f967-e8cc-11e4-bed1-118f05be4511";
-
- static void postSync(Context context, String url, JSONObject jsonBody, ResponseHandlerInterface responseHandler) throws UnsupportedEncodingException {
- Log.i("SHADOW_postSync", "url: " + url);
- lastPost = jsonBody;
-
- if (failNext) {
- ((JsonHttpResponseHandler)responseHandler).onFailure(400, null, new Exception(),new JSONObject());
- testThread.interrupt();
- return;
- }
-
- String retJson = null;
- if (url.contains("on_session"))
- retJson = "{}";
- else
- retJson = "{\"id\": \"" + testUserId + "\"}";
-
- try {
- ((JsonHttpResponseHandler)responseHandler).onSuccess(200, null, new JSONObject(retJson));
- } catch (JSONException e) {
- e.printStackTrace();
- }
-
- testThread.interrupt();
- }
-
- static void putSync(Context context, String url, JSONObject jsonBody, ResponseHandlerInterface responseHandler) throws UnsupportedEncodingException {
- Log.i("SHADOW_putSync", "url: " + url);
- lastPost = jsonBody;
-
- try {
- ((JsonHttpResponseHandler)responseHandler).onSuccess(200, null, new JSONObject("{\"id\": \"" + testUserId + "\"}"));
- } catch (JSONException e) {
- e.printStackTrace();
- }
-
- testThread.interrupt();
- }
-
- static void put(final Context context, final String url, JSONObject jsonBody, final ResponseHandlerInterface responseHandler) throws UnsupportedEncodingException {
- Log.i("SHADOW_put", "url: " + url);
-
- lastPost = jsonBody;
-
- try {
- ((JsonHttpResponseHandler)responseHandler).onSuccess(200, null, new JSONObject("{}"));
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
+ public static JSONObject lastPost;
+ public static Thread testThread;
+ public static boolean failNext, failAll;
+ public static String failResponse = "{}";
+ public static int networkCallCount;
+
+ public static final String testUserId = "a2f7f967-e8cc-11e4-bed1-118f05be4511";
+
+ public static boolean interruptibleDelayNext;
+ private static Thread lastInteruptiableDelayThread;
+
+ public static void interruptHTTPDelay() {
+ if (lastInteruptiableDelayThread != null) { //&& lastInteruptiableDelayThread.getState() == Thread.State.TIMED_WAITING) {
+ lastInteruptiableDelayThread.interrupt();
+ lastInteruptiableDelayThread = null;
+ }
+ }
+
+ static void safeInterrupt() {
+ if (testThread.getState() == Thread.State.TIMED_WAITING)
+ testThread.interrupt();
+ }
+
+ private static void doInterruptibleDelay() {
+ if (interruptibleDelayNext) {
+ lastInteruptiableDelayThread = Thread.currentThread();
+ interruptibleDelayNext = false;
+ try { Thread.sleep(20000); } catch (InterruptedException e) {}
+ }
+ }
+
+ private static boolean doFail(OneSignalRestClient.ResponseHandler responseHandler) {
+ if (failNext || failAll) {
+ responseHandler.onFailure(400, failResponse, new Exception());
+ safeInterrupt();
+ failNext = false;
+ return true;
+ }
+
+ return false;
+ }
+
+ static void postSync(String url, JSONObject jsonBody, OneSignalRestClient.ResponseHandler responseHandler) {
+ networkCallCount++;
+ lastPost = jsonBody;
+
+ doInterruptibleDelay();
+ if (doFail(responseHandler)) return;
+
+ String retJson = null;
+ if (url.contains("on_session"))
+ retJson = "{}";
+ else
+ retJson = "{\"id\": \"" + testUserId + "\"}";
+
+ responseHandler.onSuccess(retJson);
+
+ safeInterrupt();
+ }
+
+ static void putSync(String url, JSONObject jsonBody, OneSignalRestClient.ResponseHandler responseHandler) {
+ networkCallCount++;
+ lastPost = jsonBody;
+
+ System.out.println("lastPost:jsonBody: " + lastPost.toString());
+ System.out.println("testThread.getState()" + testThread.getState());
+
+ doInterruptibleDelay();
+ if (doFail(responseHandler)) return;
+
+ responseHandler.onSuccess("{\"id\": \"" + testUserId + "\"}");
+
+ safeInterrupt();
+ }
+
+ static void put(String url, JSONObject jsonBody, OneSignalRestClient.ResponseHandler responseHandler) {
+ networkCallCount++;
+ lastPost = jsonBody;
+
+ doInterruptibleDelay();
+ if (doFail(responseHandler)) return;
+
+ System.out.println("lastPost:jsonBody: " + lastPost.toString());
+ System.out.println("testThread.getState()" + testThread.getState());
+
+ responseHandler.onSuccess("{}");
+
+ safeInterrupt();
+ }
}
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowPushRegistratorADM.java b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowPushRegistratorADM.java
new file mode 100644
index 0000000000..95b441509f
--- /dev/null
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowPushRegistratorADM.java
@@ -0,0 +1,40 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import org.robolectric.annotation.Implements;
+
+@Implements(PushRegistratorADM.class)
+public class ShadowPushRegistratorADM {
+ public static boolean testNoClass = true;
+
+ public void __constructor__() throws ClassNotFoundException {
+ if (testNoClass)
+ throw new ClassNotFoundException();
+ }
+}
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowPushRegistratorGPS.java b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowPushRegistratorGPS.java
index 2adf6ba817..c9a073435f 100644
--- a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowPushRegistratorGPS.java
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowPushRegistratorGPS.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
@@ -31,19 +28,30 @@
package com.onesignal;
import android.content.Context;
+import android.os.SystemClock;
import org.robolectric.annotation.Implements;
@Implements(PushRegistratorGPS.class)
public class ShadowPushRegistratorGPS {
- public static final String regId = "aspdfoh0fhj02hr-2h";
+ public static final String regId = "aspdfoh0fhj02hr-2h";
+
+ public static boolean fail = false;
+ public static int waitTimer = 0;
+
+ private static PushRegistrator.RegisteredHandler lastCallback;
+
+ public static void manualFireRegisterForPush() {
+ lastCallback.complete(regId);
+ }
+
+ public void registerForPush(Context context, String googleProjectNumber, PushRegistrator.RegisteredHandler callback) {
+ lastCallback = callback;
- public static boolean failFirst = false;
+ if (waitTimer > 0)
+ SystemClock.sleep(waitTimer);
- public void registerForPush(Context context, String googleProjectNumber, PushRegistrator.RegisteredHandler callback) {
- if (failFirst)
- callback.complete(null);
- callback.complete(regId);
- }
+ callback.complete(fail ? null : regId);
+ }
}
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowRoboNotificationManager.java b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowRoboNotificationManager.java
index cf59fcc3a3..5864c128f2 100644
--- a/OneSignalSDK/app/src/test/java/com/onesignal/ShadowRoboNotificationManager.java
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/ShadowRoboNotificationManager.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
diff --git a/OneSignalSDK/app/src/test/java/com/onesignal/StaticResetHelper.java b/OneSignalSDK/app/src/test/java/com/onesignal/StaticResetHelper.java
new file mode 100644
index 0000000000..95071927d3
--- /dev/null
+++ b/OneSignalSDK/app/src/test/java/com/onesignal/StaticResetHelper.java
@@ -0,0 +1,112 @@
+// Clears static properties on OneSignal to simulate an app cold start.
+
+package com.onesignal;
+
+import org.json.JSONArray;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class StaticResetHelper {
+
+ static Collection classes = new ArrayList<>();
+
+ static {
+ classes.add(new StaticResetHelper().new ClassState(OneSignal.class, new OtherFieldHandler() {
+ @Override
+ public boolean onOtherField(Field field) throws Exception {
+ if (field.getName() == "unprocessedOpenedNotifis") {
+ field.set(null, new ArrayList());
+ return true;
+ }
+ return false;
+ }
+ }));
+
+ classes.add(new StaticResetHelper().new ClassState(OneSignalStateSynchronizer.class, new OtherFieldHandler() {
+ @Override
+ public boolean onOtherField(Field field) throws Exception {
+ if (field.getName() == "currentUserState" || field.getName() == "toSyncUserState") {
+ field.set(null, null);
+ return true;
+ }
+ return false;
+ }
+ }));
+ }
+
+ private interface OtherFieldHandler {
+ boolean onOtherField(Field field) throws Exception;
+ }
+
+ private class ClassState {
+ private OtherFieldHandler otherFieldHandler;
+ private Class stateClass;
+ private Map orginalVals = new HashMap();
+
+ ClassState(Class inClass, OtherFieldHandler inOtherFieldHandler) {
+ stateClass = inClass;
+ otherFieldHandler = inOtherFieldHandler;
+ }
+
+ private Object tryClone(Object v) throws Exception {
+ if (v instanceof Cloneable)
+ return v.getClass().getMethod("clone").invoke(v);
+ return v;
+ }
+
+ private void saveStaticValues() throws Exception {
+ Field[] allFields = stateClass.getDeclaredFields();
+ try {
+ for (Field field : allFields) {
+ int fieldModifiers = field.getModifiers();
+ if (Modifier.isStatic(fieldModifiers)
+ && !Modifier.isFinal(fieldModifiers)) {
+ field.setAccessible(true);
+ Object value = tryClone(field.get(null));
+ orginalVals.put(field, value);
+ }
+ }
+ } catch (IllegalAccessException e) {
+ System.err.println(e);
+ }
+ }
+
+ private void restSetStaticFields() throws Exception {
+ for (Map.Entry entry : orginalVals.entrySet()) {
+ Field field = entry.getKey();
+ Object value = entry.getValue();
+ Class> type = field.getType();
+ field.getName();
+ field.setAccessible(true);
+
+ if (!otherFieldHandler.onOtherField(field))
+ field.set(null, tryClone(value));
+ }
+ }
+ }
+
+ public static void saveStaticValues() {
+ for(ClassState aClass : classes) {
+ try {
+ aClass.saveStaticValues();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static void restSetStaticFields() {
+ for(ClassState aClass : classes) {
+ try {
+ aClass.restSetStaticFields();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/OneSignalSDK/app/src/test/java/com/test/onesignal/CustomRobolectricTestRunner.java b/OneSignalSDK/app/src/test/java/com/test/onesignal/CustomRobolectricTestRunner.java
index bc1dd6a5ac..f554e6f14e 100644
--- a/OneSignalSDK/app/src/test/java/com/test/onesignal/CustomRobolectricTestRunner.java
+++ b/OneSignalSDK/app/src/test/java/com/test/onesignal/CustomRobolectricTestRunner.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
diff --git a/OneSignalSDK/app/src/test/java/com/test/onesignal/GenerateNotificationRunner.java b/OneSignalSDK/app/src/test/java/com/test/onesignal/GenerateNotificationRunner.java
index 5e229dc485..86fac0f027 100644
--- a/OneSignalSDK/app/src/test/java/com/test/onesignal/GenerateNotificationRunner.java
+++ b/OneSignalSDK/app/src/test/java/com/test/onesignal/GenerateNotificationRunner.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
@@ -34,8 +31,6 @@
import android.app.Notification;
import android.content.Intent;
import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@@ -65,8 +60,6 @@
import java.util.List;
-import static org.robolectric.Shadows.shadowOf;
-
@Config(packageName = "com.onesignal.example",
constants = BuildConfig.class,
shadows = { ShadowRoboNotificationManager.class, ShadowOneSignalRestClient.class },
@@ -74,7 +67,7 @@
@RunWith(CustomRobolectricTestRunner.class)
public class GenerateNotificationRunner {
- private Activity blankActiviy;
+ private Activity blankActivity;
private static final String notifMessage = "Robo test message";
@@ -90,8 +83,8 @@ public void beforeEachTest() throws Exception {
// Robolectric mocks System.currentTimeMillis() to 0, we need the current real time to match our SQL records.
ShadowSystemClock.setCurrentTimeMillis(System.currentTimeMillis());
- blankActiviy = Robolectric.buildActivity(BlankActivity.class).create().get();
- blankActiviy.getApplicationInfo().name = "UnitTestApp";
+ blankActivity = Robolectric.buildActivity(BlankActivity.class).create().get();
+ blankActivity.getApplicationInfo().name = "UnitTestApp";
// Add our launcher Activity to the run time to simulate a real app.
// getRobolectricPackageManager is null if run in BeforeClass for some reason.
@@ -132,13 +125,13 @@ private Intent createOpenIntent(Bundle bundle) {
public void shouldSetTitleCorrectly() throws Exception {
// Should use app's Title by default
Bundle bundle = getBaseNotifBundle();
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
Assert.assertEquals("UnitTestApp", ShadowRoboNotificationManager.lastNotif.getContentTitle());
// Should allow title from GCM payload.
bundle = getBaseNotifBundle("UUID2");
bundle.putString("title", "title123");
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
Assert.assertEquals("title123", ShadowRoboNotificationManager.lastNotif.getContentTitle());
}
@@ -146,11 +139,11 @@ public void shouldSetTitleCorrectly() throws Exception {
public void shouldHandleBasicNotifications() throws Exception {
// Make sure the notification got posted and the content is correct.
Bundle bundle = getBaseNotifBundle();
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
Assert.assertEquals(notifMessage, ShadowRoboNotificationManager.lastNotif.getContentText());
// Should have 1 DB record with the correct time stamp
- SQLiteDatabase readableDb = new OneSignalDbHelper(blankActiviy).getReadableDatabase();
+ SQLiteDatabase readableDb = new OneSignalDbHelper(blankActivity).getReadableDatabase();
Cursor cursor = readableDb.query(NotificationTable.TABLE_NAME, new String[] { "created_time" }, null, null, null, null, null);
Assert.assertEquals(1, cursor.getCount());
// Time stamp should be set and within a small range.
@@ -159,20 +152,20 @@ public void shouldHandleBasicNotifications() throws Exception {
Assert.assertTrue(cursor.getLong(0) > currentTime - 2 && cursor.getLong(0) <= currentTime);
// Should get marked as opened.
- NotificationOpenedProcessor.processFromActivity(blankActiviy, createOpenIntent(bundle));
+ NotificationOpenedProcessor.processFromActivity(blankActivity, createOpenIntent(bundle));
cursor = readableDb.query(NotificationTable.TABLE_NAME, new String[] { "opened", "android_notification_id" }, null, null, null, null, null);
cursor.moveToFirst();
Assert.assertEquals(1, cursor.getInt(0));
int firstNotifId = cursor.getInt(1);
// Should not display a duplicate notification, count should still be 1
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
cursor = readableDb.query(NotificationTable.TABLE_NAME, null, null, null, null, null, null);
Assert.assertEquals(1, cursor.getCount());
// Display a second notification
bundle = getBaseNotifBundle("UUID2");
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
cursor = readableDb.query(NotificationTable.TABLE_NAME, new String[] { "android_notification_id" }, "android_notification_id <> " + firstNotifId, null, null, null, null);
cursor.moveToFirst();
int secondNotifId = cursor.getInt(0);
@@ -184,7 +177,7 @@ public void shouldHandleBasicNotifications() throws Exception {
// Should of been added for a total of 2 records now.
// First opened should of been cleaned up, 1 week old non opened notification should stay, and one new record.
bundle = getBaseNotifBundle("UUID3");
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
cursor = readableDb.query(NotificationTable.TABLE_NAME, new String[] { "android_notification_id" }, null, null, null, null, null);
Assert.assertEquals(2, cursor.getCount());
@@ -204,7 +197,7 @@ public void shouldGenerate2BasicGroupNotifications() throws Exception {
// Make sure the notification got posted and the content is correct.
Bundle bundle = getBaseNotifBundle();
bundle.putString("grp", "test1");
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
List postedNotifs = ShadowRoboNotificationManager.notifications;
Assert.assertEquals(2, postedNotifs.size());
@@ -219,7 +212,7 @@ public void shouldGenerate2BasicGroupNotifications() throws Exception {
// Should be 2 DB entries (summary and individual)
- SQLiteDatabase readableDb = new OneSignalDbHelper(blankActiviy).getReadableDatabase();
+ SQLiteDatabase readableDb = new OneSignalDbHelper(blankActivity).getReadableDatabase();
Cursor cursor = readableDb.query(NotificationTable.TABLE_NAME, null, null, null, null, null, null);
Assert.assertEquals(2, cursor.getCount());
@@ -230,7 +223,7 @@ public void shouldGenerate2BasicGroupNotifications() throws Exception {
bundle.putString("alert", "Notif test 2");
bundle.putString("custom", "{\"i\": \"UUID2\"}");
bundle.putString("grp", "test1");
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
postedNotifs = ShadowRoboNotificationManager.notifications;
Assert.assertEquals(2, postedNotifs.size());
@@ -250,7 +243,7 @@ public void shouldGenerate2BasicGroupNotifications() throws Exception {
// Open summary notification
Intent intent = createOpenIntent(postedNotifs.get(0).id, bundle).putExtra("summary", "test1");
- NotificationOpenedProcessor.processFromActivity(blankActiviy, intent);
+ NotificationOpenedProcessor.processFromActivity(blankActivity, intent);
// Send 3rd notification
ShadowRoboNotificationManager.notifications.clear();
@@ -258,7 +251,7 @@ public void shouldGenerate2BasicGroupNotifications() throws Exception {
bundle.putString("alert", "Notif test 3");
bundle.putString("custom", "{\"i\": \"UUID3\"}");
bundle.putString("grp", "test1");
- NotificationBundleProcessor.Process(blankActiviy, bundle);
+ NotificationBundleProcessor.Process(blankActivity, bundle);
Assert.assertEquals("Notif test 3", postedNotifs.get(0).notif.getContentText());
Assert.assertEquals(Notification.FLAG_GROUP_SUMMARY, postedNotifs.get(0).notif.getRealNotification().flags & Notification.FLAG_GROUP_SUMMARY);
diff --git a/OneSignalSDK/app/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java b/OneSignalSDK/app/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java
index 282e8e6460..eb87f6258f 100644
--- a/OneSignalSDK/app/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java
+++ b/OneSignalSDK/app/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
@@ -31,14 +28,21 @@
package com.test.onesignal;
import android.app.Activity;
-import android.content.Intent;
+import android.app.Service;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.os.Bundle;
import com.onesignal.BuildConfig;
import com.onesignal.NotificationBundleProcessor;
import com.onesignal.OneSignal;
+import com.onesignal.ShadowOSUtils;
import com.onesignal.ShadowOneSignalRestClient;
+import com.onesignal.OneSignalPackagePrivateHelper;
import com.onesignal.ShadowPushRegistratorADM;
import com.onesignal.ShadowPushRegistratorGPS;
+import com.onesignal.StaticResetHelper;
+import com.onesignal.SyncService;
import com.onesignal.example.BlankActivity;
import junit.framework.Assert;
@@ -50,24 +54,32 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowConnectivityManager;
import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowSystemClock;
+import org.robolectric.util.ActivityController;
import java.lang.reflect.Field;
-
@Config(packageName = "com.onesignal.example",
constants = BuildConfig.class,
- shadows = {ShadowOneSignalRestClient.class, ShadowPushRegistratorGPS.class, ShadowPushRegistratorADM.class},
+ shadows = {ShadowOneSignalRestClient.class, ShadowPushRegistratorGPS.class, ShadowPushRegistratorADM.class, ShadowOSUtils.class},
sdk = 21)
+
@RunWith(CustomRobolectricTestRunner.class)
public class MainOneSignalClassRunner {
+ private static final String ONESIGNAL_APP_ID = "b2f7f966-d8cc-11e4-bed1-df8f05be55ba";
private static Field OneSignal_CurrentSubscription;
- private Activity blankActiviy;
+ private Activity blankActivity;
private static String callBackUseId, getCallBackRegId;
private static String notificationOpenedMessage;
-
+ private static JSONObject lastGetTags;
+ private ActivityController blankActivityController;
+
public static void GetIdsAvailable() {
OneSignal.idsAvailable(new OneSignal.IdsAvailableHandler() {
@Override
@@ -82,7 +94,7 @@ public void idsAvailable(String userId, String registrationId) {
public static void setUpClass() throws Exception {
ShadowLog.stream = System.out;
- OneSignal_CurrentSubscription = OneSignal.class.getDeclaredField("currentSubscription");
+ OneSignal_CurrentSubscription = OneSignal.class.getDeclaredField("subscribableStatus");
OneSignal_CurrentSubscription.setAccessible(true);
OneSignal.setLogLevel(OneSignal.LOG_LEVEL.VERBOSE, OneSignal.LOG_LEVEL.NONE);
@@ -93,33 +105,70 @@ public static void setUpClass() throws Exception {
public void beforeEachTest() throws Exception {
callBackUseId = getCallBackRegId = null;
StaticResetHelper.restSetStaticFields();
- blankActiviy = Robolectric.buildActivity(BlankActivity.class).create().get();
+ blankActivityController = Robolectric.buildActivity(BlankActivity.class).create();
+ blankActivity = blankActivityController.get();
ShadowOneSignalRestClient.failNext = false;
+ ShadowOneSignalRestClient.failAll = false;
+ ShadowOneSignalRestClient.interruptibleDelayNext = false;
+ ShadowOneSignalRestClient.networkCallCount = 0;
ShadowOneSignalRestClient.testThread = Thread.currentThread();
- ShadowPushRegistratorGPS.failFirst = false;
+
+ ShadowPushRegistratorGPS.fail = false;
notificationOpenedMessage = null;
}
@Test
public void testOpenFromNotificationWhenAppIsDead() throws Exception {
- Intent intent = new Intent();
- intent.putExtra("onesignal_data", "[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\" } }]");
+ OneSignal.handleNotificationOpened(blankActivity, new JSONArray("[{ \"alert\": \"Robo test message\", \"custom\": { \"i\": \"UUID\" } }]"), false);
- blankActiviy.setIntent(intent);
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba", new OneSignal.NotificationOpenedHandler() {
+ OneSignal.init(blankActivity, "123456789", ONESIGNAL_APP_ID, new OneSignal.NotificationOpenedHandler() {
@Override
public void notificationOpened(String message, JSONObject additionalData, boolean isActive) {
notificationOpenedMessage = message;
}
});
- Assert.assertEquals("Test Msg", notificationOpenedMessage);
+ threadAndTaskWait();
+
+ Assert.assertEquals("Robo test message", notificationOpenedMessage);
+ }
+
+ @Test
+ public void shouldNotFireNotificationOpenAgainAfterAppRestart() throws Exception {
+ OneSignal.init(blankActivity, "123456789", ONESIGNAL_APP_ID, new OneSignal.NotificationOpenedHandler() {
+ @Override
+ public void notificationOpened(String message, JSONObject additionalData, boolean isActive) {
+ notificationOpenedMessage = message;
+ }
+ });
+
+ threadAndTaskWait();
+
+ Bundle bundle = GenerateNotificationRunner.getBaseNotifBundle();
+ NotificationBundleProcessor.Process(blankActivity, bundle);
+
+ threadAndTaskWait();
+
+ notificationOpenedMessage = null;
+
+ // Restart app - Should omit notification_types
+ StaticResetHelper.restSetStaticFields();
+ OneSignal.init(blankActivity, "123456789", ONESIGNAL_APP_ID, new OneSignal.NotificationOpenedHandler() {
+ @Override
+ public void notificationOpened(String message, JSONObject additionalData, boolean isActive) {
+ notificationOpenedMessage = message;
+ }
+ });
+
+ threadAndTaskWait();
+
+ Assert.assertEquals(null, notificationOpenedMessage);
}
@Test
public void testOpenFromNotificationWhenAppIsInBackground() throws Exception {
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba", new OneSignal.NotificationOpenedHandler() {
+ OneSignal.init(blankActivity, "123456789", ONESIGNAL_APP_ID, new OneSignal.NotificationOpenedHandler() {
@Override
public void notificationOpened(String message, JSONObject additionalData, boolean isActive) {
notificationOpenedMessage = message;
@@ -127,40 +176,33 @@ public void notificationOpened(String message, JSONObject additionalData, boolea
});
Assert.assertNull(notificationOpenedMessage);
- OneSignal.handleNotificationOpened(blankActiviy, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\" } }]"));
+ OneSignal.handleNotificationOpened(blankActivity, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\" } }]"), false);
Assert.assertEquals("Test Msg", notificationOpenedMessage);
+ threadWait();
}
@Test
public void testNotificationReceivedWhenAppInFocus() throws Exception {
- // Tests seem to be over lapping when running them all. Wait a bit before running this test.
- try {Thread.sleep(1000);} catch (Throwable t) {}
-
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba", new OneSignal.NotificationOpenedHandler() {
+ OneSignal.init(blankActivity, "123456789", ONESIGNAL_APP_ID, new OneSignal.NotificationOpenedHandler() {
@Override
public void notificationOpened(String message, JSONObject additionalData, boolean isActive) {
notificationOpenedMessage = message;
}
});
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ threadAndTaskWait();
Assert.assertNull(notificationOpenedMessage);
- NotificationBundleProcessor.Process(blankActiviy, GenerateNotificationRunner.getBaseNotifBundle());
- try {Thread.sleep(100);} catch (Throwable t) {}
- Robolectric.getForegroundThreadScheduler().runOneTask();
- Robolectric.getForegroundThreadScheduler().runOneTask();
+ NotificationBundleProcessor.Process(blankActivity, GenerateNotificationRunner.getBaseNotifBundle());
+ threadAndTaskWait();
Assert.assertEquals("Robo test message", notificationOpenedMessage);
}
@Test
public void testInvalidGoogleProjectNumber() throws Exception {
GetIdsAvailable();
- // Tests seem to be over lapping when running them all. Wait a bit before running this test.
- try {Thread.sleep(1000);} catch (Throwable t) {}
-
- OneSignal.init(blankActiviy, "NOT A VALID Google project number", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
+ OneSignalInitWithBadProjectNum();
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ threadAndTaskWait();
Robolectric.getForegroundThreadScheduler().runOneTask();
Assert.assertEquals(-6, ShadowOneSignalRestClient.lastPost.getInt("notification_types"));
@@ -171,8 +213,8 @@ public void testInvalidGoogleProjectNumber() throws Exception {
@Test
public void testUnsubcribeShouldMakeRegIdNullToIdsAvailable() throws Exception {
GetIdsAvailable();
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInit();
+ threadAndTaskWait();
Assert.assertEquals(ShadowPushRegistratorGPS.regId, ShadowOneSignalRestClient.lastPost.getString("identifier"));
Robolectric.getForegroundThreadScheduler().runOneTask();
@@ -180,13 +222,14 @@ public void testUnsubcribeShouldMakeRegIdNullToIdsAvailable() throws Exception {
OneSignal.setSubscription(false);
GetIdsAvailable();
+ threadAndTaskWait();
Assert.assertNull(getCallBackRegId);
}
@Test
public void testSetSubscriptionShouldNotOverrideSubscribeError() throws Exception {
- OneSignal.init(blankActiviy, "NOT A VALID Google project number", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInitWithBadProjectNum();
+ threadAndTaskWait();
// Should not try to update server
ShadowOneSignalRestClient.lastPost = null;
@@ -195,71 +238,69 @@ public void testSetSubscriptionShouldNotOverrideSubscribeError() throws Exceptio
// Restart app - Should omit notification_types
StaticResetHelper.restSetStaticFields();
- OneSignal.init(blankActiviy, "NOT A VALID Google project number", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInitWithBadProjectNum();
+ threadAndTaskWait();
Assert.assertFalse(ShadowOneSignalRestClient.lastPost.has("notification_types"));
}
@Test
public void shouldNotResetSubscriptionOnSession() throws Exception {
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
+ OneSignalInit();
OneSignal.setSubscription(false);
-
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ threadAndTaskWait();
Assert.assertEquals(-2, ShadowOneSignalRestClient.lastPost.getInt("notification_types"));
+
StaticResetHelper.restSetStaticFields();
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
- //System.out.println(ShadowOneSignalRestClient.lastPost.getInt("notification_types"));
+ OneSignalInit();
+ threadAndTaskWait();
Assert.assertFalse(ShadowOneSignalRestClient.lastPost.has("notification_types"));
}
@Test
public void shouldSetSubscriptionCorrectlyEvenAfterFirstOneSignalRestInitFail() throws Exception {
// Failed to register with OneSignal but SetSubscription was called with false
- ShadowOneSignalRestClient.failNext = true;
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
+ ShadowOneSignalRestClient.failAll = true;
+ OneSignalInit();
OneSignal.setSubscription(false);
- try {Thread.sleep(5000);} catch (Throwable t) {}
- ShadowOneSignalRestClient.failNext = false;
+ threadAndTaskWait();
+ ShadowOneSignalRestClient.failAll = false;
// Restart app - Should send unsubscribe with create player call.
StaticResetHelper.restSetStaticFields();
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInit();
+ threadAndTaskWait();
Assert.assertEquals(-2, ShadowOneSignalRestClient.lastPost.getInt("notification_types"));
// Restart app again - Value synced last time so don't send again.
StaticResetHelper.restSetStaticFields();
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInit();
+ threadAndTaskWait();
Assert.assertFalse(ShadowOneSignalRestClient.lastPost.has("notification_types"));
}
@Test
public void shouldUpdateNotificationTypesCorrectlyEvenWhenSetSubscriptionIsCalledInAnErrorState() throws Exception {
- // Failed to register with bad google project number then set subscription called at any point.
- OneSignal.init(blankActiviy, "Bad_Google_Project_Number", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInitWithBadProjectNum();
+ threadAndTaskWait();
OneSignal.setSubscription(true);
// Restart app - Should send subscribe with on_session call.
StaticResetHelper.restSetStaticFields();
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInit();
+ threadAndTaskWait();
Assert.assertEquals(1, ShadowOneSignalRestClient.lastPost.getInt("notification_types"));
}
@Test
public void shouldAllowMultipleSetSubscription() throws Exception {
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ OneSignalInit();
+ threadAndTaskWait();
OneSignal.setSubscription(false);
- try {Thread.sleep(5000);} catch (Throwable t) {}
+ threadAndTaskWait();
Assert.assertEquals(-2, ShadowOneSignalRestClient.lastPost.getInt("notification_types"));
@@ -271,11 +312,13 @@ public void shouldAllowMultipleSetSubscription() throws Exception {
OneSignal.setSubscription(true);
+ threadAndTaskWait();
Assert.assertEquals(1, ShadowOneSignalRestClient.lastPost.getInt("notification_types"));
// Should not resend same value
ShadowOneSignalRestClient.lastPost = null;
OneSignal.setSubscription(true);
+ threadAndTaskWait();
Assert.assertNull(ShadowOneSignalRestClient.lastPost);
}
@@ -283,7 +326,7 @@ public void shouldAllowMultipleSetSubscription() throws Exception {
@Test
public void shouldNotFireIdsAvailableWithoutUserId() throws Exception {
ShadowOneSignalRestClient.failNext = true;
- ShadowPushRegistratorGPS.failFirst = true;
+ ShadowPushRegistratorGPS.fail = true;
OneSignal.idsAvailable(new OneSignal.IdsAvailableHandler() {
@Override
@@ -292,8 +335,290 @@ public void idsAvailable(String userId, String registrationId) {
userIdWasNull = true;
}
});
- OneSignal.init(blankActiviy, "123456789", "b2f7f966-d8cc-11e4-bed1-df8f05be55ba");
+ OneSignalInit();
Assert.assertFalse(userIdWasNull);
+ threadAndTaskWait();
+ }
+
+ @Test
+ public void testGCMTimeOutThenSuccessesLater() throws Exception {
+ // Init with a bad connection to Google.
+ ShadowPushRegistratorGPS.fail = true;
+ OneSignalInit();
+ threadAndTaskWait();
+ Assert.assertFalse(ShadowOneSignalRestClient.lastPost.has("identifier"));
+
+ // Registers for GCM after a retry
+ ShadowPushRegistratorGPS.fail = false;
+ ShadowPushRegistratorGPS.manualFireRegisterForPush();
+ threadAndTaskWait();
+ Assert.assertEquals(ShadowPushRegistratorGPS.regId, ShadowOneSignalRestClient.lastPost.getString("identifier"));
+
+ // Cold restart app, should not send the same identifier again.
+ ShadowOneSignalRestClient.lastPost = null;
+ StaticResetHelper.restSetStaticFields();
+ OneSignalInit();
+ threadAndTaskWait();
+ Assert.assertFalse(ShadowOneSignalRestClient.lastPost.has("identifier"));
+ }
+
+ @Test
+ public void testChangeAppId() throws Exception {
+ OneSignalInit();
+ threadAndTaskWait();
+
+ int normalCreateFieldCount = ShadowOneSignalRestClient.lastPost.length();
+ StaticResetHelper.restSetStaticFields();
+ OneSignal.init(blankActivity, "123456789", "99f7f966-d8cc-11e4-bed1-df8f05be55b2");
+ threadAndTaskWait();
+
+ Assert.assertEquals(normalCreateFieldCount, ShadowOneSignalRestClient.lastPost.length());
+ }
+
+ @Test
+ public void testUserDeletedFromServer() throws Exception {
+ // First cold boot normal
+ OneSignalInit();
+ threadAndTaskWait();
+
+ int normalCreateFieldCount = ShadowOneSignalRestClient.lastPost.length();
+ ShadowOneSignalRestClient.lastPost = null;
+
+ // Developer deletes user, cold boots apps should resend all fields
+ StaticResetHelper.restSetStaticFields();
+ ShadowOneSignalRestClient.failNext = true;
+ ShadowOneSignalRestClient.failResponse = "{\"errors\":[\"Device type is not a valid device_type. Valid options are: 0 = iOS, 1 = Android, 2 = Amazon, 3 = WindowsPhone(MPNS), 4 = ChromeApp, 5 = ChromeWebsite, 6 = WindowsPhone(WNS), 7 = Safari(APNS), 8 = Firefox\"]}";
+ OneSignalInit();
+ threadAndTaskWait();
+
+ System.out.println("ShadowOneSignalRestClient.lastPost: " + ShadowOneSignalRestClient.lastPost);
+ Assert.assertEquals(normalCreateFieldCount, ShadowOneSignalRestClient.lastPost.length());
+
+
+ // Developer deletes users again from dashboard while app is running.
+ ShadowOneSignalRestClient.lastPost = null;
+ ShadowOneSignalRestClient.failNext = true;
+ ShadowOneSignalRestClient.failResponse = "{\"errors\":[\"No user with this id found\"]}";
+ OneSignal.sendTag("key1", "value1");
+ threadAndTaskWait();
+
+ System.out.println("ShadowOneSignalRestClient.lastPost: " + ShadowOneSignalRestClient.lastPost);
+ Assert.assertEquals(normalCreateFieldCount, ShadowOneSignalRestClient.lastPost.length() - 1);
+ }
+
+ @Test
+ public void testOfflineCrashes() throws Exception {
+ ConnectivityManager connectivityManager = (ConnectivityManager)RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE);
+ ShadowConnectivityManager shadowConnectivityManager = Shadows.shadowOf(connectivityManager);
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+
+ OneSignalInit();
+ threadAndTaskWait();
+
+ OneSignal.sendTag("key", "value");
+ threadAndTaskWait();
+
+ OneSignal.setSubscription(false);
+ threadAndTaskWait();
+ }
+
+ // ####### SendTags Tests ########
+
+ @Test
+ public void shouldSendTags() throws Exception {
+ OneSignalInit();
+ OneSignal.sendTags(new JSONObject("{\"test1\": \"value1\", \"test2\": \"value2\"}"));
+ threadAndTaskWait();
+ Assert.assertEquals(1, ShadowOneSignalRestClient.networkCallCount);
+ Assert.assertEquals(ONESIGNAL_APP_ID, ShadowOneSignalRestClient.lastPost.getString("app_id"));
+ Assert.assertEquals("value1", ShadowOneSignalRestClient.lastPost.getJSONObject("tags").getString("test1"));
+ Assert.assertEquals("value2", ShadowOneSignalRestClient.lastPost.getJSONObject("tags").getString("test2"));
+
+ // Should omit sending repeated tags
+ ShadowOneSignalRestClient.lastPost = null;
+ OneSignal.sendTags(new JSONObject("{\"test1\": \"value1\", \"test2\": \"value2\"}"));
+ threadAndTaskWait();
+ Assert.assertEquals(1, ShadowOneSignalRestClient.networkCallCount);
+ Assert.assertNull(ShadowOneSignalRestClient.lastPost);
+
+ // Should only send changed and new tags
+ OneSignal.sendTags(new JSONObject("{\"test1\": \"value1.5\", \"test2\": \"value2\", \"test3\": \"value3\"}"));
+ threadAndTaskWait();
+ Assert.assertEquals(2, ShadowOneSignalRestClient.networkCallCount);
+ JSONObject sentTags = ShadowOneSignalRestClient.lastPost.getJSONObject("tags");
+ Assert.assertEquals("value1.5", sentTags.getString("test1"));
+ Assert.assertFalse(sentTags.has(("test2")));
+ Assert.assertEquals("value3", sentTags.getString("test3"));
+ }
+
+ @Test
+ public void shouldSendTagsWithRequestBatching() throws Exception {
+ OneSignalInit();
+ threadAndTaskWait();
+ Assert.assertEquals(1, ShadowOneSignalRestClient.networkCallCount);
+ OneSignal.sendTags(new JSONObject("{\"test1\": \"value1\"}"));
+ OneSignal.sendTags(new JSONObject("{\"test2\": \"value2\"}"));
+
+ OneSignal.getTags(new OneSignal.GetTagsHandler() {
+ @Override
+ public void tagsAvailable(JSONObject tags) {
+ lastGetTags = tags;
+ System.out.println("tags: " + tags);
+ }
+ });
+ threadAndTaskWait();
+ threadAndTaskWait();
+
+ System.out.println("lastGetTags: " + lastGetTags);
+ Assert.assertEquals("value1", lastGetTags.getString("test1"));
+ Assert.assertEquals("value2", lastGetTags.getString("test2"));
+ Assert.assertEquals(2, ShadowOneSignalRestClient.networkCallCount);
+ }
+
+ @Test
+ public void shouldSaveToSyncIfKilledBeforeDelayedCompare() throws Exception {
+ OneSignalInit();
+ OneSignal.sendTag("key", "value");
+ threadWait();
+
+ OneSignalPackagePrivateHelper.SyncService_onTaskRemoved();
+ OneSignalPackagePrivateHelper.resetRunnables();
+ threadAndTaskWait();
+ Assert.assertEquals(0, ShadowOneSignalRestClient.networkCallCount);
+
+ StaticResetHelper.restSetStaticFields();
+
+ OneSignalInit();
+ threadAndTaskWait();
+ Assert.assertEquals("value", ShadowOneSignalRestClient.lastPost.getJSONObject("tags").getString("key"));
+ }
+
+ @Test
+ public void shouldSyncPendingChangesFromSyncService() throws Exception {
+ OneSignalInit();
+ threadAndTaskWait();
+
+ OneSignal.sendTag("key", "value");
+ OneSignalPackagePrivateHelper.SyncService_onTaskRemoved();
+ OneSignalPackagePrivateHelper.resetRunnables();
+ threadAndTaskWait();
+ Assert.assertEquals(1, ShadowOneSignalRestClient.networkCallCount);
+
+ StaticResetHelper.restSetStaticFields();
+
+ Service service = new SyncService();
+ service.onCreate();
+ threadAndTaskWait();
+ Assert.assertEquals("value", ShadowOneSignalRestClient.lastPost.getJSONObject("tags").getString("key"));
+ }
+
+ @Test
+ public void shouldNotCrashIfOnTaskRemovedIsCalledBeforeInitIsDone() {
+ OneSignalPackagePrivateHelper.SyncService_onTaskRemoved();
+ }
+
+ // ####### GetTags Tests ########
+
+ @Test
+ public void shouldGetTags() throws Exception {
+ OneSignalInit();
+ OneSignal.sendTags(new JSONObject("{\"test1\": \"value1\", \"test2\": \"value2\"}"));
+ threadAndTaskWait();
+ OneSignal.getTags(new OneSignal.GetTagsHandler() {
+ @Override
+ public void tagsAvailable(JSONObject tags) {
+ lastGetTags = tags;
+ }
+ });
+
+ Assert.assertEquals("value1", lastGetTags.getString("test1"));
+ Assert.assertEquals("value2", lastGetTags.getString("test2"));
+ }
+
+ // ####### on_focus Tests ########
+
+ @Test
+ public void sendsOnFocus() throws Exception {
+ OneSignalInit();
+ threadAndTaskWait();
+ blankActivityController.resume();
+ ShadowSystemClock.setCurrentTimeMillis(60 * 1000);
+
+ blankActivityController.pause();
+ threadAndTaskWait();
+ Assert.assertEquals(60, ShadowOneSignalRestClient.lastPost.getInt("active_time"));
+ Assert.assertEquals(2, ShadowOneSignalRestClient.networkCallCount);
+ }
+
+ /*
+ // Can't get test to work from a app flow due to the main thread being locked one way or another in a robolectric env.
+ // Running ActivityLifecycleListener.focusHandlerThread...advanceToNextPostedRunnable waits on the main thread.
+ // If it is put in its own thread then synchronized that is run when messages a runnable is added / removed hangs the main thread here too.
+ @Test
+ public void shouldNotDoubleCountFocusTime() throws Exception {
+ System.out.println("TEST IS RUNNING ONE THREAD: " + Thread.currentThread());
+
+ // Start app normally
+ OneSignalInit();
+ threadAndTaskWait();
+
+ // Press home button after 30 sec
+ blankActivityController.resume();
+ ShadowSystemClock.setCurrentTimeMillis(30 * 1000);
+ blankActivityController.pause();
+ threadAndTaskWait();
+
+ // Press home button after 30 more sec, with a network hang
+ blankActivityController.resume();
+ ShadowSystemClock.setCurrentTimeMillis(60 * 1000);
+ ShadowOneSignalRestClient.interruptibleDelayNext = true;
+ blankActivityController.pause();
+ System.out.println("HERE1");
+ threadAndTaskWait();
+ System.out.println("HERE2" + Thread.currentThread());
+
+ // Open app and press home button again right away.
+ blankActivityController.resume();
+ System.out.println("HERE3: " + Thread.currentThread());
+ blankActivityController.pause();
+ System.out.println("HERE4");
+ threadAndTaskWait();
+ System.out.println("HERE5");
+
+ ShadowOneSignalRestClient.interruptHTTPDelay();
+ System.out.println("HERE6");
+
+ threadWait();
+ System.out.println("ShadowOneSignalRestClient.lastPost: " + ShadowOneSignalRestClient.lastPost);
+ System.out.println("ShadowOneSignalRestClient.networkCallCount: " + ShadowOneSignalRestClient.networkCallCount);
+
+ Assert.assertEquals(60, ShadowOneSignalRestClient.lastPost.getInt("active_time"));
+ Assert.assertEquals(2, ShadowOneSignalRestClient.networkCallCount);
+ }
+ */
+
+
+ // ####### Unit test helper methods ########
+
+ private static void threadWait() {
+ try {Thread.sleep(1000);} catch (Throwable t) {}
+ }
+
+ private void threadAndTaskWait() {
+ try {Thread.sleep(300);} catch (Throwable t) {}
+ OneSignalPackagePrivateHelper.runAllNetworkRunnable();
+ OneSignalPackagePrivateHelper.runFocusRunnables();
+
+ Robolectric.getForegroundThreadScheduler().runOneTask();
+ }
+
+ private void OneSignalInit() {
+ OneSignal.setLogLevel(OneSignal.LOG_LEVEL.DEBUG, OneSignal.LOG_LEVEL.NONE);
+ OneSignal.init(blankActivity, "123456789", ONESIGNAL_APP_ID);
+ }
+
+ private void OneSignalInitWithBadProjectNum() {
+ OneSignal.init(blankActivity, "NOT A VALID Google project number", ONESIGNAL_APP_ID);
}
}
\ No newline at end of file
diff --git a/OneSignalSDK/app/src/test/java/com/test/onesignal/PushRegistratorRunner.java b/OneSignalSDK/app/src/test/java/com/test/onesignal/PushRegistratorRunner.java
index ccf5b18336..664820bf06 100644
--- a/OneSignalSDK/app/src/test/java/com/test/onesignal/PushRegistratorRunner.java
+++ b/OneSignalSDK/app/src/test/java/com/test/onesignal/PushRegistratorRunner.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
@@ -55,7 +52,7 @@
@RunWith(CustomRobolectricTestRunner.class)
public class PushRegistratorRunner {
- private Activity blankActiviy;
+ private Activity blankActivity;
private static boolean callbackFired;
@BeforeClass // Runs only once, before any tests
@@ -65,7 +62,7 @@ public static void setUpClass() throws Exception {
@Before // Before each test
public void beforeEachTest() throws Exception {
- blankActiviy = Robolectric.buildActivity(BlankActivity.class).create().get();
+ blankActivity = Robolectric.buildActivity(BlankActivity.class).create().get();
callbackFired = false;
ShadowGoogleCloudMessaging.exists = true;
}
@@ -75,7 +72,7 @@ public void testGooglePlayServicesAPKMissingOnDevice() throws Exception {
PushRegistratorGPS pushReg = new PushRegistratorGPS();
final Thread testThread = Thread.currentThread();
- pushReg.registerForPush(blankActiviy, "", new PushRegistrator.RegisteredHandler() {
+ pushReg.registerForPush(blankActivity, "", new PushRegistrator.RegisteredHandler() {
@Override
public void complete(String id) {
System.out.println("HERE: " + id);
@@ -95,7 +92,7 @@ public void testGCMPartOfGooglePlayServicesMissing() throws Exception {
final Thread testThread = Thread.currentThread();
- pushReg.registerForPush(blankActiviy, "", new PushRegistrator.RegisteredHandler() {
+ pushReg.registerForPush(blankActivity, "", new PushRegistrator.RegisteredHandler() {
@Override
public void complete(String id) {
System.out.println("HERE: " + id);
diff --git a/OneSignalSDK/build.gradle b/OneSignalSDK/build.gradle
index c6371d6cdc..a6fcfe1fe9 100644
--- a/OneSignalSDK/build.gradle
+++ b/OneSignalSDK/build.gradle
@@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.3.1'
+ classpath 'com.android.tools.build:gradle:1.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/OneSignalSDK/onesignal/.gitignore b/OneSignalSDK/onesignal/.gitignore
index 4c974cf4b9..fa20859984 100644
--- a/OneSignalSDK/onesignal/.gitignore
+++ b/OneSignalSDK/onesignal/.gitignore
@@ -1,2 +1,3 @@
/build
-gradle.properties
\ No newline at end of file
+gradle.properties
+!libs/*
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/build.gradle b/OneSignalSDK/onesignal/build.gradle
index cd5fac5cbe..26208aa12f 100644
--- a/OneSignalSDK/onesignal/build.gradle
+++ b/OneSignalSDK/onesignal/build.gradle
@@ -2,9 +2,7 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion 23
- buildToolsVersion "23.0.1"
-
- useLibrary 'org.apache.http.legacy'
+ buildToolsVersion "23.0.2"
defaultConfig {
minSdkVersion 10
@@ -21,8 +19,10 @@ android {
dependencies {
provided fileTree(dir: 'libs', include: ['*.jar'])
- compile "com.google.android.gms:play-services-gcm:7.3.0"
- compile "com.google.android.gms:play-services-analytics:7.3.0"
+ compile "com.google.android.gms:play-services-gcm:8.3.0"
+ compile "com.google.android.gms:play-services-analytics:8.3.0"
+ compile "com.google.android.gms:play-services-location:8.3.0"
+ compile 'com.android.support:appcompat-v7:23.1.1'
}
apply from: 'maven-push.gradle'
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/onesignal.iml b/OneSignalSDK/onesignal/onesignal.iml
index 4aa4bde524..2f2026a438 100644
--- a/OneSignalSDK/onesignal/onesignal.iml
+++ b/OneSignalSDK/onesignal/onesignal.iml
@@ -9,15 +9,14 @@
-
+
-
-
+
+
+ generateDebugAndroidTestSourcesgenerateDebugSources
- mockableAndroidJar
- prepareDebugUnitTestDependencies
@@ -30,7 +29,7 @@
-
+
@@ -39,6 +38,12 @@
+
+
+
+
+
+
@@ -46,13 +51,6 @@
-
-
-
-
-
-
-
@@ -60,52 +58,40 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
+
+
+
-
-
+
+
+
+
-
-
-
+
+
+
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/AndroidManifest.xml b/OneSignalSDK/onesignal/src/main/AndroidManifest.xml
index cde5e432fc..98fb629db9 100644
--- a/OneSignalSDK/onesignal/src/main/AndroidManifest.xml
+++ b/OneSignalSDK/onesignal/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
@@ -12,11 +12,13 @@
+ Vibration settings of the device still apply. -->
-
+
+
+
+
+
+
+
diff --git a/OneSignalSDK/onesignal/src/main/java/android/app/OnActivityPausedListener.java b/OneSignalSDK/onesignal/src/main/java/android/app/OnActivityPausedListener.java
new file mode 100644
index 0000000000..72a8e3aeb2
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/android/app/OnActivityPausedListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2011 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
+ */
+
+// Used only for compat with devices older then API 14.
+// Used to compile with, this is from the AOSP on the device so doesn't need to be part of the release jar.
+
+package android.app;
+
+/**
+ * A listener that is called when an Activity is paused. Since this is tracked client side
+ * it should not be trusted to represent the exact current state, but can be used as a hint
+ * for cleanup.
+ *
+ * @hide
+ */
+public interface OnActivityPausedListener {
+ /**
+ * Called when the given activity is paused.
+ */
+ public void onPaused(Activity activity);
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleHandler.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleHandler.java
new file mode 100644
index 0000000000..71df9dd572
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleHandler.java
@@ -0,0 +1,158 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+
+class ActivityLifecycleHandler {
+
+ static Activity curActivity;
+ static FocusHandlerThread focusHandlerThread = new FocusHandlerThread();
+
+ static void onActivityCreated(Activity activity) {
+ curActivity = activity;
+
+ logCurActivity();
+ handleFocus();
+ }
+
+ static void onActivityStarted(Activity activity) {
+ curActivity = activity;
+
+ logCurActivity();
+ handleFocus();
+ }
+
+ static void onActivityResumed(Activity activity) {
+ curActivity = activity;
+
+ logCurActivity();
+ handleFocus();
+ }
+
+ static void onActivityPaused(Activity activity) {
+ if (activity == curActivity) {
+ curActivity = null;
+ handleLostFocus();
+ }
+
+ logCurActivity();
+ }
+
+ static void onActivityStopped(Activity activity) {
+ if (activity == curActivity) {
+ curActivity = null;
+ handleLostFocus();
+ }
+
+ logCurActivity();
+ }
+
+ static void onActivityDestroyed(Activity activity) {
+ if (activity == curActivity) {
+ curActivity = null;
+ handleLostFocus();
+ }
+
+ logCurActivity();
+ }
+
+ static private void logCurActivity() {
+ OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "curActivity is NOW: " + (curActivity != null ? curActivity.getClass().getName() : "null"));
+ }
+
+ static private void handleLostFocus() {
+ focusHandlerThread.runRunnable(new AppFocusRunnable());
+ }
+
+ static private void handleFocus() {
+ if (focusHandlerThread.hasBackgrounded()) {
+ focusHandlerThread.resetBackgroundState();
+ OneSignal.onAppFocus();
+ }
+ else
+ focusHandlerThread.stopScheduledRunnable();
+ }
+
+ static class FocusHandlerThread extends HandlerThread {
+ Handler mHandler = null;
+ private AppFocusRunnable appFocusRunnable;
+
+ FocusHandlerThread() {
+ super("FocusHandlerThread");
+ start();
+ mHandler = new Handler(getLooper());
+ }
+
+ Looper getHandlerLooper() {
+ return mHandler.getLooper();
+ }
+
+ void resetBackgroundState() {
+ if (appFocusRunnable != null)
+ appFocusRunnable.backgrounded = false;
+ }
+
+ void stopScheduledRunnable() {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
+ void runRunnable(AppFocusRunnable runnable) {
+ if (appFocusRunnable != null && appFocusRunnable.backgrounded && !appFocusRunnable.completed)
+ return;
+
+ appFocusRunnable = runnable;
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.postDelayed(runnable, 2000);
+ }
+
+ boolean hasBackgrounded() {
+ if (appFocusRunnable != null)
+ return appFocusRunnable.backgrounded;
+ return false;
+ }
+ }
+
+ static private class AppFocusRunnable implements Runnable {
+ private boolean backgrounded, completed;
+
+ public void run() {
+ if (curActivity != null)
+ return;
+
+ backgrounded = true;
+ OneSignal.onAppLostFocus(false);
+ completed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleListener.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleListener.java
new file mode 100644
index 0000000000..124c72a1c3
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleListener.java
@@ -0,0 +1,70 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+
+@TargetApi(14)
+class ActivityLifecycleListener implements Application.ActivityLifecycleCallbacks {
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ ActivityLifecycleHandler.onActivityCreated(activity);
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+ ActivityLifecycleHandler.onActivityStarted(activity);
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+ ActivityLifecycleHandler.onActivityResumed(activity);
+ }
+
+ @Override
+ public void onActivityPaused(Activity activity) {
+ ActivityLifecycleHandler.onActivityPaused(activity);
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ ActivityLifecycleHandler.onActivityStopped(activity);
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ ActivityLifecycleHandler.onActivityDestroyed(activity);
+ }
+}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleListenerCompat.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleListenerCompat.java
new file mode 100644
index 0000000000..2e339d8c32
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleListenerCompat.java
@@ -0,0 +1,84 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.OnActivityPausedListener;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+// registerActivityLifecycleCallbacks equivalent for devices older then 4.0 (API 14)
+class ActivityLifecycleListenerCompat {
+
+ static void startListener() {
+ try {
+ final Class activityThreadClass = Class.forName("android.app.ActivityThread");
+ final Object activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null);
+
+ Field instrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
+ instrumentationField.setAccessible(true);
+ Instrumentation instrumentation = (Instrumentation)instrumentationField.get(activityThread);
+ final Instrumentation.ActivityMonitor allActivitiesMonitor = instrumentation.addMonitor((String)null, null, false);
+
+ startMonitorThread(activityThreadClass, activityThread, allActivitiesMonitor);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ private static void startMonitorThread(final Class activityThreadClass, final Object activityThread, final Instrumentation.ActivityMonitor allActivitiesMonitor) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ OnActivityPausedListener pausedListener = new OnActivityPausedListener() {
+ @Override
+ public void onPaused(Activity activity) {
+ ActivityLifecycleHandler.onActivityPaused(activity);
+ }
+ };
+ Method registerOnActivityPausedListener = activityThreadClass.getMethod("registerOnActivityPausedListener", Activity.class, OnActivityPausedListener.class);
+
+ while (true) {
+ // Wait for new activity events, does not fire for pauses through.
+ Activity currentActivity = allActivitiesMonitor.waitForActivity();
+
+ if (!currentActivity.isFinishing()) {
+ ActivityLifecycleHandler.onActivityResumed(currentActivity);
+ registerOnActivityPausedListener.invoke(activityThread, currentActivity, pausedListener);
+ }
+ }
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ }).start();
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderFallback.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderFallback.java
index 30daaa37c9..c9e163dffb 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderFallback.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderFallback.java
@@ -32,6 +32,7 @@
import android.content.Context;
import android.net.wifi.WifiManager;
+import android.os.Build;
import android.provider.Settings;
import android.telephony.TelephonyManager;
@@ -80,7 +81,8 @@ private String getAndroidId(Context appContext) {
// Requires android.permission.ACCESS_WIFI_STATE permission
private String getWifiMac(Context appContext) {
try {
- return ((WifiManager) appContext.getSystemService(Context.WIFI_SERVICE)).getConnectionInfo().getMacAddress();
+ if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
+ return ((WifiManager) appContext.getSystemService(Context.WIFI_SERVICE)).getConnectionInfo().getMacAddress();
} catch (RuntimeException e) {}
return null;
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderGPS.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderGPS.java
index d677c17720..8520311884 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderGPS.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/AdvertisingIdProviderGPS.java
@@ -28,7 +28,6 @@
package com.onesignal;
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
-import com.onesignal.OneSignal;
import android.content.Context;
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java
index 5ef7e40e18..1b8539fc0f 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java
@@ -44,13 +44,13 @@
import android.app.AlertDialog;
import android.app.Notification;
import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@@ -70,22 +70,23 @@ class GenerateNotification {
private static Context currentContext = null;
private static String packageName = null;
private static Resources contextResources = null;
- private static Class> notificationOpenedActivityClass;
+ private static Class> notificationOpenedClass;
+ private static boolean openerIsBroadcast;
static void setStatics(Context inContext) {
currentContext = inContext;
packageName = currentContext.getPackageName();
contextResources = currentContext.getResources();
- Intent intent = new Intent(currentContext, com.onesignal.NotificationOpenedActivity.class);
- intent.setPackage(currentContext.getPackageName());
PackageManager packageManager = currentContext.getPackageManager();
- List resolveInfo = packageManager.queryIntentActivities(intent, 0);
-
- if (resolveInfo.size() > 0)
- notificationOpenedActivityClass = com.onesignal.NotificationOpenedActivity.class;
+ Intent intent = new Intent(currentContext, NotificationOpenedReceiver.class);
+ intent.setPackage(currentContext.getPackageName());
+ if (packageManager.queryBroadcastReceivers(intent, 0).size() > 0) {
+ openerIsBroadcast = true;
+ notificationOpenedClass = NotificationOpenedReceiver.class;
+ }
else
- notificationOpenedActivityClass = com.gamethrive.NotificationOpenedActivity.class;
+ notificationOpenedClass = NotificationOpenedActivity.class;
}
public static int fromBundle(Context inContext, Bundle bundle, boolean showAsAlert) {
@@ -93,19 +94,19 @@ public static int fromBundle(Context inContext, Bundle bundle, boolean showAsAle
JSONObject jsonBundle = NotificationBundleProcessor.bundleAsJSONObject(bundle);
- if (showAsAlert)
- return showNotificationAsAlert(jsonBundle, OneSignal.appContext);
+ if (showAsAlert && ActivityLifecycleHandler.curActivity != null)
+ return showNotificationAsAlert(jsonBundle, ActivityLifecycleHandler.curActivity);
return showNotification(jsonBundle);
}
- private static int showNotificationAsAlert(final JSONObject gcmJson, final Context context) {
+ private static int showNotificationAsAlert(final JSONObject gcmJson, final Activity activity) {
final int aNotificationId = new Random().nextInt();
- ((Activity) context).runOnUiThread(new Runnable() {
+ activity.runOnUiThread(new Runnable() {
@Override
public void run() {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(getTitle(gcmJson));
try {
builder.setMessage(gcmJson.getString("alert"));
@@ -120,6 +121,7 @@ public void run() {
Intent buttonIntent = getNewBaseIntent(aNotificationId);
buttonIntent.putExtra("action_button", true);
+ buttonIntent.putExtra("from_alert", true);
buttonIntent.putExtra("onesignal_data", gcmJson.toString());
try {
if (gcmJson.has("grp"))
@@ -143,10 +145,11 @@ public void onClick(DialogInterface dialog, int which) {
finalButtonIntent.putExtra("onesignal_data", newJsonData.toString());
- NotificationOpenedProcessor.processIntent(context, finalButtonIntent);
- } catch (Throwable t) {}
+ NotificationOpenedProcessor.processIntent(activity, finalButtonIntent);
+ } catch (Throwable t) {
+ }
} else // No action buttons, close button simply pressed.
- NotificationOpenedProcessor.processIntent(context, finalButtonIntent);
+ NotificationOpenedProcessor.processIntent(activity, finalButtonIntent);
}
};
@@ -154,7 +157,7 @@ public void onClick(DialogInterface dialog, int which) {
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
- NotificationOpenedProcessor.processIntent(context, finalButtonIntent);
+ NotificationOpenedProcessor.processIntent(activity, finalButtonIntent);
}
});
@@ -186,17 +189,29 @@ private static CharSequence getTitle(JSONObject gcmBundle) {
return currentContext.getPackageManager().getApplicationLabel(currentContext.getApplicationInfo());
}
+ private static PendingIntent getNewActionPendingIntent(int requestCode, Intent intent) {
+ if (openerIsBroadcast)
+ return PendingIntent.getBroadcast(currentContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getActivity(currentContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
private static Intent getNewBaseIntent(int notificationId) {
- return new Intent(currentContext, notificationOpenedActivityClass)
- .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP)
- .putExtra("notificationId", notificationId);
+ Intent intent = new Intent(currentContext, notificationOpenedClass)
+ .putExtra("notificationId", notificationId);
+
+ if (openerIsBroadcast)
+ return intent;
+ return intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
}
private static Intent getNewBaseDeleteIntent(int notificationId) {
- return new Intent(currentContext, notificationOpenedActivityClass)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION)
- .putExtra("notificationId", notificationId)
- .putExtra("dismissed", true);
+ Intent intent = new Intent(currentContext, notificationOpenedClass)
+ .putExtra("notificationId", notificationId)
+ .putExtra("dismissed", true);
+
+ if (openerIsBroadcast)
+ return intent;
+ return intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
}
private static NotificationCompat.Builder getBaseNotificationCompatBuilder(JSONObject gcmBundle, boolean notify) {
@@ -212,11 +227,6 @@ private static NotificationCompat.Builder getBaseNotificationCompatBuilder(JSONO
message = gcmBundle.getString("alert");
} catch (Throwable t) {}
- String group = null;
- try {
- group = gcmBundle.getString("grp");
- } catch (Throwable t) {}
-
NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(currentContext)
.setAutoCancel(true)
.setSmallIcon(notificationIcon) // Small Icon required or notification doesn't display
@@ -293,18 +303,18 @@ private static int showNotification(JSONObject gcmBundle) {
addNotificationActionButtons(gcmBundle, notifBuilder, notificationId, null);
if (group != null) {
- PendingIntent contentIntent = PendingIntent.getActivity(currentContext, random.nextInt(), getNewBaseIntent(notificationId).putExtra("onesignal_data", gcmBundle.toString()).putExtra("grp", group), PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent contentIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseIntent(notificationId).putExtra("onesignal_data", gcmBundle.toString()).putExtra("grp", group));
notifBuilder.setContentIntent(contentIntent);
- PendingIntent deleteIntent = PendingIntent.getActivity(currentContext, random.nextInt(), getNewBaseDeleteIntent(notificationId).putExtra("grp", group), PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent deleteIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseDeleteIntent(notificationId).putExtra("grp", group));
notifBuilder.setDeleteIntent(deleteIntent);
notifBuilder.setGroup(group);
createSummaryNotification(gcmBundle);
}
else {
- PendingIntent contentIntent = PendingIntent.getActivity(currentContext, random.nextInt(), getNewBaseIntent(notificationId).putExtra("onesignal_data", gcmBundle.toString()), PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent contentIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseIntent(notificationId).putExtra("onesignal_data", gcmBundle.toString()));
notifBuilder.setContentIntent(contentIntent);
- PendingIntent deleteIntent = PendingIntent.getActivity(currentContext, random.nextInt(), getNewBaseDeleteIntent(notificationId), PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent deleteIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseDeleteIntent(notificationId));
notifBuilder.setDeleteIntent(deleteIntent);
}
@@ -332,7 +342,7 @@ static void createSummaryNotification(Context inContext, boolean updateSummary,
} catch (Throwable t) {}
Random random = new Random();
- PendingIntent summaryDeleteIntent = PendingIntent.getActivity(currentContext, random.nextInt(), getNewBaseDeleteIntent(0).putExtra("summary", group), PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent summaryDeleteIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseDeleteIntent(0).putExtra("summary", group));
OneSignalDbHelper dbHelper = new OneSignalDbHelper(currentContext);
SQLiteDatabase writableDb = dbHelper.getWritableDatabase();
@@ -422,7 +432,7 @@ static void createSummaryNotification(Context inContext, boolean updateSummary,
.putExtra("summary", group)
.putExtra("onesignal_data", summaryDataBundle.toString());
- PendingIntent summaryContentIntent = PendingIntent.getActivity(currentContext, random.nextInt(), summaryIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent summaryContentIntent = getNewActionPendingIntent(random.nextInt(), summaryIntent);
NotificationCompat.Builder summeryBuilder = getBaseNotificationCompatBuilder(gcmBundle, !updateSummary);
@@ -481,7 +491,7 @@ static void createSummaryNotification(Context inContext, boolean updateSummary,
NotificationCompat.Builder notifBuilder = getBaseNotificationCompatBuilder(gcmBundle, !updateSummary);
- PendingIntent summaryContentIntent = PendingIntent.getActivity(currentContext, random.nextInt(), getNewBaseIntent(summaryNotificationId).putExtra("onesignal_data", gcmBundle.toString()).putExtra("summary", group), PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent summaryContentIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseIntent(summaryNotificationId).putExtra("onesignal_data", gcmBundle.toString()).putExtra("summary", group));
addNotificationActionButtons(gcmBundle, notifBuilder, summaryNotificationId, group);
notifBuilder.setContentIntent(summaryContentIntent)
@@ -700,7 +710,7 @@ private static void addNotificationActionButtons(JSONObject gcmBundle, Notificat
else if (gcmBundle.has("grp"))
buttonIntent.putExtra("grp", gcmBundle.getString("grp"));
- PendingIntent buttonPIntent = PendingIntent.getActivity(currentContext, notificationId, buttonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent buttonPIntent = getNewActionPendingIntent(notificationId, buttonIntent);
int buttonIcon = 0;
if (button.has("icon"))
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/LocationGMS.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/LocationGMS.java
new file mode 100644
index 0000000000..1e13c630c1
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/LocationGMS.java
@@ -0,0 +1,115 @@
+package com.onesignal;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.location.LocationServices;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Arrays;
+import java.util.List;
+
+class LocationGMS {
+ private static GoogleApiClient mGoogleApiClient;
+ static String requestPermission;
+
+ interface LocationHandler {
+ void complete(Double lat, Double log);
+ }
+
+ private static LocationHandler locationHandler;
+
+ static void getLocation(Context context, boolean promptLocation, LocationHandler handler) {
+ locationHandler = handler;
+ int locationCoarsePermission = PackageManager.PERMISSION_DENIED;
+
+ int locationFinePermission = ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_FINE_LOCATION");
+ if (locationFinePermission == PackageManager.PERMISSION_DENIED)
+ locationCoarsePermission = ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_COARSE_LOCATION");
+
+ if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ if (locationFinePermission != PackageManager.PERMISSION_GRANTED && locationCoarsePermission != PackageManager.PERMISSION_GRANTED) {
+ handler.complete(null, null);
+ return;
+ }
+
+ startGetLocation();
+ }
+ else { // Android 6.0+
+ if (locationFinePermission != PackageManager.PERMISSION_GRANTED) {
+ try {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
+ List permissionList = Arrays.asList(packageInfo.requestedPermissions);
+ if (permissionList.contains("android.permission.ACCESS_FINE_LOCATION"))
+ requestPermission = "android.permission.ACCESS_FINE_LOCATION";
+ else if (permissionList.contains("android.permission.ACCESS_COARSE_LOCATION")) {
+ if (locationCoarsePermission != PackageManager.PERMISSION_GRANTED)
+ requestPermission = "android.permission.ACCESS_COARSE_LOCATION";
+ }
+
+ if (requestPermission != null && promptLocation)
+ ActivityLifecycleHandler.curActivity.startActivity(new Intent(context, PermissionsActivity.class));
+ else if (locationCoarsePermission == PackageManager.PERMISSION_GRANTED)
+ startGetLocation();
+ else
+ fireFailedComplete();
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ else
+ startGetLocation();
+ }
+ }
+
+ static void startGetLocation() {
+ GoogleApiClientListener googleApiClientListener = new GoogleApiClientListener();
+ mGoogleApiClient = new GoogleApiClient.Builder(OneSignal.appContext)
+ .addApi(LocationServices.API)
+ .addConnectionCallbacks(googleApiClientListener)
+ .addOnConnectionFailedListener(googleApiClientListener)
+ .build();
+ mGoogleApiClient.connect();
+ }
+
+ static void fireFailedComplete() {
+ locationHandler.complete(null, null);
+ if (mGoogleApiClient != null)
+ mGoogleApiClient.disconnect();
+ }
+
+ private static class GoogleApiClientListener implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
+ @Override
+ public void onConnected(Bundle bundle) {
+ Location location = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
+ // Coarse always gives out 14 digits and has an accuracy 2000. Always rounding to 7 as this is what fine returns.
+ if (location != null)
+ locationHandler.complete(new BigDecimal(location.getLatitude()).setScale(7, RoundingMode.HALF_UP).doubleValue(), new BigDecimal(location.getLongitude()).setScale(7, RoundingMode.HALF_UP).doubleValue());
+ else
+ locationHandler.complete(null, null);
+
+ mGoogleApiClient.disconnect();
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ fireFailedComplete();
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+ fireFailedComplete();
+ }
+ }
+}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java
index 9df1ebe1bc..4dca6f11a4 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java
@@ -27,7 +27,6 @@
package com.onesignal;
-import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -43,25 +42,25 @@
public class NotificationOpenedProcessor {
- private static Context activity;
+ private static Context context;
private static Intent intent;
- public static void processFromActivity(Activity inActivity, Intent inIntent) {
+ public static void processFromActivity(Context inContext, Intent inIntent) {
if (inIntent.getBooleanExtra("action_button", false)) // Pressed an action button, need to clear the notification manually
- NotificationManagerCompat.from(inActivity).cancel(inIntent.getIntExtra("notificationId", 0));
+ NotificationManagerCompat.from(inContext).cancel(inIntent.getIntExtra("notificationId", 0));
- processIntent(inActivity, inIntent);
+ processIntent(inContext, inIntent);
}
- static void processIntent(Context inActivity, Intent inIntent) {
- activity = inActivity;
+ static void processIntent(Context incContext, Intent inIntent) {
+ context = incContext;
intent = inIntent;
String summaryGroup = intent.getStringExtra("summary");
boolean dismissed = intent.getBooleanExtra("dismissed", false);
- OneSignalDbHelper dbHelper = new OneSignalDbHelper(activity);
+ OneSignalDbHelper dbHelper = new OneSignalDbHelper(context);
SQLiteDatabase writableDb = dbHelper.getWritableDatabase();
JSONArray dataArray = null;
@@ -86,7 +85,7 @@ static void processIntent(Context inActivity, Intent inIntent) {
writableDb.close();
if (!dismissed)
- OneSignal.handleNotificationOpened(activity, dataArray);
+ OneSignal.handleNotificationOpened(context, dataArray, inIntent.getBooleanExtra("from_alert", false));
}
private static void addChildNotifications(JSONArray dataArray, String summaryGroup, SQLiteDatabase writableDb) {
@@ -149,7 +148,7 @@ private static void updateSummaryNotification(SQLiteDatabase writableDb) {
writableDb.update(NotificationTable.TABLE_NAME, newContentValuesWithConsumed(), NotificationTable.COLUMN_NAME_GROUP_ID + " = ?", new String[] {grpId });
else {
try {
- GenerateNotification.createSummaryNotification(activity, true, new JSONObject("{\"grp\": \"" + grpId + "\"}"));
+ GenerateNotification.createSummaryNotification(context, true, new JSONObject("{\"grp\": \"" + grpId + "\"}"));
} catch (JSONException e) {}
}
}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedReceiver.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedReceiver.java
new file mode 100644
index 0000000000..caa1294d28
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedReceiver.java
@@ -0,0 +1,13 @@
+package com.onesignal;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class NotificationOpenedReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NotificationOpenedProcessor.processFromActivity(context, intent);
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java
new file mode 100644
index 0000000000..b4736d2a93
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java
@@ -0,0 +1,64 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+
+class OSUtils {
+ int getDeviceType() {
+ try {
+ Class.forName("com.amazon.device.messaging.ADM");
+ return 2;
+ } catch (ClassNotFoundException e) {
+ return 1;
+ }
+ }
+
+ Integer getNetType () {
+ ConnectivityManager cm = (ConnectivityManager) OneSignal.appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo netInfo = cm.getActiveNetworkInfo();
+
+ if (netInfo != null) {
+ int networkType = netInfo.getType();
+ if (networkType == ConnectivityManager.TYPE_WIFI || networkType == ConnectivityManager.TYPE_ETHERNET)
+ return 0;
+ return 1;
+ }
+
+ return null;
+ }
+
+ String getCarrierName() {
+ TelephonyManager manager = (TelephonyManager)OneSignal.appContext.getSystemService(Context.TELEPHONY_SERVICE);
+ String carrierName = manager.getNetworkOperatorName();
+ return "".equals(carrierName) ? null : carrierName;
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java
index 4ea8f73eec..aa57d4ce6f 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java
@@ -29,27 +29,24 @@
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.UUID;
-import org.apache.http.Header;
import org.json.*;
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -59,27 +56,28 @@
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
-import android.util.TypedValue;
-import com.loopj.android.http.*;
import com.stericson.RootTools.internal.RootToolsInternalMethods;
import com.onesignal.OneSignalDbContract.NotificationTable;
public class OneSignal {
-
+
public enum LOG_LEVEL {
NONE, FATAL, ERROR, WARN, INFO, DEBUG, VERBOSE
}
+ static final long MIN_ON_FOCUS_TIME = 60;
+
+ // TODO: The interface and the code using it may not be needed since the Activity Context is being dereferenced correctly.
+ // Check with Corona, Cordova, and see if it fixes the Marmalade bug around this.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TiedToCurrentActivity {}
@@ -112,22 +110,45 @@ public interface PostNotificationResponseHandler {
void onFailure(JSONObject response);
}
+ public static class Builder {
+ Context mContext;
+ NotificationOpenedHandler mNotificationOpenedHandler;
+ boolean mPromptLocation;
+
+ private Builder() {}
+
+ private Builder(Context context) {
+ mContext = context;
+ }
+
+ public Builder setNotificationOpenedHandler(NotificationOpenedHandler handler) {
+ mNotificationOpenedHandler = handler;
+ return this;
+ }
+
+ public Builder setAutoPromptLocation(boolean enable) {
+ mPromptLocation = enable;
+ return this;
+ }
+
+ public void init() {
+ OneSignal.init(this);
+ }
+ }
+
/**
* Tag used on log messages.
*/
static final String TAG = "OneSignal";
- private static String appId;
- static Activity appContext;
+ static String appId;
+ static Context appContext;
private static LOG_LEVEL visualLogLevel = LOG_LEVEL.NONE;
private static LOG_LEVEL logCatLevel = LOG_LEVEL.WARN;
- private static String registrationId, userId = null;
- private static JSONObject pendingTags;
- private static int savedSubscription, syncedSubscription;
- private static int currentSubscription = 1;
- private static final int UNSUBSCRIBE_VALUE = -2;
+ private static String userId = null;
+ private static int subscribableStatus = 1;
private static NotificationOpenedHandler notificationOpenedHandler;
@@ -136,14 +157,13 @@ public interface PostNotificationResponseHandler {
private static IdsAvailableHandler idsAvailableHandler;
- private static long lastTrackedTime, unSentActiveTime = -1;
+ private static long lastTrackedTime = 1, unSentActiveTime = -1;
private static TrackGooglePurchase trackGooglePurchase;
private static TrackAmazonPurchase trackAmazonPurchase;
- public static final String VERSION = "011007";
+ public static final String VERSION = "020000";
- private static PushRegistrator pushRegistrator;
private static AdvertisingIdentifierProvider mainAdIdProvider = new AdvertisingIdProviderGPS();
private static int deviceType;
@@ -152,26 +172,58 @@ public interface PostNotificationResponseHandler {
private static JSONObject nextInitAdditionalDataJSON = null;
private static String nextInitMessage = null;
- public static void init(Activity context, String googleProjectNumber, String oneSignalAppId) {
- init(context, googleProjectNumber, oneSignalAppId, null);
+ private static OSUtils osUtils;
+
+ private static boolean ranSessionInitThread;
+
+ private static String lastRegistrationId;
+ private static boolean registerForPushFired, locationFired;
+ private static Double lastLocLat, lastLocLong;
+ private static OneSignal.Builder mInitBuilder;
+
+ static Collection unprocessedOpenedNotifis = new ArrayList();
+
+ public static OneSignal.Builder startInit(Context context) {
+ return new OneSignal.Builder(context);
}
- public static void init(Activity context, String googleProjectNumber, String oneSignalAppId, NotificationOpenedHandler inNotificationOpenedHandler) {
+ private static void init(OneSignal.Builder inBuilder) {
+ mInitBuilder = inBuilder;
+
+ Context context = mInitBuilder.mContext;
+ mInitBuilder.mContext = null; // Clear to prevent leaks.
try {
- Class.forName("com.amazon.device.messaging.ADM");
+ ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
+ Bundle bundle = ai.metaData;
+ OneSignal.init(context, bundle.getString("onesignal_google_project_number").substring(4), bundle.getString("onesignal_app_id"), mInitBuilder.mNotificationOpenedHandler);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ public static void init(Context context, String googleProjectNumber, String oneSignalAppId) {
+ init(context, googleProjectNumber, oneSignalAppId, null);
+ }
+
+ public static void init(Context context, String googleProjectNumber, String oneSignalAppId, NotificationOpenedHandler inNotificationOpenedHandler) {
+ if (mInitBuilder == null)
+ mInitBuilder = new OneSignal.Builder();
+
+ osUtils = new OSUtils();
+
+ deviceType = osUtils.getDeviceType();
+ PushRegistrator pushRegistrator;
+ if (deviceType == 2)
pushRegistrator = new PushRegistratorADM();
- deviceType = 2;
- } catch (ClassNotFoundException e) {
+ else
pushRegistrator = new PushRegistratorGPS();
- deviceType = 1;
- }
// START: Init validation
try {
UUID.fromString(oneSignalAppId);
} catch (Throwable t) {
- Log(LOG_LEVEL.FATAL, "OneSignal AppId format is invalid.\nExample: 'b2f7f966-d8cc-11e4-bed1-df8f05be55ba'\n", t, context);
+ Log(LOG_LEVEL.FATAL, "OneSignal AppId format is invalid.\nExample: 'b2f7f966-d8cc-11e4-bed1-df8f05be55ba'\n", t);
return;
}
@@ -184,15 +236,15 @@ public static void init(Activity context, String googleProjectNumber, String one
if (googleProjectNumber.length() < 8 || googleProjectNumber.length() > 16)
throw new IllegalArgumentException("Google Project number (Sender_ID) should be a 10 to 14 digit number in length.");
} catch (Throwable t) {
- Log(LOG_LEVEL.FATAL, "Google Project number (Sender_ID) format is invalid. Please use the 10 to 14 digit number found in the Google Developer Console for your project.\nExample: '703322744261'\n", t, context);
- currentSubscription = -6;
+ Log(LOG_LEVEL.FATAL, "Google Project number (Sender_ID) format is invalid. Please use the 10 to 14 digit number found in the Google Developer Console for your project.\nExample: '703322744261'\n", t);
+ subscribableStatus = -6;
}
try {
Class.forName("com.google.android.gms.gcm.GoogleCloudMessaging");
} catch (ClassNotFoundException e) {
- Log(LOG_LEVEL.FATAL, "The GCM Google Play services client library was not found. Please make sure to include it in your project.", e, context);
- currentSubscription = -4;
+ Log(LOG_LEVEL.FATAL, "The GCM Google Play services client library was not found. Please make sure to include it in your project.", e);
+ subscribableStatus = -4;
}
}
@@ -201,22 +253,20 @@ public static void init(Activity context, String googleProjectNumber, String one
try {
Class.forName("android.support.v4.content.WakefulBroadcastReceiver");
} catch (ClassNotFoundException e) {
- Log(LOG_LEVEL.FATAL, "The included Android Support Library v4 is to old. Please update your project's android-support-v4.jar to the latest revision.", e, context);
- currentSubscription = -5;
+ Log(LOG_LEVEL.FATAL, "The included Android Support Library v4 is to old. Please update your project's android-support-v4.jar to the latest revision.", e);
+ subscribableStatus = -5;
}
} catch (ClassNotFoundException e) {
- Log(LOG_LEVEL.FATAL, "Could not find the Android Support Library v4. Please make sure android-support-v4.jar has been correctly added to your project.", e, context);
- currentSubscription = -3;
+ Log(LOG_LEVEL.FATAL, "Could not find the Android Support Library v4. Please make sure android-support-v4.jar has been correctly added to your project.", e);
+ subscribableStatus = -3;
}
if (initDone) {
if (context != null)
- appContext = context;
+ appContext = context.getApplicationContext();
if (inNotificationOpenedHandler != null)
notificationOpenedHandler = inNotificationOpenedHandler;
- onResumed();
-
if (nextInitMessage != null && notificationOpenedHandler != null) {
fireNotificationOpenedHandler(nextInitMessage, nextInitAdditionalDataJSON, false);
@@ -229,16 +279,21 @@ public static void init(Activity context, String googleProjectNumber, String one
// END: Init validation
- savedSubscription = getSubscription(context);
- syncedSubscription = getSyncedSubscription(context);
- if (currentSubscription > 0 && savedSubscription > -3)
- currentSubscription = savedSubscription;
-
appId = oneSignalAppId;
- appContext = context;
+ appContext = context.getApplicationContext();
+ if (context instanceof Activity)
+ ActivityLifecycleHandler.curActivity = (Activity)context;
notificationOpenedHandler = inNotificationOpenedHandler;
lastTrackedTime = SystemClock.elapsedRealtime();
+ OneSignalStateSynchronizer.initUserState(appContext);
+ appContext.startService(new Intent(appContext, SyncService.class));
+
+ if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2)
+ ((Application)appContext).registerActivityLifecycleCallbacks(new ActivityLifecycleListener());
+ else
+ ActivityLifecycleListenerCompat.startListener();
+
try {
Class.forName("com.amazon.device.iap.PurchasingListener");
trackAmazonPurchase = new TrackAmazonPurchase(appContext);
@@ -249,59 +304,51 @@ public static void init(Activity context, String googleProjectNumber, String one
if (oldAppId != null) {
if (!oldAppId.equals(appId)) {
Log(LOG_LEVEL.DEBUG, "APP ID changed, clearing user id as it is no longer valid.");
- saveUserId(null);
SaveAppId(appId);
+ OneSignalStateSynchronizer.resetCurrentState();
}
}
else
SaveAppId(appId);
pushRegistrator.registerForPush(appContext, googleProjectNumber, new PushRegistrator.RegisteredHandler() {
- private boolean firstRun = true;
@Override
public void complete(String id) {
- if (firstRun)
- registerUser(id);
- else
- updateRegistrationId(id);
- firstRun = false;
+ lastRegistrationId = id;
+ registerForPushFired = true;
+ registerUser();
}
});
- // Called from tapping on a Notification from the status bar when the activity is completely dead and not open in any state.
- if (appContext.getIntent() != null) {
- String oneSignalDataJsonString = appContext.getIntent().getStringExtra("onesignal_data");
- if (oneSignalDataJsonString != null) {
- try {
- JSONArray dataArray = new JSONArray(oneSignalDataJsonString);
- openWebURLFromNotification(dataArray);
- runNotificationOpenedCallback(dataArray, false);
- } catch (JSONException e) {
- Log(LOG_LEVEL.ERROR, "Failed to process onesignal_data intent on app cold start.");
- }
+ LocationGMS.getLocation(appContext, mInitBuilder.mPromptLocation, new LocationGMS.LocationHandler() {
+ @Override
+ public void complete(Double lat, Double log) {
+ lastLocLat = lat; lastLocLong = log;
+ locationFired = true;
+ registerUser();
}
- }
+ });
+
+ fireCallbackForOpenedNotifications();
if (TrackGooglePurchase.CanTrack(appContext))
trackGooglePurchase = new TrackGooglePurchase(appContext);
initDone = true;
+ }
+
+ private static void fireCallbackForOpenedNotifications() {
+ for(JSONArray dataArray : unprocessedOpenedNotifis)
+ runNotificationOpenedCallback(dataArray, false);
- // In the future on Android 4.0 (API 14)+ devices use registerActivityLifecycleCallbacks
- // instead of requiring developers to call onPause and onResume in each activity.
- // Might be able to use registerOnActivityPausedListener in Android 2.3.3 (API 10) to 3.2 (API 13) for backwards compatibility
+ unprocessedOpenedNotifis.clear();
}
- private static void updateRegistrationId(String id) {
- String orgRegId = GetRegistrationId();
- if (id != null && !id.equals(orgRegId)) {
- SaveRegistrationId(id);
+ private static void updateRegistrationId() {
+ String orgRegId = OneSignalStateSynchronizer.getRegistrationId();
+ if (lastRegistrationId != null && !lastRegistrationId.equals(orgRegId)) {
+ OneSignalStateSynchronizer.updateIdentifier(lastRegistrationId);
fireIdsAvailableCallback();
- try {
- JSONObject jsonBody = playerUpdateBaseJSON();
- jsonBody.put("identifier", registrationId);
- postPlayerUpdate(jsonBody);
- } catch (JSONException e) {}
}
}
@@ -341,14 +388,10 @@ private static boolean atLogLevel(LOG_LEVEL level) {
}
static void Log(LOG_LEVEL level, String message) {
- Log(level, message, null, appContext);
+ Log(level, message, null);
}
static void Log(final LOG_LEVEL level, String message, Throwable throwable) {
- Log(level, message, throwable, appContext);
- }
-
- private static void Log(final LOG_LEVEL level, String message, Throwable throwable, final Activity context) {
if (level.compareTo(logCatLevel) < 1) {
if (level == LOG_LEVEL.VERBOSE)
Log.v(TAG, message, throwable);
@@ -362,7 +405,7 @@ else if (level == LOG_LEVEL.ERROR || level == LOG_LEVEL.FATAL)
Log.e(TAG, message, throwable);
}
- if (context != null && level.compareTo(visualLogLevel) < 1) {
+ if (level.compareTo(visualLogLevel) < 1 && ActivityLifecycleHandler.curActivity != null) {
try {
String fullMessage = message + "\n";
if (throwable != null) {
@@ -374,14 +417,14 @@ else if (level == LOG_LEVEL.ERROR || level == LOG_LEVEL.FATAL)
}
final String finalFullMessage = fullMessage;
-
- context.runOnUiThread(new Runnable() {
+ runOnUiThread(new Runnable() {
@Override
public void run() {
- new AlertDialog.Builder(context)
- .setTitle(level.toString())
- .setMessage(finalFullMessage)
- .show();
+ if (ActivityLifecycleHandler.curActivity != null)
+ new AlertDialog.Builder(ActivityLifecycleHandler.curActivity)
+ .setTitle(level.toString())
+ .setMessage(finalFullMessage)
+ .show();
}
});
} catch(Throwable t) {
@@ -390,24 +433,38 @@ public void run() {
}
}
- private static void logHttpError(String errorString, int statusCode, Throwable throwable, JSONObject errorResponse) {
+ private static void logHttpError(String errorString, int statusCode, Throwable throwable, String errorResponse) {
String jsonError = "";
if (errorResponse != null && atLogLevel(LOG_LEVEL.INFO))
- jsonError = "\n" + errorResponse.toString() + "\n";
+ jsonError = "\n" + errorResponse + "\n";
Log(LOG_LEVEL.WARN, "HTTP code: " + statusCode + " " + errorString + jsonError, throwable);
}
+ /**
+ * Now automatically tracked, remove from your Activities.
+ *
+ * @deprecated Automatically tracked.
+ * @Deprecated Automatically tracked.
+ */
public static void onPaused() {
+ Log(LOG_LEVEL.INFO, "Deprecated! onPaused is now tracked automatically, please remove calls to OneSignal.onPaused() and OneSignal.onResume().");
+ }
+
+ static void onAppLostFocus(boolean onlySave) {
foreground = false;
+ if (!initDone) return;
+
if (trackAmazonPurchase != null)
trackAmazonPurchase.checkListener();
+ if (lastTrackedTime == -1)
+ return;
+
long time_elapsed = (long) (((SystemClock.elapsedRealtime() - lastTrackedTime) / 1000d) + 0.5d);
lastTrackedTime = SystemClock.elapsedRealtime();
if (time_elapsed < 0 || time_elapsed > 604800)
return;
-
if (appContext == null) {
Log(LOG_LEVEL.ERROR, "Android Context not found, please call OneSignal.init when your app starts.");
return;
@@ -416,14 +473,15 @@ public static void onPaused() {
long unSentActiveTime = GetUnsentActiveTime();
long totalTimeActive = unSentActiveTime + time_elapsed;
- if (totalTimeActive < 30) {
+ if (onlySave || totalTimeActive < MIN_ON_FOCUS_TIME || getUserId() == null) {
SaveUnsentActiveTime(totalTimeActive);
return;
}
- if (getUserId() == null)
- return;
+ sendOnFocus(totalTimeActive, true);
+ }
+ static void sendOnFocus(long totalTimeActive, boolean synchronous) {
JSONObject jsonBody = new JSONObject();
try {
jsonBody.put("app_id", appId);
@@ -431,20 +489,39 @@ public static void onPaused() {
jsonBody.put("active_time", totalTimeActive);
addNetType(jsonBody);
- OneSignalRestClient.post(appContext, "players/" + getUserId() + "/on_focus", jsonBody, new JsonHttpResponseHandler() {
+ String url = "players/" + getUserId() + "/on_focus";
+ OneSignalRestClient.ResponseHandler responseHandler = new OneSignalRestClient.ResponseHandler() {
@Override
- public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
- logHttpError("sending on_focus Failed", statusCode, throwable, errorResponse);
+ void onFailure(int statusCode, String response, Throwable throwable) {
+ logHttpError("sending on_focus Failed", statusCode, throwable, response);
}
- });
- SaveUnsentActiveTime(0);
+ @Override
+ void onSuccess(String response) {
+ SaveUnsentActiveTime(0);
+ }
+ };
+
+ if (synchronous)
+ OneSignalRestClient.postSync(url, jsonBody, responseHandler);
+ else
+ OneSignalRestClient.post(url, jsonBody, responseHandler);
} catch (Throwable t) {
Log(LOG_LEVEL.ERROR, "Generating on_focus:JSON Failed.", t);
}
}
+ /**
+ * Now automatically tracked, remove from your Activities.
+ *
+ * @deprecated Automatically tracked.
+ * @Deprecated Automatically tracked.
+ */
public static void onResumed() {
+ Log(LOG_LEVEL.INFO, "Deprecated! onResumed is now tracked automatically, please remove calls to OneSignal.onPaused() and OneSignal.onResume().");
+ }
+
+ static void onAppFocus() {
foreground = true;
lastTrackedTime = SystemClock.elapsedRealtime();
@@ -458,165 +535,78 @@ static boolean isForeground() {
private static void addNetType(JSONObject jsonObj) {
try {
- ConnectivityManager cm = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo netInfo = cm.getActiveNetworkInfo();
-
- int networkType = netInfo.getType();
- int netType = 1;
- if (networkType == ConnectivityManager.TYPE_WIFI || networkType == ConnectivityManager.TYPE_ETHERNET)
- netType = 0;
- jsonObj.put("net_type", netType);
+ jsonObj.put("net_type", osUtils.getNetType());
} catch (Throwable t) {}
}
private static int getTimeZoneOffset() {
- TimeZone timzone = Calendar.getInstance().getTimeZone();
- int offset = timzone.getRawOffset();
+ TimeZone timezone = Calendar.getInstance().getTimeZone();
+ int offset = timezone.getRawOffset();
- if (timzone.inDaylightTime(new Date()))
- offset = offset + timzone.getDSTSavings();
+ if (timezone.inDaylightTime(new Date()))
+ offset = offset + timezone.getDSTSavings();
return offset / 1000;
}
- private static void registerUser(String id) {
- if (id != null)
- SaveRegistrationId(id);
-
- // Must run in its own thread due to the use of getAdvertisingId
- new Thread(new Runnable() {
- public void run() {
- try {
- String packageName = appContext.getPackageName();
- PackageManager packageManager = appContext.getPackageManager();
-
- final JSONObject jsonBody = new JSONObject();
- jsonBody.put("app_id", appId);
- if (registrationId != null)
- jsonBody.put("identifier", registrationId);
-
- String adId = mainAdIdProvider.getIdentifier(appContext);
- // "... must use the advertising ID (when available on a device) in lieu of any other device identifiers ..."
- // https://play.google.com/about/developer-content-policy.html
- if (adId != null)
- jsonBody.put("ad_id", adId);
- else {
- adId = new AdvertisingIdProviderFallback().getIdentifier(appContext);
- if (adId != null)
- jsonBody.put("ad_id", adId);
- }
-
- jsonBody.put("device_os", Build.VERSION.RELEASE);
- jsonBody.put("timezone", getTimeZoneOffset());
- jsonBody.put("language", Locale.getDefault().getLanguage());
- jsonBody.put("sdk", VERSION);
-
- if (getUserId() == null) {
- jsonBody.put("android_package", packageName);
- jsonBody.put("sdk_type", sdkType);
- }
-
- // These values would never change as well but need to send them in case the user has been deleted via the dashboard.
- jsonBody.put("device_model", Build.MODEL);
- jsonBody.put("device_type", deviceType);
-
- if (syncedSubscription != currentSubscription)
- jsonBody.put("notification_types", currentSubscription);
-
- try {
- jsonBody.put("game_version", "" + packageManager.getPackageInfo(packageName, 0).versionCode);
- } catch (PackageManager.NameNotFoundException e) {}
- List packList = packageManager.getInstalledPackages(0);
- int count = -1;
- for(int i = 0; i < packList.size(); i++)
- count += ((packList.get(i).applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) ? 1 : 0;
- jsonBody.put("pkgc", count);
-
- addNetType(jsonBody);
+ private static LocationGMS locationGMS;
- if (RootToolsInternalMethods.isRooted())
- jsonBody.put("rooted", true);
-
- try {
- Field[] fields = Class.forName(packageName + ".R$raw").getFields();
- JSONArray soundList = new JSONArray();
- TypedValue fileType = new TypedValue();
- String fileName;
-
- for (int i = 0; i < fields.length; i++) {
- appContext.getResources().getValue(fields[i].getInt(null), fileType, true);
- fileName = fileType.string.toString().toLowerCase();
+ private static void registerUser() {
+ if (!registerForPushFired || !locationFired)
+ return;
- if (fileName.endsWith(".wav") || fileName.endsWith(".mp3"))
- soundList.put(fields[i].getName());
- }
+ if (ranSessionInitThread) {
+ updateRegistrationId();
+ return;
+ }
- if (soundList.length() > 0)
- jsonBody.put("sounds", soundList);
- } catch (Throwable t) {}
-
- String urlStr;
- if (getUserId() == null)
- urlStr = "players";
- else
- urlStr = "players/" + getUserId() + "/on_session";
-
- JsonHttpResponseHandler jsonHandler = new JsonHttpResponseHandler() {
- @Override
- public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
- try {
- try {
- if (jsonBody.has("notification_types"))
- saveSyncedSubscription(jsonBody.getInt("notification_types"));
- } catch (JSONException e) { e.printStackTrace(); }
-
- if (response.has("id")) {
- saveUserId(response.getString("id"));
- sendPendingIdData();
-
- fireIdsAvailableCallback();
-
- Log(LOG_LEVEL.INFO, "Device registered with OneSignal, UserId = " + response.getString("id"));
- }
- else
- Log(LOG_LEVEL.INFO, "Device session registered with OneSignal, UserId = " + getUserId());
- } catch (Throwable t) {
- Log(LOG_LEVEL.ERROR, "ERROR parsing on_session or create JSON Response.", t);
- }
- }
+ ranSessionInitThread = true;
- @Override
- public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
- logHttpError("Create or on_session for user failed to send!", statusCode, throwable, errorResponse);
- }
- };
- OneSignalRestClient.postSync(appContext, urlStr, jsonBody, jsonHandler);
+ new Thread(new Runnable() {
+ public void run() {
+ OneSignalStateSynchronizer.UserState userState = OneSignalStateSynchronizer.getNewUserState();
+
+ String packageName = appContext.getPackageName();
+ PackageManager packageManager = appContext.getPackageManager();
+
+ userState.set("app_id", appId);
+ userState.set("identifier", lastRegistrationId);
+
+ String adId = mainAdIdProvider.getIdentifier(appContext);
+ // "... must use the advertising ID (when available on a device) in lieu of any other device identifiers ..."
+ // https://play.google.com/about/developer-content-policy.html
+ if (adId == null)
+ adId = new AdvertisingIdProviderFallback().getIdentifier(appContext);
+ userState.set("ad_id", adId);
+ userState.set("device_os", Build.VERSION.RELEASE);
+ userState.set("timezone", getTimeZoneOffset());
+ userState.set("language", Locale.getDefault().getLanguage());
+ userState.set("sdk", VERSION);
+ userState.set("sdk_type", sdkType);
+ userState.set("android_package", packageName);
+ userState.set("device_model", Build.MODEL);
+ userState.set("device_type", deviceType);
+ userState.setState("subscribableStatus", subscribableStatus);
- } catch (Throwable t) { // JSONException and UnsupportedEncodingException
- Log(LOG_LEVEL.ERROR, "Generating JSON create or on_session for user failed!", t);
- }
+ try {
+ userState.set("game_version", packageManager.getPackageInfo(packageName, 0).versionCode);
+ } catch (PackageManager.NameNotFoundException e) {}
+
+ List packList = packageManager.getInstalledPackages(0);
+ int count = -1;
+ for (int i = 0; i < packList.size(); i++)
+ count += ((packList.get(i).applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) ? 1 : 0;
+ userState.set("pkgc", count);
+ userState.set("net_type", osUtils.getNetType());
+ userState.set("carrier", osUtils.getCarrierName());
+ userState.set("rooted", RootToolsInternalMethods.isRooted());
+ userState.set("lat", lastLocLat); userState.set("long", lastLocLong);
+
+ OneSignalStateSynchronizer.postSession(userState);
}
}).start();
}
- private static void sendPendingIdData() {
- if (pendingTags != null || currentSubscription != 1) {
- try {
- JSONObject json = playerUpdateBaseJSON();
- if (pendingTags != null) {
- json.put("tags", pendingTags);
- pendingTags = null;
- }
-
- if (currentSubscription != 1)
- json.put("notification_types", currentSubscription);
- postPlayerUpdate(json);
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- }
-
public static void sendTag(String key, String value) {
try {
sendTags(new JSONObject().put(key, value));
@@ -634,61 +624,8 @@ public static void sendTags(String jsonString) {
}
public static void sendTags(JSONObject keyValues) {
- try {
- if (getUserId() == null) {
- if (pendingTags == null)
- pendingTags = new JSONObject();
- Iterator keys = keyValues.keys();
- String key;
- while (keys.hasNext()) {
- key = keys.next();
- pendingTags.put(key, keyValues.get(key));
- }
- } else {
- JSONObject jsonBody = playerUpdateBaseJSON();
- if (keyValues != null)
- jsonBody.put("tags", keyValues);
-
- postPlayerUpdate(jsonBody);
- }
- } catch (Throwable t) { // JSONException and UnsupportedEncodingException
- Log(LOG_LEVEL.ERROR, "Generating JSON sendTags failed!", t);
- }
- }
-
- private static JSONObject playerUpdateBaseJSON() {
- JSONObject jsonBody = new JSONObject();
- try {
- jsonBody.put("app_id", appId);
- addNetType(jsonBody);
- } catch (JSONException e) {
- Log(LOG_LEVEL.ERROR, "Generating player update base JSON failed!", e);
- }
-
- return jsonBody;
- }
-
- private static void postPlayerUpdate(final JSONObject postBody) {
- try {
- OneSignalRestClient.put(appContext, "players/" + getUserId(), postBody, new JsonHttpResponseHandler() {
- @Override
- public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
- logHttpError("player update failed!", statusCode, throwable, errorResponse);
- }
-
- @Override
- public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
- try {
- if (postBody.has("notification_types"))
- saveSyncedSubscription(postBody.getInt("notification_types"));
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- });
- } catch (UnsupportedEncodingException e) {
- Log(LOG_LEVEL.ERROR, "HTTP player update encoding exception!", e);
- }
+ if (keyValues == null) return;
+ OneSignalStateSynchronizer.sendTags(keyValues);
}
public static void postNotification(String json, final PostNotificationResponseHandler handler) {
@@ -703,40 +640,35 @@ public static void postNotification(JSONObject json, final PostNotificationRespo
try {
json.put("app_id", getSavedAppId());
- OneSignalRestClient.post(appContext, "notifications/", json, new JsonHttpResponseHandler() {
+ OneSignalRestClient.post("notifications/", json, new OneSignalRestClient.ResponseHandler() {
@Override
- public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
+ public void onSuccess(String response) {
Log(LOG_LEVEL.DEBUG, "HTTP create notification success: " + (response != null ? response : "null"));
- if (handler != null)
- handler.onSuccess(response);
+ if (handler != null) {
+ try {
+ handler.onSuccess(new JSONObject(response));
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
}
@Override
- public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject response) {
+ void onFailure(int statusCode, String response, Throwable throwable) {
logHttpError("create notification failed", statusCode, throwable, response);
- if (statusCode == 0) {
+ if (statusCode == 0)
+ response = "{'error': 'HTTP no response error'}";
+
+ if (handler != null) {
try {
- response = new JSONObject("{'error': 'HTTP no response error'}");
- } catch (JSONException e1) {
- if (atLogLevel(LOG_LEVEL.INFO))
- e1.printStackTrace();
+ handler.onFailure(new JSONObject(response));
+ } catch (Throwable t) {
+ handler.onFailure(null);
}
}
-
- if (handler != null)
- handler.onFailure(response);
}
});
- } catch (UnsupportedEncodingException e) {
- Log(LOG_LEVEL.ERROR, "HTTP create notification encoding exception!", e);
- if (handler != null) {
- try {
- handler.onFailure(new JSONObject("{'error': 'HTTP create notification encoding exception!'}"));
- } catch (JSONException e1) {
- e1.printStackTrace();
- }
- }
} catch (JSONException e) {
Log(LOG_LEVEL.ERROR, "HTTP create notification json exception!", e);
if (handler != null) {
@@ -750,26 +682,7 @@ public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSO
}
public static void getTags(final GetTagsHandler getTagsHandler) {
- OneSignalRestClient.get(appContext, "players/" + getUserId(), new JsonHttpResponseHandler() {
- @Override
- public void onSuccess(int statusCode, Header[] headers, final JSONObject response) {
- appContext.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- try {
- getTagsHandler.tagsAvailable(response.getJSONObject("tags"));
- } catch (Throwable t) {
- Log(LOG_LEVEL.ERROR, "Failed to Parse getTags.", t);
- }
- }
- });
- }
-
- @Override
- public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
- logHttpError("failed to getTags.", statusCode, throwable, errorResponse);
- }
- });
+ getTagsHandler.tagsAvailable(OneSignalStateSynchronizer.getTags());
}
public static void deleteTag(String key) {
@@ -785,7 +698,7 @@ public static void deleteTags(Collection keys) {
jsonTags.put(key, "");
sendTags(jsonTags);
- } catch (Throwable t) { // JSONException and UnsupportedEncodingException
+ } catch (Throwable t) {
Log(LOG_LEVEL.ERROR, "Failed to generate JSON for deleteTags.", t);
}
}
@@ -799,7 +712,7 @@ public static void deleteTags(String jsonArrayString) {
jsonTags.put(jsonArray.getString(i), "");
sendTags(jsonTags);
- } catch (Throwable t) { // JSONException and UnsupportedEncodingException
+ } catch (Throwable t) {
Log(LOG_LEVEL.ERROR, "Failed to generate JSON for deleteTags.", t);
}
}
@@ -811,9 +724,9 @@ public static void idsAvailable(IdsAvailableHandler inIdsAvailableHandler) {
internalFireIdsAvailableCallback();
}
- private static void fireIdsAvailableCallback() {
+ static void fireIdsAvailableCallback() {
if (idsAvailableHandler != null) {
- appContext.runOnUiThread(new Runnable() {
+ runOnUiThread(new Runnable() {
@Override
public void run() {
internalFireIdsAvailableCallback();
@@ -823,8 +736,11 @@ public void run() {
}
private static void internalFireIdsAvailableCallback() {
- String regId = GetRegistrationId();
- if (currentSubscription < 1)
+ if (idsAvailableHandler == null)
+ return;
+
+ String regId = OneSignalStateSynchronizer.getRegistrationId();
+ if (!OneSignalStateSynchronizer.getSubscribed())
regId = null;
String userId = getUserId();
@@ -837,7 +753,7 @@ private static void internalFireIdsAvailableCallback() {
idsAvailableHandler = null;
}
- static void sendPurchases(JSONArray purchases, boolean newAsExisting, ResponseHandlerInterface httpHandler) {
+ static void sendPurchases(JSONArray purchases, boolean newAsExisting, OneSignalRestClient.ResponseHandler responseHandler) {
if (getUserId() == null)
return;
@@ -847,38 +763,42 @@ static void sendPurchases(JSONArray purchases, boolean newAsExisting, ResponseHa
if (newAsExisting)
jsonBody.put("existing", true);
jsonBody.put("purchases", purchases);
-
- if (httpHandler == null)
- httpHandler = new JsonHttpResponseHandler();
- OneSignalRestClient.post(appContext, "players/" + getUserId() + "/on_purchase", jsonBody, httpHandler);
- } catch (Throwable t) { // JSONException and UnsupportedEncodingException
+ OneSignalRestClient.post("players/" + getUserId() + "/on_purchase", jsonBody, responseHandler);
+ } catch (Throwable t) {
Log(LOG_LEVEL.ERROR, "Failed to generate JSON for sendPurchases.", t);
}
}
- private static void openWebURLFromNotification(JSONArray dataArray) {
+ private static boolean openURLFromNotification(JSONArray dataArray) {
int jsonArraySize = dataArray.length();
+ boolean urlOpened = false;
+
for (int i = 0; i < jsonArraySize; i++) {
try {
JSONObject data = dataArray.getJSONObject(i);
if (!data.has("custom"))
- return;
+ continue;
JSONObject customJSON = new JSONObject(data.getString("custom"));
if (customJSON.has("u")) {
String url = customJSON.getString("u");
- if (!url.startsWith("http://") && !url.startsWith("https://"))
+ if (!url.contains("://"))
url = "http://" + url;
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- appContext.startActivity(browserIntent);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET |Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ appContext.startActivity(intent);
+ urlOpened = true;
}
} catch (Throwable t) {
Log(LOG_LEVEL.ERROR, "Error parsing JSON item " + i + "/" + jsonArraySize + " for launching a web URL.", t);
}
}
+
+ return urlOpened;
}
private static void runNotificationOpenedCallback(final JSONArray dataArray, final boolean isActive) {
@@ -945,7 +865,7 @@ private static void runNotificationOpenedCallback(final JSONArray dataArray, fin
}
}
- if (appContext.isFinishing()
+ if (ActivityLifecycleHandler.curActivity != null && ActivityLifecycleHandler.curActivity.isFinishing()
&& (notificationOpenedHandler.getClass().isAnnotationPresent(TiedToCurrentActivity.class)
|| notificationOpenedHandler instanceof Activity)) {
@@ -962,7 +882,7 @@ private static void fireNotificationOpenedHandler(final String message, final JS
if (Looper.getMainLooper().getThread() == Thread.currentThread()) // isUIThread
notificationOpenedHandler.notificationOpened(message, additionalDataJSON, isActive);
else {
- appContext.runOnUiThread(new Runnable() {
+ runOnUiThread(new Runnable() {
@Override
public void run() {
notificationOpenedHandler.notificationOpened(message, additionalDataJSON, isActive);
@@ -977,12 +897,23 @@ static void handleNotificationOpened(JSONArray data) {
runNotificationOpenedCallback(data, true);
}
- // Called when opening a notification when the app is suspended in the background or when it is dead
- public static void handleNotificationOpened(Context inContext, JSONArray data) {
+ // Called when opening a notification when the app is suspended in the background, from alert type notification, or when it is dead
+ public static void handleNotificationOpened(Context inContext, JSONArray data, boolean fromAlert) {
sendNotificationOpened(inContext, data);
+ boolean urlOpened = openURLFromNotification(data);
+
+ if (initDone)
+ runNotificationOpenedCallback(data, false);
+ else
+ unprocessedOpenedNotifis.add(data);
+
// Open/Resume app when opening the notification.
+ if (!fromAlert && !urlOpened)
+ fireIntentFromNotificationOpen(inContext, data);
+ }
+ private static void fireIntentFromNotificationOpen(Context inContext, JSONArray data) {
PackageManager packageManager = inContext.getPackageManager();
boolean isCustom = false;
@@ -997,12 +928,14 @@ public static void handleNotificationOpened(Context inContext, JSONArray data) {
isCustom = true;
}
+
+ // Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag.
resolveInfo = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfo.size() > 0) {
if (!isCustom)
intent.putExtra("onesignal_data", data.toString());
isCustom = true;
- intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
inContext.startActivity(intent);
}
@@ -1010,16 +943,10 @@ public static void handleNotificationOpened(Context inContext, JSONArray data) {
Intent launchIntent = inContext.getPackageManager().getLaunchIntentForPackage(inContext.getPackageName());
if (launchIntent != null) {
- launchIntent.putExtra("onesignal_data", data.toString())
- .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ launchIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
inContext.startActivity(launchIntent);
}
}
-
- if (initDone) {
- openWebURLFromNotification(data);
- runNotificationOpenedCallback(data, false);
- }
}
private static void sendNotificationOpened(Context inContext, JSONArray dataArray) {
@@ -1044,10 +971,10 @@ private static void sendNotificationOpened(Context inContext, JSONArray dataArra
jsonBody.put("player_id", getSavedUserId(inContext));
jsonBody.put("opened", true);
- OneSignalRestClient.put(inContext, "notifications/" + notificationId, jsonBody, new JsonHttpResponseHandler() {
+ OneSignalRestClient.put("notifications/" + notificationId, jsonBody, new OneSignalRestClient.ResponseHandler() {
@Override
- public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
- logHttpError("sending Notification Opened Failed", statusCode, throwable, errorResponse);
+ void onFailure(int statusCode, String response, Throwable throwable) {
+ logHttpError("sending Notification Opened Failed", statusCode, throwable, response);
}
});
}
@@ -1066,7 +993,7 @@ private static void SaveAppId(String appId) {
editor.commit();
}
- private static String getSavedAppId() {
+ static String getSavedAppId() {
return getSavedAppId(appContext);
}
@@ -1093,7 +1020,7 @@ static String getUserId() {
return userId;
}
- private static void saveUserId(String inUserId) {
+ static void saveUserId(String inUserId) {
userId = inUserId;
if (appContext == null)
return;
@@ -1103,14 +1030,6 @@ private static void saveUserId(String inUserId) {
editor.commit();
}
- static String GetRegistrationId() {
- if (registrationId == null && appContext != null) {
- final SharedPreferences prefs = getGcmPreferences(appContext);
- registrationId = prefs.getString("GT_REGISTRATION_ID", null);
- }
- return registrationId;
- }
-
// If true(default) - Device will always vibrate unless the device is in silent mode.
// If false - Device will only vibrate when the device is set on it's vibrate only mode.
public static void enableVibrate(boolean enable) {
@@ -1177,62 +1096,27 @@ public static void setSubscription(boolean enable) {
return;
}
- currentSubscription = currentSubscription < UNSUBSCRIBE_VALUE ? currentSubscription : (enable ? 1 : UNSUBSCRIBE_VALUE);
- saveSubscription(currentSubscription);
- if (syncedSubscription == currentSubscription)
- return;
-
- try {
- if (getUserId() != null) {
- JSONObject jsonBody = playerUpdateBaseJSON();
- jsonBody.put("notification_types", currentSubscription);
- postPlayerUpdate(jsonBody);
- }
- } catch (Throwable t) { // JSONException and UnsupportedEncodingException
- Log(LOG_LEVEL.ERROR, "Generating JSON setSubscription failed!", t);
- }
- }
-
- private static void saveSubscription(int value) {
- final SharedPreferences prefs = getGcmPreferences(appContext);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putInt("ONESIGNAL_SUBSCRIPTION", value);
- editor.commit();
- savedSubscription = value;
- }
-
- private static void saveSyncedSubscription(int value) {
- final SharedPreferences prefs = getGcmPreferences(appContext);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putInt("ONESIGNAL_SYNCED_SUBSCRIPTION", value);
- editor.commit();
- syncedSubscription = value;
- }
-
- static int getSyncedSubscription(Context context) {
- final SharedPreferences prefs = getGcmPreferences(context);
- return prefs.getInt("ONESIGNAL_SYNCED_SUBSCRIPTION", 1);
- }
-
- static int getSubscription(Context context) {
- final SharedPreferences prefs = getGcmPreferences(context);
- return prefs.getInt("ONESIGNAL_SUBSCRIPTION", 1);
+ OneSignalStateSynchronizer.setSubscription(enable);
}
- private static void SaveRegistrationId(String inRegistrationId) {
- registrationId = inRegistrationId;
- final SharedPreferences prefs = getGcmPreferences(appContext);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString("GT_REGISTRATION_ID", registrationId);
- editor.commit();
+ public static void promptLocation() {
+ LocationGMS.getLocation(appContext, true, new LocationGMS.LocationHandler() {
+ @Override
+ public void complete(Double lat, Double log) {
+ if (lat != null && log != null)
+ OneSignalStateSynchronizer.updateLocation(lat, log);
+ }
+ });
}
- private static long GetUnsentActiveTime() {
+ static long GetUnsentActiveTime() {
if (unSentActiveTime == -1 && appContext != null) {
final SharedPreferences prefs = getGcmPreferences(appContext);
unSentActiveTime = prefs.getLong("GT_UNSENT_ACTIVE_TIME", 0);
}
+ Log(LOG_LEVEL.INFO, "GetUnsentActiveTime: " + unSentActiveTime);
+
return unSentActiveTime;
}
@@ -1240,6 +1124,9 @@ private static void SaveUnsentActiveTime(long time) {
unSentActiveTime = time;
if (appContext == null)
return;
+
+ Log(LOG_LEVEL.INFO, "SaveUnsentActiveTime: " + unSentActiveTime);
+
final SharedPreferences prefs = getGcmPreferences(appContext);
SharedPreferences.Editor editor = prefs.edit();
editor.putLong("GT_UNSENT_ACTIVE_TIME", time);
@@ -1277,6 +1164,11 @@ static boolean isDuplicateNotification(String id, Context context) {
return false;
}
+
+ static void runOnUiThread(Runnable action) {
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(action);
+ }
static boolean isValidAndNotDuplicated(Context context, Bundle bundle) {
if (bundle.isEmpty())
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbContract.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbContract.java
index ea613e7e92..fc1472ed76 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbContract.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbContract.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbHelper.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbHelper.java
index c28e547751..6156a89661 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbHelper.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalDbHelper.java
@@ -3,9 +3,6 @@
*
* Copyright 2015 OneSignal
*
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
- *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java
index 2b7971d947..60fe5823c0 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java
@@ -27,68 +27,114 @@
package com.onesignal;
-import java.io.UnsupportedEncodingException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Scanner;
-import org.apache.http.entity.StringEntity;
import org.json.JSONObject;
-import android.content.Context;
-
-import com.loopj.android.http.*;
-
-// We use new Threads for async calls instead of loopj's AsyncHttpClient for 2 reasons:
-// 1. To make sure our callbacks finish in cases where these methods might be called from a short lived thread.
-// 2. If there isn't a looper on the current thread we can't use loopj's built in async implementation without calling
-// Looper.prepare() which can have unexpected results on the current thread.
-
class OneSignalRestClient {
- private static final String BASE_URL = "https://onesignal.com/api/v1/";
- private static final int TIMEOUT = 20000;
-
- private static SyncHttpClient clientSync = new SyncHttpClient();
-
- static {
- // setTimeout method = socket timeout
- // setMaxRetriesAndTimeout = sleep between retries
- clientSync.setTimeout(TIMEOUT);
- clientSync.setMaxRetriesAndTimeout(3, TIMEOUT);
+ static class ResponseHandler {
+ void onSuccess(String response) {}
+ void onFailure(int statusCode, String response, Throwable throwable) {}
}
- static void put(final Context context, final String url, JSONObject jsonBody, final ResponseHandlerInterface responseHandler) throws UnsupportedEncodingException {
- final StringEntity entity = new StringEntity(jsonBody.toString(), "UTF-8");
+ private static final String BASE_URL = "https://onesignal.com/api/v1/";
+ private static final int TIMEOUT = 120000;
+
+ static void put(final String url, final JSONObject jsonBody, final ResponseHandler responseHandler) {
new Thread(new Runnable() {
public void run() {
- clientSync.put(context, BASE_URL + url, entity, "application/json", responseHandler);
+ makeRequest(url, "PUT", jsonBody, responseHandler);
}
}).start();
}
- static void post(final Context context, final String url, JSONObject jsonBody, final ResponseHandlerInterface responseHandler) throws UnsupportedEncodingException {
- final StringEntity entity = new StringEntity(jsonBody.toString(), "UTF-8");
+ static void post(final String url, final JSONObject jsonBody, final ResponseHandler responseHandler) {
new Thread(new Runnable() {
public void run() {
- clientSync.post(context, BASE_URL + url, entity, "application/json", responseHandler);
+ makeRequest(url, "POST", jsonBody, responseHandler);
}
}).start();
}
- static void get(final Context context, final String url, final ResponseHandlerInterface responseHandler) {
+ static void get(final String url, final ResponseHandler responseHandler) {
new Thread(new Runnable() {
public void run() {
- clientSync.get(context, BASE_URL + url, responseHandler);
+ makeRequest(url, null, null, responseHandler);
}
}).start();
}
- static void putSync(Context context, String url, JSONObject jsonBody, ResponseHandlerInterface responseHandler) throws UnsupportedEncodingException {
- StringEntity entity = new StringEntity(jsonBody.toString(), "UTF-8");
- clientSync.put(context, BASE_URL + url, entity, "application/json", responseHandler);
+ static void putSync(String url, JSONObject jsonBody, ResponseHandler responseHandler) {
+ makeRequest(url, "PUT", jsonBody, responseHandler);
}
- static void postSync(Context context, String url, JSONObject jsonBody, ResponseHandlerInterface responseHandler) throws UnsupportedEncodingException {
- StringEntity entity = new StringEntity(jsonBody.toString(), "UTF-8");
- clientSync.post(context, BASE_URL + url, entity, "application/json", responseHandler);
+ static void postSync(String url, JSONObject jsonBody, ResponseHandler responseHandler) {
+ makeRequest(url, "POST", jsonBody, responseHandler);
}
+ private static void makeRequest(String url, String method, JSONObject jsonBody, ResponseHandler responseHandler) {
+ HttpURLConnection con = null;
+ int httpResponse = -1;
+ String json;
+
+ try {
+ con = (HttpURLConnection)new URL(BASE_URL + url).openConnection();
+ con.setUseCaches(false);
+ con.setDoOutput(true);
+ con.setConnectTimeout(TIMEOUT);
+ con.setReadTimeout(TIMEOUT);
+
+ if (jsonBody != null)
+ con.setDoInput(true);
+
+ con.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+ con.setRequestMethod(method);
+
+ if (jsonBody != null) {
+ String strJsonBody = jsonBody.toString();
+ OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, method + " SEND JSON: " + strJsonBody);
+
+ byte[] sendBytes = strJsonBody.getBytes("UTF-8");
+ con.setFixedLengthStreamingMode(sendBytes.length);
+
+ OutputStream outputStream = con.getOutputStream();
+ outputStream.write(sendBytes);
+ }
+
+ httpResponse = con.getResponseCode();
+
+ if (httpResponse == HttpURLConnection.HTTP_OK) {
+ Scanner scanner = new Scanner(con.getInputStream(), "UTF-8");
+ json = scanner.useDelimiter("\\A").hasNext() ? scanner.next() : "";
+ OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, method + " RECEIVED JSON: " + json);
+
+ if (responseHandler != null)
+ responseHandler.onSuccess(json);
+ }
+ else {
+ Scanner scanner = new Scanner(con.getErrorStream(), "UTF-8");
+ json = scanner.useDelimiter("\\A").hasNext() ? scanner.next() : "";
+ OneSignal.Log(OneSignal.LOG_LEVEL.WARN, method + " RECEIVED JSON: " + json);
+
+ if (responseHandler != null)
+ responseHandler.onFailure(httpResponse, json, null);
+ }
+ } catch (Throwable t) {
+ if (t instanceof java.net.ConnectException)
+ OneSignal.Log(OneSignal.LOG_LEVEL.INFO, "Could not send last request, device is offline.");
+ else
+ OneSignal.Log(OneSignal.LOG_LEVEL.WARN, method + " Error thrown from network stack. ", t);
+
+ if (responseHandler != null)
+ responseHandler.onFailure(httpResponse, null, t);
+ }
+ finally {
+ if (con != null)
+ con.disconnect();
+ }
+ }
}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalStateSynchronizer.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalStateSynchronizer.java
new file mode 100644
index 0000000000..cc993f10cd
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalStateSynchronizer.java
@@ -0,0 +1,489 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+class OneSignalStateSynchronizer {
+ private static boolean onSessionDone = false, postSessionCalled = false, waitingForSessionResponse = false;
+
+ // currentUserState - current know state of the user on OneSignal's server.
+ // toSyncUserState - pending state that will be synced to the OneSignal server.
+ // diff will be generated between currentUserState when sync call is made to the server.
+ private static UserState currentUserState, toSyncUserState;
+
+ static HashMap networkHandlerThreads = new HashMap<>();
+
+ private static Context appContext;
+
+ static private JSONObject generateJsonDiff(JSONObject cur, JSONObject changedTo, JSONObject baseOutput) {
+ Iterator keys = changedTo.keys();
+ String key;
+ Object value;
+
+ JSONObject output;
+ if (baseOutput != null)
+ output = baseOutput;
+ else
+ output = new JSONObject();
+
+ while (keys.hasNext()) {
+ try {
+ key = keys.next();
+ value = changedTo.get(key);
+
+ if (cur.has(key)) {
+ if (value instanceof JSONObject) {
+ JSONObject curValue = cur.getJSONObject(key);
+ JSONObject outValue = null;
+ if (baseOutput != null && baseOutput.has(key))
+ outValue = baseOutput.getJSONObject(key);
+ JSONObject returnedJson = generateJsonDiff(curValue, (JSONObject) value, outValue);
+ String returnedJsonStr = returnedJson.toString();
+ if (!returnedJsonStr.equals("{}"))
+ output.put(key, new JSONObject(returnedJsonStr));
+ }
+ else if (!value.equals(cur.get(key)))
+ output.put(key, value);
+ }
+ else {
+ if (value instanceof JSONObject)
+ output.put(key, new JSONObject(value.toString()));
+ else
+ output.put(key, value);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return output;
+ }
+
+ public static void stopAndPersist() {
+ for (Map.Entry handlerThread : OneSignalStateSynchronizer.networkHandlerThreads.entrySet())
+ handlerThread.getValue().stopScheduledRunnable();
+
+ if (toSyncUserState != null)
+ toSyncUserState.persistState();
+ }
+
+ class UserState {
+ private final int UNSUBSCRIBE_VALUE = -2;
+
+ private String persistKey;
+
+ JSONObject dependValues, syncValues;
+
+ private UserState(String inPersistKey, boolean load) {
+ persistKey = inPersistKey;
+ if (load)
+ loadState();
+ else {
+ dependValues = new JSONObject();
+ syncValues = new JSONObject();
+ }
+ }
+
+ private UserState deepClone(String persistKey) {
+ UserState clonedUserState = new UserState(persistKey, false);
+
+ try {
+ clonedUserState.dependValues = new JSONObject(dependValues.toString());
+ clonedUserState.syncValues = new JSONObject(syncValues.toString());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return clonedUserState;
+ }
+
+ private void addDependFields() {
+ try {
+ syncValues.put("notification_types", getNotificationTypes());
+ } catch (JSONException e) {}
+ }
+
+ private int getNotificationTypes() {
+ try {
+ int subscribableStatus = dependValues.getInt("subscribableStatus");
+ boolean userSubscribePref = dependValues.getBoolean("userSubscribePref");
+ return subscribableStatus < UNSUBSCRIBE_VALUE ? subscribableStatus : (userSubscribePref ? 1 : UNSUBSCRIBE_VALUE);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return 1;
+ }
+
+ private JSONObject generateJsonDiff(UserState newState, boolean isSessionCall) {
+ addDependFields(); newState.addDependFields();
+ JSONObject sendJson = OneSignalStateSynchronizer.generateJsonDiff(syncValues, newState.syncValues, null);
+
+ if (!isSessionCall && sendJson.toString().equals("{}"))
+ return null;
+
+ try {
+ // This makes sure app_id is in all our REST calls.
+ if (!sendJson.has("app_id"))
+ sendJson.put("app_id", (String) syncValues.opt("app_id"));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return sendJson;
+ }
+
+ void set(String key, Object value) {
+ try {
+ syncValues.put(key, value);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void setState(String key, Object value) {
+ try {
+ dependValues.put(key, value);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void loadState() {
+ final SharedPreferences prefs = OneSignal.getGcmPreferences(appContext);
+
+ String dependValuesStr = prefs.getString("ONESIGNAL_USERSTATE_DEPENDVALYES_" + persistKey, null);
+ if (dependValuesStr == null) {
+ dependValues = new JSONObject();
+ try {
+ int subscribableStatus;
+ boolean userSubscribePref = true;
+ if (persistKey.equals("CURRENT_STATE"))
+ subscribableStatus = prefs.getInt("ONESIGNAL_SUBSCRIPTION", 1);
+ else
+ subscribableStatus = prefs.getInt("ONESIGNAL_SYNCED_SUBSCRIPTION", 1);
+
+ if (subscribableStatus == UNSUBSCRIBE_VALUE) {
+ subscribableStatus = 1;
+ userSubscribePref = false;
+ }
+
+ dependValues.put("subscribableStatus", subscribableStatus);
+ dependValues.put("userSubscribePref", userSubscribePref);
+ } catch (JSONException e) {}
+ }
+ else {
+ try {
+ dependValues = new JSONObject(dependValuesStr);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ String syncValuesStr = prefs.getString("ONESIGNAL_USERSTATE_SYNCVALYES_" + persistKey, null);
+ try {
+ if (syncValuesStr == null) {
+ syncValues = new JSONObject();
+ syncValues.put("identifier", prefs.getString("GT_REGISTRATION_ID", null));
+ }
+ else
+ syncValues = new JSONObject(syncValuesStr);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void persistState() {
+ final SharedPreferences prefs = OneSignal.getGcmPreferences(appContext);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString("ONESIGNAL_USERSTATE_SYNCVALYES_" + persistKey, syncValues.toString());
+ editor.putString("ONESIGNAL_USERSTATE_DEPENDVALYES_" + persistKey, dependValues.toString());
+ editor.commit();
+ }
+
+ private void persistStateAfterSync(JSONObject inDependValues, JSONObject inSyncValues) {
+ if (inDependValues != null)
+ OneSignalStateSynchronizer.generateJsonDiff(dependValues, inDependValues, dependValues);
+ if (inSyncValues != null)
+ OneSignalStateSynchronizer.generateJsonDiff(syncValues, inSyncValues, syncValues);
+
+ if (inDependValues != null || inSyncValues != null)
+ persistState();
+ }
+ }
+
+ static class NetworkHandlerThread extends HandlerThread {
+ private static final int NETWORK_HANDLER_USERSTATE = 0;
+
+ int mType;
+
+ Handler mHandler = null;
+
+ static final int MAX_RETRIES = 3;
+ int currentRetry;
+
+ NetworkHandlerThread(int type) {
+ super("NetworkHandlerThread");
+ mType = type;
+ start();
+ mHandler = new Handler(getLooper());
+ }
+
+ public void runNewJob() {
+ currentRetry = 0;
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.postDelayed(getNewRunnable(), 5000);
+ }
+
+ private Runnable getNewRunnable() {
+ switch (mType) {
+ case NETWORK_HANDLER_USERSTATE:
+ return new Runnable() {
+ @Override
+ public void run() {
+ syncUserState(false);
+ }
+ };
+ }
+
+ return null;
+ }
+
+ void stopScheduledRunnable() {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
+ void doRetry() {
+ if (currentRetry < MAX_RETRIES && !mHandler.hasMessages(0)) {
+ currentRetry++;
+ mHandler.postDelayed(getNewRunnable(), currentRetry * 10000);
+ }
+ }
+ }
+
+ static void initUserState(Context context) {
+ appContext = context;
+ if (currentUserState != null) return;
+
+ currentUserState = new OneSignalStateSynchronizer().new UserState("CURRENT_STATE", true);
+ toSyncUserState = new OneSignalStateSynchronizer().new UserState("TOSYNC_STATE", true);
+ }
+
+ static UserState getNewUserState() {
+ return new OneSignalStateSynchronizer().new UserState("nonPersist", false);
+ }
+
+ static void syncUserState(boolean fromSyncService) {
+ boolean isSessionCall = !onSessionDone && postSessionCalled && !waitingForSessionResponse;
+
+ final JSONObject jsonBody = currentUserState.generateJsonDiff(toSyncUserState, isSessionCall);
+ final JSONObject dependDiff = generateJsonDiff(currentUserState.dependValues, toSyncUserState.dependValues, null);
+
+ if (jsonBody == null) {
+ currentUserState.persistStateAfterSync(dependDiff, null);
+ return;
+ }
+
+ final String userId = OneSignal.getUserId();
+
+ toSyncUserState.persistState();
+ if (onSessionDone || fromSyncService) {
+ OneSignalRestClient.putSync("players/" + userId, jsonBody, new OneSignalRestClient.ResponseHandler() {
+ @Override
+ void onFailure(int statusCode, String response, Throwable throwable) {
+ OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "Failed last request. statusCode: " + statusCode + "\nresponse: " + response);
+
+ if (response400WithErrorsContaining(statusCode, response, "No user with this id found")) {
+ resetCurrentState();
+ postNewSyncUserState();
+ }
+ else
+ getNetworkHandlerThread(NetworkHandlerThread.NETWORK_HANDLER_USERSTATE).doRetry();
+ }
+
+ @Override
+ void onSuccess(String response) {
+ currentUserState.persistStateAfterSync(dependDiff, jsonBody);
+ }
+ });
+ }
+ else if (postSessionCalled) {
+ String urlStr;
+ if (userId == null)
+ urlStr = "players";
+ else
+ urlStr = "players/" + userId + "/on_session";
+
+ waitingForSessionResponse = true;
+ OneSignalRestClient.postSync(urlStr, jsonBody, new OneSignalRestClient.ResponseHandler() {
+ @Override
+ void onFailure(int statusCode, String response, Throwable throwable) {
+ waitingForSessionResponse = false;
+ OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "Failed last request. statusCode: " + statusCode + "\nresponse: " + response);
+
+ if (response400WithErrorsContaining(statusCode, response, "not a valid device_type")) {
+ resetCurrentState();
+ postNewSyncUserState();
+ }
+ else
+ getNetworkHandlerThread(NetworkHandlerThread.NETWORK_HANDLER_USERSTATE).doRetry();
+ }
+
+ @Override
+ void onSuccess(String response) {
+ onSessionDone = true;
+ waitingForSessionResponse = false;
+ currentUserState.persistStateAfterSync(dependDiff, jsonBody);
+
+ try {
+ JSONObject jsonResponse = new JSONObject(response);
+
+ if (jsonResponse.has("id")) {
+ String userId = jsonResponse.getString("id");
+ OneSignal.saveUserId(userId);
+
+ OneSignal.fireIdsAvailableCallback();
+
+ OneSignal.Log(OneSignal.LOG_LEVEL.INFO, "Device registered, UserId = " + userId);
+ } else
+ OneSignal.Log(OneSignal.LOG_LEVEL.INFO, "session sent, UserId = " + OneSignal.getUserId());
+ } catch (Throwable t) {
+ OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "ERROR parsing on_session or create JSON Response.", t);
+ }
+ }
+ });
+ }
+ }
+
+ private static boolean response400WithErrorsContaining(int statusCode, String response, String contains) {
+ if (statusCode == 400 && response != null) {
+ try {
+ JSONObject responseJson = new JSONObject(response);
+ return responseJson.has("errors") && responseJson.optString("errors").contains(contains);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ return false;
+ }
+
+ private static NetworkHandlerThread getNetworkHandlerThread(Integer type) {
+ if (!networkHandlerThreads.containsKey(type))
+ networkHandlerThreads.put(type, new NetworkHandlerThread(type));
+ return networkHandlerThreads.get(type);
+ }
+
+ private static UserState getUserStateForModification() {
+ if (toSyncUserState == null)
+ toSyncUserState = currentUserState.deepClone("TOSYNC_STATE");
+
+ postNewSyncUserState();
+
+ return toSyncUserState;
+ }
+
+ private static void postNewSyncUserState() {
+ getNetworkHandlerThread(NetworkHandlerThread.NETWORK_HANDLER_USERSTATE).runNewJob();
+ }
+
+ static void postSession(UserState postSession) {
+ JSONObject toSync = getUserStateForModification().syncValues;
+ generateJsonDiff(toSync, postSession.syncValues, toSync);
+ JSONObject dependValues = getUserStateForModification().dependValues;
+ generateJsonDiff(dependValues, postSession.dependValues, dependValues);
+
+ postSessionCalled = true;
+ }
+
+ static void sendTags(JSONObject newTags) {
+ JSONObject userStateTags = getUserStateForModification().syncValues;
+ try {
+ generateJsonDiff(userStateTags, new JSONObject().put("tags", newTags), userStateTags);
+ } catch (JSONException e) { e.printStackTrace(); }
+ }
+
+ static void setSubscription(boolean enable) {
+ try {
+ getUserStateForModification().dependValues.put("userSubscribePref", enable);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ static void updateIdentifier(String identifier) {
+ UserState userState = getUserStateForModification();
+ try {
+ userState.syncValues.put("identifier", identifier);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ static void updateLocation(Double lat, Double log) {
+ UserState userState = getUserStateForModification();
+ try {
+ userState.syncValues.put("lat", lat);
+ userState.syncValues.put("long", log);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ static boolean getSubscribed() {
+ return toSyncUserState.getNotificationTypes() > 0;
+ }
+
+
+ static String getRegistrationId() {
+ return toSyncUserState.syncValues.optString("identifier", null);
+ }
+
+ static JSONObject getTags() {
+ return toSyncUserState.syncValues.optJSONObject("tags");
+ }
+
+ static void resetCurrentState() {
+ onSessionDone = false;
+ OneSignal.saveUserId(null);
+
+ currentUserState.syncValues = new JSONObject();
+ currentUserState.persistState();
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/PermissionsActivity.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/PermissionsActivity.java
new file mode 100644
index 0000000000..bf4b1cda53
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/PermissionsActivity.java
@@ -0,0 +1,67 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+
+public class PermissionsActivity extends Activity {
+
+ private static final int REQUEST_LOCATION = 2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestPermission();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ requestPermission();
+ }
+
+ private void requestPermission() {
+ ActivityCompat.requestPermissions(this, new String[] {LocationGMS.requestPermission}, REQUEST_LOCATION);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+ if (requestCode == REQUEST_LOCATION) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ LocationGMS.startGetLocation();
+ else
+ LocationGMS.fireFailedComplete();
+ }
+
+ finish();
+ }
+}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/PushRegistratorGPS.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/PushRegistratorGPS.java
index f0397ebddd..cd1e0b0147 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/PushRegistratorGPS.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/PushRegistratorGPS.java
@@ -2,9 +2,6 @@
* Modified MIT License
*
* Copyright 2015 OneSignal
- *
- * Portions Copyright 2013 Google Inc.
- * This file includes portions from the Google GcmClient demo project
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -159,7 +156,8 @@ public void run() {
OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "GCM_RETRY_COUNT of " + GCM_RETRY_COUNT + " exceed! Could not get a Google Registration Id", e);
else {
OneSignal.Log(OneSignal.LOG_LEVEL.INFO, "Google Play services returned SERVICE_NOT_AVAILABLE error. Current retry count: " + currentRetry, e);
- if (currentRetry == 0) {
+ if (currentRetry == 2) {
+ // Retry 3 times before firing a null response and continuing a few more times.
registeredHandler.complete(null);
firedComplete = true;
}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/SyncService.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/SyncService.java
new file mode 100644
index 0000000000..ee172569c4
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/SyncService.java
@@ -0,0 +1,101 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2015 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.onesignal;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+
+public class SyncService extends Service {
+
+ private void checkOnFocusSync() {
+ long unsentTime = OneSignal.GetUnsentActiveTime();
+ if (unsentTime < OneSignal.MIN_ON_FOCUS_TIME)
+ return;
+
+ OneSignal.sendOnFocus(unsentTime, true);
+ }
+
+ @Override
+ public void onCreate() {
+ // If service was started from outside the app.
+ if (OneSignal.appContext == null) {
+ OneSignal.appContext = this.getApplicationContext();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (OneSignal.getUserId() == null) {
+ stopSelf();
+ return;
+ }
+
+ OneSignal.appId = OneSignal.getSavedAppId();
+
+ OneSignalStateSynchronizer.initUserState(OneSignal.appContext);
+ OneSignalStateSynchronizer.syncUserState(true);
+ checkOnFocusSync();
+
+ stopSelf();
+ }
+ }).start();
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_STICKY;
+ }
+
+ // This Service does not support bindings.
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+
+ // Called by Android when a user swipes a way a task. On Android 4.1+ the process will be killed shortly after.
+ // However there is a rare case where if the process has 2 tasks that a both swiped away this will be called right after onCreate.
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ super.onTaskRemoved(rootIntent);
+ onTaskRemoved();
+ }
+
+
+ /* Always make sure we are shutting down as quickly as possible here! We are on the main thread and have 20 sec before the process is forcefully killed.
+ Also if the user reopens the app or there is another task still open on the same process it will hang the startup/UI there.
+ */
+ static void onTaskRemoved() {
+ ActivityLifecycleHandler.focusHandlerThread.stopScheduledRunnable();
+ OneSignalStateSynchronizer.stopAndPersist();
+ OneSignal.onAppLostFocus(true); // Save only
+ }
+}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/TrackGooglePurchase.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/TrackGooglePurchase.java
index 3496d6e7be..32826c3256 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/TrackGooglePurchase.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/TrackGooglePurchase.java
@@ -36,9 +36,7 @@
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import org.apache.http.Header;
-import com.loopj.android.http.JsonHttpResponseHandler;
import com.onesignal.OneSignal;
import com.onesignal.OneSignal.IdsAvailableHandler;
@@ -59,7 +57,7 @@ class TrackGooglePurchase {
private static Class> IInAppBillingServiceClass;
private Object mIInAppBillingService;
private Method getPurchasesMethod, getSkuDetailsMethod;
- private Activity appContext;
+ private Context appContext;
private ArrayList purchaseTokens;
private SharedPreferences.Editor prefsEditor;
@@ -69,7 +67,7 @@ class TrackGooglePurchase {
private boolean newAsExisting = true;
private boolean isWaitingForPurchasesRequest = false;
- TrackGooglePurchase(Activity activity) {
+ TrackGooglePurchase(Context activity) {
appContext = activity;
SharedPreferences prefs = appContext.getSharedPreferences("GTPlayerPurchases", Context.MODE_PRIVATE);
@@ -90,9 +88,9 @@ class TrackGooglePurchase {
trackIAP();
}
- static boolean CanTrack(Activity activity) {
+ static boolean CanTrack(Context context) {
if (iapEnabled == -99)
- iapEnabled = activity.checkCallingOrSelfPermission("com.android.vending.BILLING");
+ iapEnabled = context.checkCallingOrSelfPermission("com.android.vending.BILLING");
try {
if (iapEnabled == PackageManager.PERMISSION_GRANTED)
IInAppBillingServiceClass = Class.forName("com.android.vending.billing.IInAppBillingService");
@@ -131,8 +129,7 @@ public void onServiceConnected(ComponentName name, IBinder service) {
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
- Context applicationContext = appContext.getApplicationContext();
- applicationContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ appContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
} else if (mIInAppBillingService != null)
QueryBoughtItems();
}
@@ -230,13 +227,13 @@ private void sendPurchases(final ArrayList skusToAdd, final ArrayList