import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.NetworkInterface; import java.net.SocketException; import java.net.URL; import java.security.NoSuchAlgorithmException; import java.util.Enumeration; import java.util.Set; import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.NetworkInterface; import java.net.SocketException; import java.security.NoSuchAlgorithmException; import java.util.Scanner; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; // Submits build information to Realm when assembling the app // // To be clear: this does *not* run when your app is in production or on // your end-user's devices; it will only run when you build your app from source. // // Why are we doing this? Because it helps us build a better product for you. // None of the data personally identifies you, your employer or your app, but it // *will* help us understand what Realm version you use, what host OS you use, // etc. Having this info will help with prioritizing our time, adding new // features and deprecating old features. Collecting an anonymized bundle & // anonymized MAC is the only way for us to count actual usage of the other // metrics accurately. If we don't have a way to deduplicate the info reported, // it will be useless, as a single developer building their app on Windows ten // times would report 10 times more than a single developer that only builds // once from Mac OS X, making the data all but useless. No one likes sharing // data unless it's necessary, we get it, and we've debated adding this for a // long long time. Since Realm is a free product without an email signup, we // feel this is a necessary step so we can collect relevant data to build a // better product for you. // // Currently the following information is reported: // - What version of Realm is being used // - What OS you are running on // - An anonymized MAC address and bundle ID to aggregate the other information on. class SendAnalyticsTask extends DefaultTask { String applicationId = 'UNKNOWN' String version = 'UNKNOWN' @TaskAction def sendAnalytics() { try { def env = System.getenv() def disableAnalytics= env['REALM_DISABLE_ANALYTICS'] if (disableAnalytics == null || disableAnalytics != "true") { send() } } catch(all) {} } //TODO replace with properties private static final int READ_TIMEOUT = 2000 private static final int CONNECT_TIMEOUT = 4000 private static final String ADDRESS_PREFIX = "https://api.mixpanel.com/track/?data=" private static final String ADDRESS_SUFFIX = "&ip=1" private static final String TOKEN = "ce0fac19508f6c8f20066d345d360fd0" private static final String EVENT_NAME = "Run" private static final String JSON_TEMPLATE = ''' { "event": "%EVENT%", "properties": { "token": "%TOKEN%, "distinct_id": "%USER_ID%", "Anonymized MAC Address": "%USER_ID%", "Anonymized Bundle ID": "%APP_ID%", "Binding": "js", "Language": "js", "Framework": "react-native", "Virtual Machine": "jsc", "Realm Version": "%REALM_VERSION%", "Host OS Type": "%OS_TYPE%", "Host OS Version": "%OS_VERSION%", "Target OS Type": "android" } }''' void send() { def connection = (ADDRESS_PREFIX + Utils.base64Encode(generateJson()) + ADDRESS_SUFFIX) .toURL().openConnection() connection.setConnectTimeout(CONNECT_TIMEOUT); connection.setReadTimeout(READ_TIMEOUT); connection.setRequestMethod("GET") connection.connect() connection.getResponseCode() } private String generateJson() { JSON_TEMPLATE .replaceAll("%EVENT%", EVENT_NAME) .replaceAll("%TOKEN%", TOKEN) .replaceAll("%USER_ID%", ComputerIdentifierGenerator.get()) .replaceAll("%APP_ID%", getAnonymousAppId()) .replaceAll("%REALM_VERSION%", version) .replaceAll("%OS_TYPE%", System.getProperty("os.name")) .replaceAll("%OS_VERSION%", System.getProperty("os.version")) } /** * Computes an anonymous app/library id from the packages containing RealmObject classes * @return the anonymous app/library id * @throws NoSuchAlgorithmException */ private String getAnonymousAppId() { byte[] packagesBytes = applicationId?.getBytes() Utils.hexStringify(Utils.sha256Hash(packagesBytes)) } } class ComputerIdentifierGenerator { private static final String UNKNOWN = "unknown"; private static String OS = System.getProperty("os.name").toLowerCase() public static String get() { if (isWindows()) { return getWindowsIdentifier() } else if (isMac()) { return getMacOsIdentifier() } else if (isLinux()) { return getLinuxMacAddress() } else { return UNKNOWN } } private static boolean isWindows() { OS.contains("win") } private static boolean isMac() { OS.contains("mac") } private static boolean isLinux() { OS.contains("inux") } private static String getLinuxMacAddress() { File machineId = new File("/var/lib/dbus/machine-id") if (!machineId.exists()) { machineId = new File("/etc/machine-id") } if (!machineId.exists()) { return UNKNOWN } Scanner scanner = null try { scanner = new Scanner(machineId) String id = scanner.useDelimiter("\\A").next() return Utils.hexStringify(Utils.sha256Hash(id.getBytes())) } finally { if (scanner != null) { scanner.close() } } } private static String getMacOsIdentifier() { NetworkInterface networkInterface = NetworkInterface.getByName("en0") byte[] hardwareAddress = networkInterface.getHardwareAddress() Utils.hexStringify(Utils.sha256Hash(hardwareAddress)) } private static String getWindowsIdentifier() { Runtime runtime = Runtime.getRuntime() Process process = runtime.exec(["wmic", "csproduct", "get", "UUID"]) String result = null InputStream is = process.getInputStream() Scanner sc = new Scanner(process.getInputStream()) try { while (sc.hasNext()) { String next = sc.next() if (next.contains("UUID")) { result = sc.next().trim() break } } } finally { is.close() } result==null?UNKNOWN:Utils.hexStringify(Utils.sha256Hash(result.getBytes())) } } class Utils { /** * Encode the given string with Base64 * @param data the string to encode * @return the encoded string * @throws UnsupportedEncodingException */ public static String base64Encode(String data) throws UnsupportedEncodingException { return Base64.encodeToString(data.getBytes("UTF-8"), Base64.DEFAULT); } /** * Compute the SHA-256 hash of the given byte array * @param data the byte array to hash * @return the hashed byte array * @throws NoSuchAlgorithmException */ public static byte[] sha256Hash(byte[] data) throws NoSuchAlgorithmException { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); return messageDigest.digest(data); } /** * Convert a byte array to its hex-string * @param data the byte array to convert * @return the hex-string of the byte array */ public static String hexStringify(byte[] data) { StringBuilder stringBuilder = new StringBuilder(); for (byte singleByte : data) { stringBuilder.append(Integer.toString((singleByte & 0xff) + 0x100, 16).substring(1)); } return stringBuilder.toString(); } } /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Note: This version has been stripped down so it only supports encoding. */ import java.io.UnsupportedEncodingException; /** * Utilities for encoding and decoding the Base64 representation of * binary data. See RFCs 2045 and 3548. */ class Base64 { /** * Default values for encoder/decoder flags. */ public static final int DEFAULT = 0; /** * Encoder flag bit to omit the padding '=' characters at the end * of the output (if any). */ public static final int NO_PADDING = 1; /** * Encoder flag bit to omit all line terminators (i.e., the output * will be on one long line). */ public static final int NO_WRAP = 2; /** * Encoder flag bit to indicate lines should be terminated with a * CRLF pair instead of just an LF. Has no effect if {@code * NO_WRAP} is specified as well. */ public static final int CRLF = 4; /** * Encoder/decoder flag bit to indicate using the "URL and * filename safe" variant of Base64 (see RFC 3548 section 4) where * {@code -} and {@code _} are used in place of {@code +} and * {@code /}. */ public static final int URL_SAFE = 8; /** * Flag to pass to {@link Base64OutputStream} to indicate that it * should not close the output stream it is wrapping when it * itself is closed. */ public static final int NO_CLOSE = 16; // -------------------------------------------------------- // shared code // -------------------------------------------------------- /* package */ static abstract class Coder { public byte[] output; public int op; /** * Encode/decode another block of input data. this.output is * provided by the caller, and must be big enough to hold all * the coded data. On exit, this.opwill be set to the length * of the coded data. * * @param finish true if this is the final call to process for * this object. Will finalize the coder state and * include any final bytes in the output. * * @return true if the input so far is good; false if some * error has been detected in the input stream.. */ public abstract boolean process(byte[] input, int offset, int len, boolean finish); /** * @return the maximum number of bytes a call to process() * could produce for the given number of input bytes. This may * be an overestimate. */ public abstract int maxOutputSize(int len); } // -------------------------------------------------------- // encoding // -------------------------------------------------------- /** * Base64-encode the given data and return a newly allocated * String with the result. * * @param input the data to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static String encodeToString(byte[] input, int flags) { try { return new String(encode(input, flags), "US-ASCII"); } catch (UnsupportedEncodingException e) { // US-ASCII is guaranteed to be available. throw new AssertionError(e); } } /** * Base64-encode the given data and return a newly allocated * String with the result. * * @param input the data to encode * @param offset the position within the input array at which to * start * @param len the number of bytes of input to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static String encodeToString(byte[] input, int offset, int len, int flags) { try { return new String(encode(input, offset, len, flags), "US-ASCII"); } catch (UnsupportedEncodingException e) { // US-ASCII is guaranteed to be available. throw new AssertionError(e); } } /** * Base64-encode the given data and return a newly allocated * byte[] with the result. * * @param input the data to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static byte[] encode(byte[] input, int flags) { return encode(input, 0, input.length, flags); } /** * Base64-encode the given data and return a newly allocated * byte[] with the result. * * @param input the data to encode * @param offset the position within the input array at which to * start * @param len the number of bytes of input to encode * @param flags controls certain features of the encoded output. * Passing {@code DEFAULT} results in output that * adheres to RFC 2045. */ public static byte[] encode(byte[] input, int offset, int len, int flags) { Encoder encoder = new Encoder(flags, null); // Compute the exact length of the array we will produce. int output_len = len / 3 * 4; // Account for the tail of the data and the padding bytes, if any. if (encoder.do_padding) { if (len % 3 > 0) { output_len += 4; } } else { switch (len % 3) { case 0: break; case 1: output_len += 2; break; case 2: output_len += 3; break; } } // Account for the newlines, if any. if (encoder.do_newline && len > 0) { output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) * (encoder.do_cr ? 2 : 1); } encoder.output = new byte[output_len]; encoder.process(input, offset, len, true); assert encoder.op == output_len; return encoder.output; } static class Encoder extends Coder { /** * Lookup table for turning Base64 alphabet positions (6 bits) * into output bytes. */ private static final byte[] ENCODE = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', ]; /** * Lookup table for turning Base64 alphabet positions (6 bits) * into output bytes. */ private static final byte[] ENCODE_WEBSAFE = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', ]; final private byte[] tail; /* package */ int tailLen; private int count; final public boolean do_padding; final public boolean do_newline; final public boolean do_cr; final private byte[] alphabet; public Encoder(int flags, byte[] output) { this.output = output; do_padding = (flags & NO_PADDING) == 0; do_newline = (flags & NO_WRAP) == 0; do_cr = (flags & CRLF) != 0; alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; tail = new byte[2]; tailLen = 0; count = do_newline ? LINE_GROUPS : -1; } /** * @return an overestimate for the number of bytes {@code * len} bytes could encode to. */ public int maxOutputSize(int len) { return len * 8/5 + 10; } public boolean process(byte[] input, int offset, int len, boolean finish) { // Using local variables makes the encoder about 9% faster. final byte[] alphabet = this.alphabet; final byte[] output = this.output; int op = 0; int count = this.count; int p = offset; len += offset; int v = -1; // First we need to concatenate the tail of the previous call // with any input bytes available now and see if we can empty // the tail. switch (tailLen) { case 0: // There was no tail. break; case 1: if (p+2 <= len) { // A 1-byte tail with at least 2 bytes of // input available now. v = ((tail[0] & 0xff) << 16) | ((input[p++] & 0xff) << 8) | (input[p++] & 0xff); tailLen = 0; }; break; case 2: if (p+1 <= len) { // A 2-byte tail with at least 1 byte of input. v = ((tail[0] & 0xff) << 16) | ((tail[1] & 0xff) << 8) | (input[p++] & 0xff); tailLen = 0; } break; } if (v != -1) { output[op++] = alphabet[(v >> 18) & 0x3f]; output[op++] = alphabet[(v >> 12) & 0x3f]; output[op++] = alphabet[(v >> 6) & 0x3f]; output[op++] = alphabet[v & 0x3f]; if (--count == 0) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; count = LINE_GROUPS; } } // At this point either there is no tail, or there are fewer // than 3 bytes of input available. // The main loop, turning 3 input bytes into 4 output bytes on // each iteration. while (p+3 <= len) { v = ((input[p] & 0xff) << 16) | ((input[p+1] & 0xff) << 8) | (input[p+2] & 0xff); output[op] = alphabet[(v >> 18) & 0x3f]; output[op+1] = alphabet[(v >> 12) & 0x3f]; output[op+2] = alphabet[(v >> 6) & 0x3f]; output[op+3] = alphabet[v & 0x3f]; p += 3; op += 4; if (--count == 0) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; count = LINE_GROUPS; } } if (finish) { // Finish up the tail of the input. Note that we need to // consume any bytes in tail before any bytes // remaining in input; there should be at most two bytes // total. if (p-tailLen == len-1) { int t = 0; v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; tailLen -= t; output[op++] = alphabet[(v >> 6) & 0x3f]; output[op++] = alphabet[v & 0x3f]; if (do_padding) { output[op++] = '='; output[op++] = '='; } if (do_newline) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; } } else if (p-tailLen == len-2) { int t = 0; v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); tailLen -= t; output[op++] = alphabet[(v >> 12) & 0x3f]; output[op++] = alphabet[(v >> 6) & 0x3f]; output[op++] = alphabet[v & 0x3f]; if (do_padding) { output[op++] = '='; } if (do_newline) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; } } else if (do_newline && op > 0 && count != LINE_GROUPS) { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; } assert tailLen == 0; assert p == len; } else { // Save the leftovers in tail to be consumed on the next // call to encodeInternal. if (p == len-1) { tail[tailLen++] = input[p]; } else if (p == len-2) { tail[tailLen++] = input[p]; tail[tailLen++] = input[p+1]; } } this.op = op; this.count = count; return true; } } private Base64() { } // don't instantiate } // see https://discuss.gradle.org/t/build-gradle-cant-find-task-class-defined-in-an-apply-from-external-script/5836/2 ext.SendAnalyticsTask = SendAnalyticsTask