blob: 5cccd0ff0583a1e7b0d494718aef73667c7b5b7e [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.stats;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class to send "ping" usage reports to the server.
* Imported from the android/tools/swt/sdkstats project, and simplified for usage in Studio.
* */
@SuppressWarnings("MethodMayBeStatic")
public class LegacySdkStatsService {
private static final Logger LOG = Logger.getInstance("#" + LegacySdkStatsService.class.getName());
protected static final String SYS_PROP_OS_ARCH = "os.arch"; //$NON-NLS-1$
protected static final String SYS_PROP_JAVA_VERSION = "java.version"; //$NON-NLS-1$
protected static final String SYS_PROP_OS_VERSION = "os.version"; //$NON-NLS-1$
protected static final String SYS_PROP_OS_NAME = "os.name"; //$NON-NLS-1$
/** Minimum interval between ping, in milliseconds. */
private static final long PING_INTERVAL_MSEC = 86400 * 1000; // 1 day
private static final boolean DEBUG = System.getenv("ANDROID_DEBUG_PING") != null; //$NON-NLS-1$
private DdmsPreferenceStore mStore = new DdmsPreferenceStore();
public LegacySdkStatsService() {
}
/**
* Send a "ping" to the Google toolbar server, if enough time has
* elapsed since the last ping, and if the user has not opted out.
* <p/>
* This is a simplified version of {@link #ping(String[])} that only
* sends an "application" name and a "version" string. See the explanation
* there for details.
*
* @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
* Valid characters are a-zA-Z0-9 only.
* @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.)
*/
public void ping(@NotNull String app, @NotNull String version) {
doPing(app, version, null);
}
// -------
/**
* Pings the usage stats server, as long as the prefs contain the opt-in boolean
*
* @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
* Will be normalized. Valid characters are a-zA-Z0-9 only.
* @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.)
* @param extras Extra key/value parameters to send. They are send as-is and must
* already be well suited and escaped using {@link java.net.URLEncoder#encode(String, String)}.
*/
protected void doPing(@NotNull String app,
@NotNull String version,
@Nullable final Map<String, String> extras) {
// Note: if you change the implementation here, you also need to change
// the overloaded SdkStatsServiceTest.doPing() used for testing.
// Validate the application and version input.
final String nApp = normalizeAppName(app);
final String nVersion = normalizeVersion(version);
// If the user has not opted in, do nothing and quietly return.
if (!mStore.isPingOptIn()) {
// user opted out.
return;
}
// If the last ping *for this app* was too recent, do nothing.
long now = System.currentTimeMillis();
long then = mStore.getPingTime(app);
if (now - then < PING_INTERVAL_MSEC) {
// too soon after a ping.
return;
}
// Record the time of the attempt, whether or not it succeeds.
mStore.setPingTime(app, now);
// Send the ping itself in the background (don't block if the
// network is down or slow or confused).
long id = mStore.getPingId();
if (id == 0) {
id = mStore.generateNewPingId();
}
try {
URL url = createPingUrl(nApp, nVersion, id, extras);
actuallySendPing(url);
} catch (Exception e) {
LOG.warn("AndroidSdk.SendPing failed", e);
}
}
/**
* Unconditionally send a "ping" request to the server.
*
* @param url The URL to send to the server.
* * @throws IOException if the ping failed
*/
private void actuallySendPing(URL url) throws IOException {
assert url != null;
if (DEBUG) {
LOG.debug("Ping: " + url.toString()); //$NON-NLS-1$
}
// Discard the actual response, but make sure it reads OK
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
int responseCode;
try {
responseCode = conn.getResponseCode();
}
catch (UnknownHostException e) {
responseCode = HttpURLConnection.HTTP_BAD_REQUEST;
}
// Believe it or not, a 404 response indicates success:
// the ping was logged, but no update is configured.
if (responseCode != HttpURLConnection.HTTP_OK &&
responseCode != HttpURLConnection.HTTP_NOT_FOUND) {
throw new IOException(conn.getResponseMessage() + ": " + url); //$NON-NLS-1$
}
}
/**
* Compute the ping URL to send the data to the server.
*
* @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
* Valid characters are a-zA-Z0-9 only.
* @param version The version string already formatted as a 4 dotted group (e.g. "1.2.3.4".)
* @param id of the local installation
* @param extras Extra key/value parameters to send. They are send as-is and must
* already be well suited and escaped using {@link java.net.URLEncoder#encode(String, String)}.
*/
protected URL createPingUrl(@NotNull String app,
@NotNull String version,
long id,
@Nullable Map<String, String> extras)
throws UnsupportedEncodingException, MalformedURLException {
String osName = URLEncoder.encode(getOsName().getOsFull(), "UTF-8"); //$NON-NLS-1$
String osArch = URLEncoder.encode(getOsArch(), "UTF-8"); //$NON-NLS-1$
String jvmArch = URLEncoder.encode(getJvmInfo(), "UTF-8"); //$NON-NLS-1$
// Include the application's name as part of the as= value.
// Share the user ID for all apps, to allow unified activity reports.
String extraStr = ""; //$NON-NLS-1$
if (extras != null && !extras.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : extras.entrySet()) {
sb.append('&').append(entry.getKey()).append('=').append(entry.getValue());
}
extraStr = sb.toString();
}
//noinspection UnnecessaryLocalVariable
URL url = new URL("http", //$NON-NLS-1$
"tools.google.com", //$NON-NLS-1$
"/service/update?as=androidsdk_" + app + //$NON-NLS-1$
"&id=" + Long.toHexString(id) + //$NON-NLS-1$
"&version=" + version + //$NON-NLS-1$
"&os=" + osName + //$NON-NLS-1$
"&osa=" + osArch + //$NON-NLS-1$
"&vma=" + jvmArch + //$NON-NLS-1$
extraStr);
return url;
}
/**
* Detects and reports the host OS: "linux", "win" or "mac".
* For Windows and Mac also append the version, so for example
* Win XP will return win-5.1.
*/
OsInfo getOsName() { // made protected for testing
String os = getSystemProperty(SYS_PROP_OS_NAME);
OsInfo info = new OsInfo();
if (os == null || os.length() == 0) {
return info.setOsName("unknown");
}
String os2 = os.toLowerCase(Locale.US);
String osVers = null;
if (os2.startsWith("mac")) {
os = "mac";
osVers = getOsVersion();
}
else if (os2.startsWith("win")) {
os = "win";
osVers = getOsVersion();
}
else if (os2.startsWith("linux")) {
os = "linux";
}
else if (os.length() > 32) {
// Unknown -- send it verbatim so we can see it
// but protect against arbitrarily long values
os = os.substring(0, 32);
}
info.setOsName(os);
info.setOsVersion(osVers);
return info;
}
/**
* Detects and returns the OS architecture: x86, x86_64, ppc.
* This may differ or be equal to the JVM architecture in the sense that
* a 64-bit OS can run a 32-bit JVM.
*/
String getOsArch() { // made protected for testing
String arch = getJvmArch();
if ("x86_64".equals(arch)) { //$NON-NLS-1$
// This is a simple case: the JVM runs in 64-bit so the
// OS must be a 64-bit one.
return arch;
}
else if ("x86".equals(arch)) { //$NON-NLS-1$
// This is the misleading case: the JVM is 32-bit but the OS
// might be either 32 or 64. We can't tell just from this
// property.
// Macs are always on 64-bit, so we just need to figure it
// out for Windows and Linux.
String os = getOsName().getOsName();
if (os.startsWith("win")) { //$NON-NLS-1$
// When WOW64 emulates a 32-bit environment under a 64-bit OS,
// it sets PROCESSOR_ARCHITEW6432 to AMD64 or IA64 accordingly.
// Ref: http://msdn.microsoft.com/en-us/library/aa384274(v=vs.85).aspx
String w6432 = getSystemEnv("PROCESSOR_ARCHITEW6432"); //$NON-NLS-1$
if (w6432 != null && w6432.contains("64")) { //$NON-NLS-1$
return "x86_64"; //$NON-NLS-1$
}
}
else if (os.startsWith("linux")) { //$NON-NLS-1$
// Let's try the obvious. This works in Ubuntu and Debian
String s = getSystemEnv("HOSTTYPE"); //$NON-NLS-1$
s = sanitizeOsArch(s);
if (s.contains("86")) { //$NON-NLS-1$
arch = s;
}
}
}
return arch;
}
/**
* Returns the version of the OS version if it is defined as X.Y, or null otherwise.
* <p/>
* Example of returned versions can be found at http://lopica.sourceforge.net/os.html
* <p/>
* This method removes any exiting micro versions.
* Returns null if the version doesn't match X.Y.Z.
*/
@Nullable
String getOsVersion() { // made protected for testing
Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$
String osVers = getSystemProperty(SYS_PROP_OS_VERSION);
if (osVers != null && osVers.length() > 0) {
Matcher m = p.matcher(osVers);
if (m.matches()) {
return m.group(1) + '.' + m.group(2);
}
}
return null;
}
/**
* Detects and returns the JVM info: version + architecture.
* Examples: 1.4-ppc, 1.6-x86, 1.7-x86_64
*/
String getJvmInfo() { // made protected for testing
return getJvmVersion() + '-' + getJvmArch();
}
/**
* Returns the major.minor Java version.
* <p/>
* The "java.version" property returns something like "1.6.0_20"
* of which we want to return "1.6".
*/
String getJvmVersion() { // made protected for testing
String version = getSystemProperty(SYS_PROP_JAVA_VERSION);
if (version == null || version.length() == 0) {
return "unknown"; //$NON-NLS-1$
}
Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$
Matcher m = p.matcher(version);
if (m.matches()) {
return m.group(1) + '.' + m.group(2);
}
// Unknown version. Send it as-is within a reasonable size limit.
if (version.length() > 8) {
version = version.substring(0, 8);
}
return version;
}
/**
* Detects and returns the JVM architecture.
* <p/>
* The HotSpot JVM has a private property for this, "sun.arch.data.model",
* which returns either "32" or "64". However it's not in any kind of spec.
* <p/>
* What we want is to know whether the JVM is running in 32-bit or 64-bit and
* the best indicator is to use the "os.arch" property.
* - On a 32-bit system, only a 32-bit JVM can run so it will be x86 or ppc.<br/>
* - On a 64-bit system, a 32-bit JVM will also return x86 since the OS needs
* to masquerade as a 32-bit OS for backward compatibility.<br/>
* - On a 64-bit system, a 64-bit JVM will properly return x86_64.
* <pre>
* JVM: Java 32-bit Java 64-bit
* Windows: x86 x86_64
* Linux: x86 x86_64
* Mac untested x86_64
* </pre>
*/
String getJvmArch() { // made protected for testing
String arch = getSystemProperty(SYS_PROP_OS_ARCH);
return sanitizeOsArch(arch);
}
private String sanitizeOsArch(String arch) {
if (arch == null || arch.length() == 0) {
return "unknown"; //$NON-NLS-1$
}
if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$
arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$
arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$
return "x86_64"; //$NON-NLS-1$
}
if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) { //$NON-NLS-1$
// Any variation of iX86 counts as x86 (i386, i486, i686).
return "x86"; //$NON-NLS-1$
}
if (arch.equalsIgnoreCase("PowerPC")) { //$NON-NLS-1$
return "ppc"; //$NON-NLS-1$
}
// Unknown arch. Send it as-is but protect against arbitrarily long values.
if (arch.length() > 32) {
arch = arch.substring(0, 32);
}
return arch;
}
/**
* Normalize the supplied application name.
*
* @param app to report
*/
protected String normalizeAppName(String app) {
// Filter out \W , non-word character: [^a-zA-Z_0-9]
String app2 = app.replaceAll("\\W", ""); //$NON-NLS-1$ //$NON-NLS-2$
if (app.length() == 0) {
throw new IllegalArgumentException("Bad app name: " + app); //$NON-NLS-1$
}
return app2;
}
/**
* Validate the supplied application version, and normalize the version.
*
* @param version supplied by caller
* @return normalized dotted quad version
*/
protected String normalizeVersion(String version) {
Pattern regex = Pattern.compile(
//1=major 2=minor 3=micro 4=build | 5=rc
"^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+)| +rc(\\d+))?"); //$NON-NLS-1$
Matcher m = regex.matcher(version);
if (m != null && m.lookingAt()) {
StringBuilder normal = new StringBuilder();
for (int i = 1; i <= 4; i++) {
int v = 0;
// If build is null but we have an rc, take that number instead as the 4th part.
if (i == 4 &&
i < m.groupCount() &&
m.group(i) == null &&
m.group(i + 1) != null) {
//noinspection AssignmentToForLoopParameter
i++;
}
if (m.group(i) != null) {
try {
v = Integer.parseInt(m.group(i));
} catch (Exception ignore) {
}
}
if (i > 1) {
normal.append('.');
}
normal.append(v);
}
return normal.toString();
}
throw new IllegalArgumentException("Bad version: " + version); //$NON-NLS-1$
}
/**
* Calls {@link System#getProperty(String)}.
* Allows unit-test to override the return value.
* @see System#getProperty(String)
*/
protected String getSystemProperty(String name) {
return System.getProperty(name);
}
/**
* Calls {@link System#getenv(String)}.
* Allows unit-test to override the return value.
* @see System#getenv(String)
*/
protected String getSystemEnv(String name) {
return System.getenv(name);
}
}