• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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