/*
* Copyright 2015 Realm 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 io.realm.transformer;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.Set;
// Asynchronously submits build information to Realm when the annotation
// processor is running
//
// 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.
public class RealmAnalytics {
private static RealmAnalytics instance;
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
= "{\n"
+ " \"event\": \"%EVENT%\",\n"
+ " \"properties\": {\n"
+ " \"token\": \"%TOKEN%\",\n"
+ " \"distinct_id\": \"%USER_ID%\",\n"
+ " \"Anonymized MAC Address\": \"%USER_ID%\",\n"
+ " \"Anonymized Bundle ID\": \"%APP_ID%\",\n"
+ " \"Binding\": \"java\",\n"
+ " \"Language\": \"%LANGUAGE%\",\n"
+ " \"Sync Version\": %SYNC_VERSION%,\n"
+ " \"Realm Version\": \"%REALM_VERSION%\",\n"
+ " \"Host OS Type\": \"%OS_TYPE%\",\n"
+ " \"Host OS Version\": \"%OS_VERSION%\",\n"
+ " \"Target OS Type\": \"android\",\n"
+ " \"Target OS Version\": \"%TARGET_SDK%\",\n"
+ " \"Target OS Minimum Version\": \"%MIN_SDK%\"\n"
+ " }\n"
+ "}";
// The list of packages the model classes reside in
private Set<String> packages;
private boolean usesKotlin;
private boolean usesSync;
private String targetSdk;
private String minSdk;
public RealmAnalytics(Set<String> packages, boolean usesKotlin, boolean usesSync, String targetSdk, String minSdk) {
this.packages = packages;
this.usesKotlin = usesKotlin;
this.usesSync = usesSync;
this.targetSdk = targetSdk;
this.minSdk = minSdk;
}
private void send() {
try {
URL url = getUrl();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
connection.getResponseCode();
} catch (Exception ignored) {
}
}
public void execute() {
Thread backgroundThread = new Thread(new Runnable() {
@Override
public void run() {
send();
}
});
backgroundThread.start();
try {
backgroundThread.join(CONNECT_TIMEOUT + READ_TIMEOUT);
} catch (InterruptedException ignored) {
// We ignore this exception on purpose not to break the build system if this class fails
} catch (IllegalArgumentException ignored) {
// We ignore this exception on purpose not to break the build system if this class fails
}
}
public URL getUrl() throws
MalformedURLException,
SocketException,
NoSuchAlgorithmException,
UnsupportedEncodingException {
return new URL(ADDRESS_PREFIX + Utils.base64Encode(generateJson()) + ADDRESS_SUFFIX);
}
public String generateJson() throws SocketException, NoSuchAlgorithmException {
return JSON_TEMPLATE
.replaceAll("%EVENT%", EVENT_NAME)
.replaceAll("%TOKEN%", TOKEN)
.replaceAll("%USER_ID%", ComputerIdentifierGenerator.get())
.replaceAll("%APP_ID%", getAnonymousAppId())
.replaceAll("%LANGUAGE%", usesKotlin ? "kotlin" : "java")
.replaceAll("%SYNC_VERSION%", usesSync ? "\"" + Version.SYNC_VERSION + "\"": "null")
.replaceAll("%REALM_VERSION%", Version.VERSION)
.replaceAll("%OS_TYPE%", System.getProperty("os.name"))
.replaceAll("%OS_VERSION%", System.getProperty("os.version"))
.replaceAll("%TARGET_SDK%", targetSdk)
.replaceAll("%MIN_SDK%", minSdk);
}
/**
* Computes an anonymous app/library id from the packages containing RealmObject classes
* @return the anonymous app/library id
* @throws NoSuchAlgorithmException
*/
public String getAnonymousAppId() throws NoSuchAlgorithmException {
StringBuilder stringBuilder = new StringBuilder();
for (String modelPackage : packages) {
stringBuilder.append(modelPackage).append(":");
}
byte[] packagesBytes = stringBuilder.toString().getBytes();
return Utils.hexStringify(Utils.sha256Hash(packagesBytes));
}
}