• 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 com.android.prefs.AndroidLocation;
20 import com.android.prefs.AndroidLocation.AndroidLocationException;
21 
22 import org.eclipse.jface.preference.PreferenceStore;
23 import org.eclipse.swt.SWT;
24 import org.eclipse.swt.events.SelectionAdapter;
25 import org.eclipse.swt.events.SelectionEvent;
26 import org.eclipse.swt.graphics.Color;
27 import org.eclipse.swt.graphics.Font;
28 import org.eclipse.swt.graphics.FontData;
29 import org.eclipse.swt.graphics.Point;
30 import org.eclipse.swt.graphics.Rectangle;
31 import org.eclipse.swt.layout.GridData;
32 import org.eclipse.swt.layout.GridLayout;
33 import org.eclipse.swt.program.Program;
34 import org.eclipse.swt.widgets.Button;
35 import org.eclipse.swt.widgets.Display;
36 import org.eclipse.swt.widgets.Label;
37 import org.eclipse.swt.widgets.Link;
38 import org.eclipse.swt.widgets.Shell;
39 
40 import java.io.File;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.net.HttpURLConnection;
44 import java.net.URL;
45 import java.net.URLEncoder;
46 import java.util.Random;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49 
50 /** Utility class to send "ping" usage reports to the server. */
51 public class SdkStatsService {
52 
53     /** Minimum interval between ping, in milliseconds. */
54     private static final long PING_INTERVAL_MSEC = 86400 * 1000;  // 1 day
55 
56     /* Text strings displayed in the opt-out dialog. */
57     private static final String WINDOW_TITLE_TEXT =
58         "Android SDK";
59 
60     private static final String HEADER_TEXT =
61         "Thanks for using the Android SDK!";
62 
63     private static final String NOTICE_TEXT =
64         "We know you just want to get started but please read this first.";
65 
66     /** Used in the preference pane (PrefsDialog) as well. */
67     public static final String BODY_TEXT =
68         "By choosing to send certain usage statistics to Google, you can " +
69         "help us improve the Android SDK.  These usage statistics let us " +
70         "measure things like active usage of the SDK and let us know things " +
71         "like which versions of the SDK are in use and which tools are the " +
72         "most popular with developers.  This limited data is not associated " +
73         "with personal information about you, is examined on an aggregate " +
74         "basis, and is maintained in accordance with the " +
75         "<a href=\"http://www.google.com/intl/en/privacy.html\">Google " +
76         "Privacy Policy</a>.";
77 
78     /** Used in the preference pane (PrefsDialog) as well. */
79     public static final String CHECKBOX_TEXT =
80         "Send usage statistics to Google.";
81 
82     private static final String FOOTER_TEXT =
83         "If you later decide to change this setting, you can do so in the " +
84         "\"ddms\" tool under \"File\" > \"Preferences\" > \"Usage Stats\".";
85 
86     private static final String BUTTON_TEXT =
87         "   Proceed   ";
88 
89     /** List of Linux browser commands to try, in order (see openUrl). */
90     private static final String[] LINUX_BROWSERS = new String[] {
91         "firefox -remote openurl(%URL%,new-window)",  // $NON-NLS-1$ running FF
92         "mozilla -remote openurl(%URL%,new-window)",  // $NON-NLS-1$ running Moz
93         "firefox %URL%",                              // $NON-NLS-1$ new FF
94         "mozilla %URL%",                              // $NON-NLS-1$ new Moz
95         "kfmclient openURL %URL%",                    // $NON-NLS-1$ Konqueror
96         "opera -newwindow %URL%",                     // $NON-NLS-1$ Opera
97     };
98 
99     public final static String PING_OPT_IN = "pingOptIn"; //$NON-NLS-1$
100     public final static String PING_TIME = "pingTime"; //$NON-NLS-1$
101     public final static String PING_ID = "pingId"; //$NON-NLS-1$
102 
103 
104     private static PreferenceStore sPrefStore;
105 
106     /**
107      * Send a "ping" to the Google toolbar server, if enough time has
108      * elapsed since the last ping, and if the user has not opted out.
109      * If this is the first time, notify the user and offer an opt-out.
110      * Note: UI operations (if any) are synchronous, but the actual ping
111      * (if any) is sent in a <i>non-daemon</i> background thread.
112      *
113      * @param app name to report in the ping
114      * @param version to report in the ping
115      * @param display an optional {@link Display} object to use, or null, if a new one should be
116      * created.
117      */
ping(final String app, final String version, final Display display)118     public static void ping(final String app, final String version, final Display display) {
119         // Unique, randomly assigned ID for this installation.
120         PreferenceStore prefs = getPreferenceStore();
121         if (prefs != null) {
122             if (prefs.contains(PING_ID) == false) {
123                 // First time: make up a new ID.  TODO: Use something more random?
124                 prefs.setValue(PING_ID, new Random().nextLong());
125 
126                 // ask the user whether he/she wants to opt-out.
127                 // This will call doPing in the Display thread after the dialog closes.
128                 getUserPermissionAndPing(app, version, prefs, display);
129             } else {
130                 doPing(app, version, prefs);
131             }
132         }
133     }
134 
135     /**
136      * Returns the DDMS {@link PreferenceStore}.
137      */
getPreferenceStore()138     public static synchronized PreferenceStore getPreferenceStore() {
139         if (sPrefStore == null) {
140             // get the location of the preferences
141             String homeDir = null;
142             try {
143                 homeDir = AndroidLocation.getFolder();
144             } catch (AndroidLocationException e1) {
145                 // pass, we'll do a dummy store since homeDir is null
146             }
147 
148             if (homeDir != null) {
149                 String rcFileName = homeDir + "ddms.cfg"; //$NON-NLS-1$
150 
151                 // also look for an old pref file in the previous location
152                 String oldPrefPath = System.getProperty("user.home") //$NON-NLS-1$
153                     + File.separator + ".ddmsrc"; //$NON-NLS-1$
154                 File oldPrefFile = new File(oldPrefPath);
155                 if (oldPrefFile.isFile()) {
156                     try {
157                         PreferenceStore oldStore = new PreferenceStore(oldPrefPath);
158                         oldStore.load();
159 
160                         oldStore.save(new FileOutputStream(rcFileName), "");
161                         oldPrefFile.delete();
162 
163                         PreferenceStore newStore = new PreferenceStore(rcFileName);
164                         newStore.load();
165                         sPrefStore = newStore;
166                     } catch (IOException e) {
167                         // create a new empty store.
168                         sPrefStore = new PreferenceStore(rcFileName);
169                     }
170                 } else {
171                     sPrefStore = new PreferenceStore(rcFileName);
172 
173                     try {
174                         sPrefStore.load();
175                     } catch (IOException e) {
176                         System.err.println("Error Loading Preferences");
177                     }
178                 }
179             } else {
180                 sPrefStore = new PreferenceStore();
181             }
182         }
183 
184         return sPrefStore;
185     }
186 
187     /**
188      * Pings the usage stats server, as long as the prefs contain the opt-in boolean
189      * @param app name to report in the ping
190      * @param version to report in the ping
191      * @param prefs the preference store where the opt-in value and ping times are store
192      */
doPing(final String app, String version, PreferenceStore prefs)193     private static void doPing(final String app, String version, PreferenceStore prefs) {
194         // Validate the application and version input.
195         final String normalVersion = normalizeVersion(app, version);
196 
197         // If the user has not opted in, do nothing and quietly return.
198         if (!prefs.getBoolean(PING_OPT_IN)) {
199             // user opted out.
200             return;
201         }
202 
203         // If the last ping *for this app* was too recent, do nothing.
204         String timePref = PING_TIME + "." + app;  // $NON-NLS-1$
205         long now = System.currentTimeMillis();
206         long then = prefs.getLong(timePref);
207         if (now - then < PING_INTERVAL_MSEC) {
208             // too soon after a ping.
209             return;
210         }
211 
212         // Record the time of the attempt, whether or not it succeeds.
213         prefs.setValue(timePref, now);
214         try {
215             prefs.save();
216         }
217         catch (IOException ioe) {
218         }
219 
220         // Send the ping itself in the background (don't block if the
221         // network is down or slow or confused).
222         final long id = prefs.getLong(PING_ID);
223         new Thread() {
224             @Override
225             public void run() {
226                 try {
227                     actuallySendPing(app, normalVersion, id);
228                 } catch (IOException e) {
229                     e.printStackTrace();
230                 }
231             }
232         }.start();
233     }
234 
235 
236     /**
237      * Unconditionally send a "ping" request to the Google toolbar server.
238      *
239      * @param app name to report in the ping
240      * @param version to report in the ping (dotted numbers, no more than four)
241      * @param id of the local installation
242      * @throws IOException if the ping failed
243      */
244     @SuppressWarnings("deprecation")
actuallySendPing(String app, String version, long id)245     private static void actuallySendPing(String app, String version, long id)
246                 throws IOException {
247         // Detect and report the host OS.
248         String os = System.getProperty("os.name");          // $NON-NLS-1$
249         if (os.startsWith("Mac OS")) {                      // $NON-NLS-1$
250             os = "mac";                                     // $NON-NLS-1$
251             String osVers = getVersion();
252             if (osVers != null) {
253                 os = os + "-" + osVers;                     // $NON-NLS-1$
254             }
255         } else if (os.startsWith("Windows")) {              // $NON-NLS-1$
256             os = "win";                                     // $NON-NLS-1$
257             String osVers = getVersion();
258             if (osVers != null) {
259                 os = os + "-" + osVers;                     // $NON-NLS-1$
260             }
261         } else if (os.startsWith("Linux")) {                // $NON-NLS-1$
262             os = "linux";                                   // $NON-NLS-1$
263         } else {
264             // Unknown -- surprising -- send it verbatim so we can see it.
265             os = URLEncoder.encode(os);
266         }
267 
268         // Include the application's name as part of the as= value.
269         // Share the user ID for all apps, to allow unified activity reports.
270 
271         URL url = new URL(
272             "http",                                         // $NON-NLS-1$
273             "tools.google.com",                             // $NON-NLS-1$
274             "/service/update?as=androidsdk_" + app +        // $NON-NLS-1$
275                 "&id=" + Long.toHexString(id) +             // $NON-NLS-1$
276                 "&version=" + version +                     // $NON-NLS-1$
277                 "&os=" + os);                               // $NON-NLS-1$
278 
279         // Discard the actual response, but make sure it reads OK
280         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
281 
282         // Believe it or not, a 404 response indicates success:
283         // the ping was logged, but no update is configured.
284         if (conn.getResponseCode() != HttpURLConnection.HTTP_OK &&
285             conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) {
286             throw new IOException(
287                 conn.getResponseMessage() + ": " + url);    // $NON-NLS-1$
288         }
289     }
290 
291     /**
292      * Returns the version of the os if it is defined as X.Y, or null otherwise.
293      * <p/>
294      * Example of returned versions can be found at http://lopica.sourceforge.net/os.html
295      * <p/>
296      * This method removes any exiting micro versions.
297      */
getVersion()298     private static String getVersion() {
299         Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); // $NON-NLS-1$
300         String osVers = System.getProperty("os.version"); // $NON-NLS-1$
301         Matcher m = p.matcher(osVers);
302         if (m.matches()) {
303             return m.group(1) + "." + m.group(2);         // $NON-NLS-1$
304         }
305 
306         return null;
307     }
308 
309     /**
310      * Prompt the user for whether they want to opt out of reporting, and then calls
311      * {@link #doPing(String, String, PreferenceStore)}
312      */
getUserPermissionAndPing(final String app, final String version, final PreferenceStore prefs, Display display)313     private static void getUserPermissionAndPing(final String app, final String version,
314             final PreferenceStore prefs, Display display) {
315         boolean dispose = false;
316         if (display == null) {
317             display = new Display();
318             dispose = true;
319         }
320 
321         final Display currentDisplay = display;
322         final boolean disposeDisplay = dispose;
323 
324         display.asyncExec(new Runnable() {
325             public void run() {
326                 // Whether the user gave permission (size-1 array for writing to).
327                 // Initialize to false, set when the user clicks the button.
328                 final boolean[] permission = new boolean[] { false };
329 
330 
331                 final Shell shell = new Shell(currentDisplay, SWT.TITLE | SWT.BORDER);
332                 shell.setText(WINDOW_TITLE_TEXT);
333                 shell.setLayout(new GridLayout(1, false)); // 1 column
334 
335                 // Take the default font and scale it up for the title.
336                 final Label title = new Label(shell, SWT.CENTER | SWT.WRAP);
337                 final FontData[] fontdata = title.getFont().getFontData();
338                 for (int i = 0; i < fontdata.length; i++) {
339                     fontdata[i].setHeight(fontdata[i].getHeight() * 4 / 3);
340                 }
341                 title.setFont(new Font(currentDisplay, fontdata));
342                 title.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
343                 title.setText(HEADER_TEXT);
344 
345                 final Label notice = new Label(shell, SWT.WRAP);
346                 notice.setFont(title.getFont());
347                 notice.setForeground(new Color(currentDisplay, 255, 0, 0));
348                 notice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
349                 notice.setText(NOTICE_TEXT);
350 
351                 final Link text = new Link(shell, SWT.WRAP);
352                 text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
353                 text.setText(BODY_TEXT);
354                 text.addSelectionListener(new SelectionAdapter() {
355                     @Override
356                     public void widgetSelected(SelectionEvent event) {
357                         openUrl(event.text);
358                     }
359                 });
360 
361                 final Button checkbox = new Button(shell, SWT.CHECK);
362                 checkbox.setSelection(true); // Opt-in by default.
363                 checkbox.setText(CHECKBOX_TEXT);
364 
365                 final Link footer = new Link(shell, SWT.WRAP);
366                 footer.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
367                 footer.setText(FOOTER_TEXT);
368 
369                 final Button button = new Button(shell, SWT.PUSH);
370                 button.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
371                 button.setText(BUTTON_TEXT);
372                 button.addSelectionListener(new SelectionAdapter() {
373                     @Override
374                     public void widgetSelected(SelectionEvent event) {
375                         permission[0] = checkbox.getSelection();
376                         shell.close();
377                     }
378                 });
379 
380                 // Size the window to a fixed width, as high as necessary,
381                 // centered.
382                 final Point size = shell.computeSize(450, SWT.DEFAULT, true);
383                 final Rectangle screen = currentDisplay.getClientArea();
384                 shell.setBounds(screen.x + screen.width / 2 - size.x / 2, screen.y + screen.height
385                         / 2 - size.y / 2, size.x, size.y);
386 
387                 shell.open();
388 
389                 while (!shell.isDisposed()) {
390                     if (!currentDisplay.readAndDispatch())
391                         currentDisplay.sleep();
392                 }
393 
394                 // the dialog has closed, take care of storing the user preference
395                 // and do the ping (in a different thread)
396                 prefs.setValue(PING_OPT_IN, permission[0]);
397                 try {
398                     prefs.save();
399                     doPing(app, version, prefs);
400                 }
401                 catch (IOException ioe) {
402                 }
403 
404 
405                 if (disposeDisplay) {
406                     currentDisplay.dispose();
407                 }
408             }
409         });
410     }
411 
412     /**
413      * Open a URL in an external browser.
414      * @param url to open - MUST be sanitized and properly formed!
415      */
openUrl(final String url)416     public static void openUrl(final String url) {
417         // TODO: consider using something like BrowserLauncher2
418         // (http://browserlaunch2.sourceforge.net/) instead of these hacks.
419 
420         // SWT's Program.launch() should work on Mac, Windows, and GNOME
421         // (because the OS shell knows how to launch a default browser).
422         if (!Program.launch(url)) {
423             // Must be Linux non-GNOME (or something else broke).
424             // Try a few Linux browser commands in the background.
425             new Thread() {
426                 @Override
427                 public void run() {
428                     for (String cmd : LINUX_BROWSERS) {
429                         cmd = cmd.replaceAll("%URL%", url);  // $NON-NLS-1$
430                         try {
431                             Process proc = Runtime.getRuntime().exec(cmd);
432                             if (proc.waitFor() == 0) break;  // Success!
433                         } catch (InterruptedException e) {
434                             // Should never happen!
435                             throw new RuntimeException(e);
436                         } catch (IOException e) {
437                             // Swallow the exception and try the next browser.
438                         }
439                     }
440 
441                     // TODO: Pop up some sort of error here?
442                     // (We're in a new thread; can't use the existing Display.)
443                 }
444             }.start();
445         }
446     }
447 
448     /**
449      * Validate the supplied application version, and normalize the version.
450      * @param app to report
451      * @param version supplied by caller
452      * @return normalized dotted quad version
453      */
normalizeVersion(String app, String version)454     private static String normalizeVersion(String app, String version) {
455         // Application name must contain only word characters (no punctuaation)
456         if (!app.matches("\\w+")) {
457             throw new IllegalArgumentException("Bad app name: " + app);
458         }
459 
460         // Version must be between 1 and 4 dotted numbers
461         String[] numbers = version.split("\\.");
462         if (numbers.length > 4) {
463             throw new IllegalArgumentException("Bad version: " + version);
464         }
465         for (String part: numbers) {
466             if (!part.matches("\\d+")) {
467                 throw new IllegalArgumentException("Bad version: " + version);
468             }
469         }
470 
471         // Always output 4 numbers, even if fewer were supplied (pad with .0)
472         StringBuffer normal = new StringBuffer(numbers[0]);
473         for (int i = 1; i < 4; i++) {
474             normal.append(".").append(i < numbers.length ? numbers[i] : "0");
475         }
476         return normal.toString();
477     }
478 }
479