1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.sdkstats; 18 19 import org.eclipse.swt.widgets.Display; 20 import org.eclipse.swt.widgets.Shell; 21 22 import java.io.IOException; 23 import java.net.HttpURLConnection; 24 import java.net.URL; 25 import java.net.URLEncoder; 26 import java.util.Locale; 27 import java.util.regex.Matcher; 28 import java.util.regex.Pattern; 29 30 /** Utility class to send "ping" usage reports to the server. */ 31 public class SdkStatsService { 32 33 protected static final String SYS_PROP_OS_ARCH = "os.arch"; //$NON-NLS-1$ 34 protected static final String SYS_PROP_JAVA_VERSION = "java.version"; //$NON-NLS-1$ 35 protected static final String SYS_PROP_OS_VERSION = "os.version"; //$NON-NLS-1$ 36 protected static final String SYS_PROP_OS_NAME = "os.name"; //$NON-NLS-1$ 37 38 /** Minimum interval between ping, in milliseconds. */ 39 private static final long PING_INTERVAL_MSEC = 86400 * 1000; // 1 day 40 41 private DdmsPreferenceStore mStore = new DdmsPreferenceStore(); 42 SdkStatsService()43 public SdkStatsService() { 44 } 45 46 /** 47 * Send a "ping" to the Google toolbar server, if enough time has 48 * elapsed since the last ping, and if the user has not opted out.<br> 49 * 50 * The ping will not be sent if the user opt out dialog has not been shown yet. 51 * Use {@link #checkUserPermissionForPing(Shell)} to display the dialog requesting 52 * user permissions.<br> 53 * 54 * Note: The actual ping (if any) is sent in a <i>non-daemon</i> background thread. 55 * 56 * @param app name to report in the ping 57 * @param version to report in the ping 58 */ ping(String app, String version)59 public void ping(String app, String version) { 60 doPing(app, version); 61 } 62 63 /** 64 * Display a dialog to the user providing information about the ping service, 65 * and whether they'd like to opt-out of it. 66 * 67 * Once the dialog has been shown, it sets a preference internally indicating 68 * that the user has viewed this dialog. 69 */ checkUserPermissionForPing(Shell parent)70 public void checkUserPermissionForPing(Shell parent) { 71 if (!mStore.hasPingId()) { 72 askUserPermissionForPing(parent); 73 mStore.generateNewPingId(); 74 } 75 } 76 77 /** 78 * Prompt the user for whether they want to opt out of reporting, and save the user 79 * input in preferences. 80 */ askUserPermissionForPing(final Shell parent)81 private void askUserPermissionForPing(final Shell parent) { 82 final Display display = parent.getDisplay(); 83 display.syncExec(new Runnable() { 84 @Override 85 public void run() { 86 SdkStatsPermissionDialog dialog = new SdkStatsPermissionDialog(parent); 87 dialog.open(); 88 mStore.setPingOptIn(dialog.getPingUserPreference()); 89 } 90 }); 91 } 92 93 // ------- 94 95 /** 96 * Pings the usage stats server, as long as the prefs contain the opt-in boolean 97 * 98 * @param app name to report in the ping 99 * @param version to report in the ping 100 */ doPing(final String app, String version)101 private void doPing(final String app, String version) { 102 // Validate the application and version input. 103 final String normalVersion = normalizeVersion(app, version); 104 105 // If the user has not opted in, do nothing and quietly return. 106 if (!mStore.isPingOptIn()) { 107 // user opted out. 108 return; 109 } 110 111 // If the last ping *for this app* was too recent, do nothing. 112 long now = System.currentTimeMillis(); 113 long then = mStore.getPingTime(app); 114 if (now - then < PING_INTERVAL_MSEC) { 115 // too soon after a ping. 116 return; 117 } 118 119 // Record the time of the attempt, whether or not it succeeds. 120 mStore.setPingTime(app, now); 121 122 // Send the ping itself in the background (don't block if the 123 // network is down or slow or confused). 124 final long id = mStore.getPingId(); 125 new Thread() { 126 @Override 127 public void run() { 128 try { 129 actuallySendPing(app, normalVersion, id); 130 } catch (IOException e) { 131 e.printStackTrace(); 132 } 133 } 134 }.start(); 135 } 136 137 138 /** 139 * Unconditionally send a "ping" request to the Google toolbar server. 140 * 141 * @param app name to report in the ping 142 * @param version to report in the ping (dotted numbers, no more than four) 143 * @param id of the local installation 144 * @throws IOException if the ping failed 145 */ actuallySendPing(String app, String version, long id)146 private void actuallySendPing(String app, String version, long id) 147 throws IOException { 148 String osName = URLEncoder.encode(getOsName(), "UTF-8"); 149 String osArch = URLEncoder.encode(getOsArch(), "UTF-8"); 150 String jvmArch = URLEncoder.encode(getJvmInfo(), "UTF-8"); 151 152 // Include the application's name as part of the as= value. 153 // Share the user ID for all apps, to allow unified activity reports. 154 155 URL url = new URL( 156 "http", //$NON-NLS-1$ 157 "tools.google.com", //$NON-NLS-1$ 158 "/service/update?as=androidsdk_" + app + //$NON-NLS-1$ 159 "&id=" + Long.toHexString(id) + //$NON-NLS-1$ 160 "&version=" + version + //$NON-NLS-1$ 161 "&os=" + osName + //$NON-NLS-1$ 162 "&osa=" + osArch + //$NON-NLS-1$ 163 "&vma=" + jvmArch); //$NON-NLS-1$ 164 165 // Discard the actual response, but make sure it reads OK 166 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 167 168 // Believe it or not, a 404 response indicates success: 169 // the ping was logged, but no update is configured. 170 if (conn.getResponseCode() != HttpURLConnection.HTTP_OK && 171 conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) { 172 throw new IOException( 173 conn.getResponseMessage() + ": " + url); //$NON-NLS-1$ 174 } 175 } 176 177 /** 178 * Detects and reports the host OS: "linux", "win" or "mac". 179 * For Windows and Mac also append the version, so for example 180 * Win XP will return win-5.1. 181 */ getOsName()182 protected String getOsName() { // made protected for testing 183 String os = getSystemProperty(SYS_PROP_OS_NAME); 184 185 if (os == null || os.length() == 0) { 186 return "unknown"; //$NON-NLS-1$ 187 } 188 189 String os2 = os.toLowerCase(Locale.US); 190 191 if (os2.startsWith("mac")) { //$NON-NLS-1$ 192 os = "mac"; //$NON-NLS-1$ 193 String osVers = getOsVersion(); 194 if (osVers != null) { 195 os = os + '-' + osVers; 196 } 197 } else if (os2.startsWith("win")) { //$NON-NLS-1$ 198 os = "win"; //$NON-NLS-1$ 199 String osVers = getOsVersion(); 200 if (osVers != null) { 201 os = os + '-' + osVers; 202 } 203 } else if (os2.startsWith("linux")) { //$NON-NLS-1$ 204 os = "linux"; //$NON-NLS-1$ 205 206 } else if (os.length() > 32) { 207 // Unknown -- send it verbatim so we can see it 208 // but protect against arbitrarily long values 209 os = os.substring(0, 32); 210 } 211 return os; 212 } 213 214 /** 215 * Detects and returns the OS architecture: x86, x86_64, ppc. 216 * This may differ or be equal to the JVM architecture in the sense that 217 * a 64-bit OS can run a 32-bit JVM. 218 */ getOsArch()219 protected String getOsArch() { // made protected for testing 220 String arch = getJvmArch(); 221 222 if ("x86_64".equals(arch)) { //$NON-NLS-1$ 223 // This is a simple case: the JVM runs in 64-bit so the 224 // OS must be a 64-bit one. 225 return arch; 226 227 } else if ("x86".equals(arch)) { //$NON-NLS-1$ 228 // This is the misleading case: the JVM is 32-bit but the OS 229 // might be either 32 or 64. We can't tell just from this 230 // property. 231 // Macs are always on 64-bit, so we just need to figure it 232 // out for Windows and Linux. 233 234 String os = getOsName(); 235 if (os.startsWith("win")) { //$NON-NLS-1$ 236 // When WOW64 emulates a 32-bit environment under a 64-bit OS, 237 // it sets PROCESSOR_ARCHITEW6432 to AMD64 or IA64 accordingly. 238 // Ref: http://msdn.microsoft.com/en-us/library/aa384274(v=vs.85).aspx 239 240 String w6432 = getSystemEnv("PROCESSOR_ARCHITEW6432"); //$NON-NLS-1$ 241 if (w6432 != null && w6432.indexOf("64") != -1) { //$NON-NLS-1$ 242 return "x86_64"; //$NON-NLS-1$ 243 } 244 } else if (os.startsWith("linux")) { //$NON-NLS-1$ 245 // Let's try the obvious. This works in Ubuntu and Debian 246 String s = getSystemEnv("HOSTTYPE"); //$NON-NLS-1$ 247 248 s = sanitizeOsArch(s); 249 if (s.indexOf("86") != -1) { //$NON-NLS-1$ 250 arch = s; 251 } 252 } 253 } 254 255 return arch; 256 } 257 258 /** 259 * Returns the version of the OS version if it is defined as X.Y, or null otherwise. 260 * <p/> 261 * Example of returned versions can be found at http://lopica.sourceforge.net/os.html 262 * <p/> 263 * This method removes any exiting micro versions. 264 * Returns null if the version doesn't match X.Y.Z. 265 */ getOsVersion()266 protected String getOsVersion() { // made protected for testing 267 Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$ 268 String osVers = getSystemProperty(SYS_PROP_OS_VERSION); 269 if (osVers != null && osVers.length() > 0) { 270 Matcher m = p.matcher(osVers); 271 if (m.matches()) { 272 return m.group(1) + '.' + m.group(2); 273 } 274 } 275 return null; 276 } 277 278 /** 279 * Detects and returns the JVM info: version + architecture. 280 * Examples: 1.4-ppc, 1.6-x86, 1.7-x86_64 281 */ getJvmInfo()282 protected String getJvmInfo() { // made protected for testing 283 return getJvmVersion() + '-' + getJvmArch(); 284 } 285 286 /** 287 * Returns the major.minor Java version. 288 * <p/> 289 * The "java.version" property returns something like "1.6.0_20" 290 * of which we want to return "1.6". 291 */ getJvmVersion()292 protected String getJvmVersion() { // made protected for testing 293 String version = getSystemProperty(SYS_PROP_JAVA_VERSION); 294 295 if (version == null || version.length() == 0) { 296 return "unknown"; //$NON-NLS-1$ 297 } 298 299 Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$ 300 Matcher m = p.matcher(version); 301 if (m.matches()) { 302 return m.group(1) + '.' + m.group(2); 303 } 304 305 // Unknown version. Send it as-is within a reasonable size limit. 306 if (version.length() > 8) { 307 version = version.substring(0, 8); 308 } 309 return version; 310 } 311 312 /** 313 * Detects and returns the JVM architecture. 314 * <p/> 315 * The HotSpot JVM has a private property for this, "sun.arch.data.model", 316 * which returns either "32" or "64". However it's not in any kind of spec. 317 * <p/> 318 * What we want is to know whether the JVM is running in 32-bit or 64-bit and 319 * the best indicator is to use the "os.arch" property. 320 * - On a 32-bit system, only a 32-bit JVM can run so it will be x86 or ppc.<br/> 321 * - On a 64-bit system, a 32-bit JVM will also return x86 since the OS needs 322 * to masquerade as a 32-bit OS for backward compatibility.<br/> 323 * - On a 64-bit system, a 64-bit JVM will properly return x86_64. 324 * <pre> 325 * JVM: Java 32-bit Java 64-bit 326 * Windows: x86 x86_64 327 * Linux: x86 x86_64 328 * Mac untested x86_64 329 * </pre> 330 */ getJvmArch()331 protected String getJvmArch() { // made protected for testing 332 String arch = getSystemProperty(SYS_PROP_OS_ARCH); 333 return sanitizeOsArch(arch); 334 } 335 sanitizeOsArch(String arch)336 private String sanitizeOsArch(String arch) { 337 if (arch == null || arch.length() == 0) { 338 return "unknown"; //$NON-NLS-1$ 339 } 340 341 if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$ 342 arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$ 343 arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$ 344 return "x86_64"; //$NON-NLS-1$ 345 } 346 347 if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) { //$NON-NLS-1$ 348 // Any variation of iX86 counts as x86 (i386, i486, i686). 349 return "x86"; //$NON-NLS-1$ 350 } 351 352 if (arch.equalsIgnoreCase("PowerPC")) { //$NON-NLS-1$ 353 return "ppc"; //$NON-NLS-1$ 354 } 355 356 // Unknown arch. Send it as-is but protect against arbitrarily long values. 357 if (arch.length() > 32) { 358 arch = arch.substring(0, 32); 359 } 360 return arch; 361 } 362 363 /** 364 * Validate the supplied application version, and normalize the version. 365 * @param app to report 366 * @param version supplied by caller 367 * @return normalized dotted quad version 368 */ normalizeVersion(String app, String version)369 private String normalizeVersion(String app, String version) { 370 // Application name must contain only word characters (no punctuation) 371 if (!app.matches("\\w+")) { //$NON-NLS-1$ 372 throw new IllegalArgumentException("Bad app name: " + app); //$NON-NLS-1$ 373 } 374 375 // Version must be between 1 and 4 dotted numbers 376 String[] numbers = version.split("\\."); //$NON-NLS-1$ 377 if (numbers.length > 4) { 378 throw new IllegalArgumentException("Bad version: " + version); //$NON-NLS-1$ 379 } 380 for (String part: numbers) { 381 if (!part.matches("\\d+")) { //$NON-NLS-1$ 382 throw new IllegalArgumentException("Bad version: " + version); //$NON-NLS-1$ 383 } 384 } 385 386 // Always output 4 numbers, even if fewer were supplied (pad with .0) 387 StringBuffer normal = new StringBuffer(numbers[0]); 388 for (int i = 1; i < 4; i++) { 389 normal.append('.').append(i < numbers.length ? numbers[i] : "0"); //$NON-NLS-1$ 390 } 391 return normal.toString(); 392 } 393 394 /** 395 * Calls {@link System#getProperty(String)}. 396 * Allows unit-test to override the return value. 397 * @see System#getProperty(String) 398 */ 399 protected String getSystemProperty(String name) { 400 return System.getProperty(name); 401 } 402 403 /** 404 * Calls {@link System#getenv(String)}. 405 * Allows unit-test to override the return value. 406 * @see System#getenv(String) 407 */ 408 protected String getSystemEnv(String name) { 409 return System.getenv(name); 410 } 411 } 412