• 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         // Validate the application and version input.
120         final String normalVersion = normalizeVersion(app, version);
121 
122         // Unique, randomly assigned ID for this installation.
123         PreferenceStore prefs = getPreferenceStore();
124         if (prefs != null) {
125             if (!prefs.contains(PING_ID)) {
126                 // First time: make up a new ID.  TODO: Use something more random?
127                 prefs.setValue(PING_ID, new Random().nextLong());
128 
129                 // Also give them a chance to opt out.
130                 prefs.setValue(PING_OPT_IN, getUserPermission(display));
131                 try {
132                     prefs.save();
133                 }
134                 catch (IOException ioe) {
135                 }
136             }
137 
138             // If the user has not opted in, do nothing and quietly return.
139             if (!prefs.getBoolean(PING_OPT_IN)) {
140                 // user opted out.
141                 return;
142             }
143 
144             // If the last ping *for this app* was too recent, do nothing.
145             String timePref = PING_TIME + "." + app;  // $NON-NLS-1$
146             long now = System.currentTimeMillis();
147             long then = prefs.getLong(timePref);
148             if (now - then < PING_INTERVAL_MSEC) {
149                 // too soon after a ping.
150                 return;
151             }
152 
153             // Record the time of the attempt, whether or not it succeeds.
154             prefs.setValue(timePref, now);
155             try {
156                 prefs.save();
157             }
158             catch (IOException ioe) {
159             }
160 
161             // Send the ping itself in the background (don't block if the
162             // network is down or slow or confused).
163             final long id = prefs.getLong(PING_ID);
164             new Thread() {
165                 @Override
166                 public void run() {
167                     try {
168                         actuallySendPing(app, normalVersion, id);
169                     } catch (IOException e) {
170                         e.printStackTrace();
171                     }
172                 }
173             }.start();
174         }
175     }
176 
177     /**
178      * Returns the DDMS {@link PreferenceStore}.
179      */
getPreferenceStore()180     public static synchronized PreferenceStore getPreferenceStore() {
181         if (sPrefStore == null) {
182             // get the location of the preferences
183             String homeDir = null;
184             try {
185                 homeDir = AndroidLocation.getFolder();
186             } catch (AndroidLocationException e1) {
187                 // pass, we'll do a dummy store since homeDir is null
188             }
189 
190             if (homeDir != null) {
191                 String rcFileName = homeDir + "ddms.cfg"; //$NON-NLS-1$
192 
193                 // also look for an old pref file in the previous location
194                 String oldPrefPath = System.getProperty("user.home") //$NON-NLS-1$
195                     + File.separator + ".ddmsrc"; //$NON-NLS-1$
196                 File oldPrefFile = new File(oldPrefPath);
197                 if (oldPrefFile.isFile()) {
198                     try {
199                         PreferenceStore oldStore = new PreferenceStore(oldPrefPath);
200                         oldStore.load();
201 
202                         oldStore.save(new FileOutputStream(rcFileName), "");
203                         oldPrefFile.delete();
204 
205                         PreferenceStore newStore = new PreferenceStore(rcFileName);
206                         newStore.load();
207                         sPrefStore = newStore;
208                     } catch (IOException e) {
209                         // create a new empty store.
210                         sPrefStore = new PreferenceStore(rcFileName);
211                     }
212                 } else {
213                     sPrefStore = new PreferenceStore(rcFileName);
214 
215                     try {
216                         sPrefStore.load();
217                     } catch (IOException e) {
218                         System.err.println("Error Loading Preferences");
219                     }
220                 }
221             } else {
222                 sPrefStore = new PreferenceStore();
223             }
224         }
225 
226         return sPrefStore;
227     }
228 
229     /**
230      * Unconditionally send a "ping" request to the Google toolbar server.
231      *
232      * @param app name to report in the ping
233      * @param version to report in the ping (dotted numbers, no more than four)
234      * @param id of the local installation
235      * @throws IOException if the ping failed
236      */
237     @SuppressWarnings("deprecation")
actuallySendPing(String app, String version, long id)238     private static void actuallySendPing(String app, String version, long id)
239         throws IOException {
240         // Detect and report the host OS.
241         String os = System.getProperty("os.name");          // $NON-NLS-1$
242         if (os.startsWith("Mac OS")) {                      // $NON-NLS-1$
243             os = "mac";                                     // $NON-NLS-1$
244             String osVers = getVersion();
245             if (osVers != null) {
246                 os = os + "-" + osVers;                     // $NON-NLS-1$
247             }
248         } else if (os.startsWith("Windows")) {              // $NON-NLS-1$
249             os = "win";                                     // $NON-NLS-1$
250             String osVers = getVersion();
251             if (osVers != null) {
252                 os = os + "-" + osVers;                     // $NON-NLS-1$
253             }
254         } else if (os.startsWith("Linux")) {                // $NON-NLS-1$
255             os = "linux";                                   // $NON-NLS-1$
256         } else {
257             // Unknown -- surprising -- send it verbatim so we can see it.
258             os = URLEncoder.encode(os);
259         }
260 
261         // Include the application's name as part of the as= value.
262         // Share the user ID for all apps, to allow unified activity reports.
263 
264         URL url = new URL(
265             "http",                                         // $NON-NLS-1$
266             "tools.google.com",                             // $NON-NLS-1$
267             "/service/update?as=androidsdk_" + app +        // $NON-NLS-1$
268                 "&id=" + Long.toHexString(id) +             // $NON-NLS-1$
269                 "&version=" + version +                     // $NON-NLS-1$
270                 "&os=" + os);                               // $NON-NLS-1$
271 
272         // Discard the actual response, but make sure it reads OK
273         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
274 
275         // Believe it or not, a 404 response indicates success:
276         // the ping was logged, but no update is configured.
277         if (conn.getResponseCode() != HttpURLConnection.HTTP_OK &&
278             conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) {
279             throw new IOException(
280                 conn.getResponseMessage() + ": " + url);    // $NON-NLS-1$
281         }
282     }
283 
284     /**
285      * Returns the version of the os if it is defined as X.Y, or null otherwise.
286      * <p/>
287      * Example of returned versions can be found at http://lopica.sourceforge.net/os.html
288      * <p/>
289      * This method removes any exiting micro versions.
290      */
getVersion()291     private static String getVersion() {
292         Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); // $NON-NLS-1$
293         String osVers = System.getProperty("os.version"); // $NON-NLS-1$
294         Matcher m = p.matcher(osVers);
295         if (m.matches()) {
296             return m.group(1) + "." + m.group(2);         // $NON-NLS-1$
297         }
298 
299         return null;
300     }
301 
302     /**
303      * Prompt the user for whether they want to opt out of reporting.
304      * @return whether the user allows reporting (they do not opt out).
305      */
getUserPermission(Display display)306     private static boolean getUserPermission(Display display) {
307         // Whether the user gave permission (size-1 array for writing to).
308         // Initialize to false, set when the user clicks the button.
309         final boolean[] permission = new boolean[] { false };
310 
311         boolean dispose = false;
312         if (display == null) {
313             display = new Display();
314             dispose = true;
315         }
316 
317         final Display currentDisplay = display;
318         final boolean disposeDisplay = dispose;
319 
320         display.syncExec(new Runnable() {
321             public void run() {
322                 final Shell shell = new Shell(currentDisplay, SWT.TITLE | SWT.BORDER);
323                 shell.setText(WINDOW_TITLE_TEXT);
324                 shell.setLayout(new GridLayout(1, false)); // 1 column
325 
326                 // Take the default font and scale it up for the title.
327                 final Label title = new Label(shell, SWT.CENTER | SWT.WRAP);
328                 final FontData[] fontdata = title.getFont().getFontData();
329                 for (int i = 0; i < fontdata.length; i++) {
330                     fontdata[i].setHeight(fontdata[i].getHeight() * 4 / 3);
331                 }
332                 title.setFont(new Font(currentDisplay, fontdata));
333                 title.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
334                 title.setText(HEADER_TEXT);
335 
336                 final Label notice = new Label(shell, SWT.WRAP);
337                 notice.setFont(title.getFont());
338                 notice.setForeground(new Color(currentDisplay, 255, 0, 0));
339                 notice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
340                 notice.setText(NOTICE_TEXT);
341 
342                 final Link text = new Link(shell, SWT.WRAP);
343                 text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
344                 text.setText(BODY_TEXT);
345                 text.addSelectionListener(new SelectionAdapter() {
346                     @Override
347                     public void widgetSelected(SelectionEvent event) {
348                         openUrl(event.text);
349                     }
350                 });
351 
352                 final Button checkbox = new Button(shell, SWT.CHECK);
353                 checkbox.setSelection(true); // Opt-in by default.
354                 checkbox.setText(CHECKBOX_TEXT);
355 
356                 final Link footer = new Link(shell, SWT.WRAP);
357                 footer.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
358                 footer.setText(FOOTER_TEXT);
359 
360                 final Button button = new Button(shell, SWT.PUSH);
361                 button.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
362                 button.setText(BUTTON_TEXT);
363                 button.addSelectionListener(new SelectionAdapter() {
364                     @Override
365                     public void widgetSelected(SelectionEvent event) {
366                         permission[0] = checkbox.getSelection();
367                         shell.close();
368                     }
369                 });
370 
371                 // Size the window to a fixed width, as high as necessary,
372                 // centered.
373                 final Point size = shell.computeSize(450, SWT.DEFAULT, true);
374                 final Rectangle screen = currentDisplay.getClientArea();
375                 shell.setBounds(screen.x + screen.width / 2 - size.x / 2, screen.y + screen.height
376                         / 2 - size.y / 2, size.x, size.y);
377 
378                 shell.open();
379                 while (!shell.isDisposed()) {
380                     if (!currentDisplay.readAndDispatch())
381                         currentDisplay.sleep();
382                 }
383 
384                 if (disposeDisplay) {
385                     currentDisplay.dispose();
386                 }
387             }
388         });
389 
390         return permission[0];
391     }
392 
393     /**
394      * Open a URL in an external browser.
395      * @param url to open - MUST be sanitized and properly formed!
396      */
openUrl(final String url)397     public static void openUrl(final String url) {
398         // TODO: consider using something like BrowserLauncher2
399         // (http://browserlaunch2.sourceforge.net/) instead of these hacks.
400 
401         // SWT's Program.launch() should work on Mac, Windows, and GNOME
402         // (because the OS shell knows how to launch a default browser).
403         if (!Program.launch(url)) {
404             // Must be Linux non-GNOME (or something else broke).
405             // Try a few Linux browser commands in the background.
406             new Thread() {
407                 @Override
408                 public void run() {
409                     for (String cmd : LINUX_BROWSERS) {
410                         cmd = cmd.replaceAll("%URL%", url);  // $NON-NLS-1$
411                         try {
412                             Process proc = Runtime.getRuntime().exec(cmd);
413                             if (proc.waitFor() == 0) break;  // Success!
414                         } catch (InterruptedException e) {
415                             // Should never happen!
416                             throw new RuntimeException(e);
417                         } catch (IOException e) {
418                             // Swallow the exception and try the next browser.
419                         }
420                     }
421 
422                     // TODO: Pop up some sort of error here?
423                     // (We're in a new thread; can't use the existing Display.)
424                 }
425             }.start();
426         }
427     }
428 
429     /**
430      * Validate the supplied application version, and normalize the version.
431      * @param app to report
432      * @param version supplied by caller
433      * @return normalized dotted quad version
434      */
normalizeVersion(String app, String version)435     private static String normalizeVersion(String app, String version) {
436         // Application name must contain only word characters (no punctuaation)
437         if (!app.matches("\\w+")) {
438             throw new IllegalArgumentException("Bad app name: " + app);
439         }
440 
441         // Version must be between 1 and 4 dotted numbers
442         String[] numbers = version.split("\\.");
443         if (numbers.length > 4) {
444             throw new IllegalArgumentException("Bad version: " + version);
445         }
446         for (String part: numbers) {
447             if (!part.matches("\\d+")) {
448                 throw new IllegalArgumentException("Bad version: " + version);
449             }
450         }
451 
452         // Always output 4 numbers, even if fewer were supplied (pad with .0)
453         StringBuffer normal = new StringBuffer(numbers[0]);
454         for (int i = 1; i < 4; i++) {
455             normal.append(".").append(i < numbers.length ? numbers[i] : "0");
456         }
457         return normal.toString();
458     }
459 }
460