• 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.regex.Matcher;
27 import java.util.regex.Pattern;
28 
29 /** Utility class to send "ping" usage reports to the server. */
30 public class SdkStatsService {
31 
32     /** Minimum interval between ping, in milliseconds. */
33     private static final long PING_INTERVAL_MSEC = 86400 * 1000;  // 1 day
34 
35     private DdmsPreferenceStore mStore = new DdmsPreferenceStore();
36 
SdkStatsService()37     public SdkStatsService() {
38     }
39 
40     /**
41      * Send a "ping" to the Google toolbar server, if enough time has
42      * elapsed since the last ping, and if the user has not opted out.<br>
43      *
44      * The ping will not be sent if the user opt out dialog has not been shown yet.
45      * Use {@link #getUserPermissionForPing(Shell)} to display the dialog requesting
46      * user permissions.<br>
47      *
48      * Note: The actual ping (if any) is sent in a <i>non-daemon</i> background thread.
49      *
50      * @param app name to report in the ping
51      * @param version to report in the ping
52      */
ping(String app, String version)53     public void ping(String app, String version) {
54         doPing(app, version);
55     }
56 
57     /**
58      * Display a dialog to the user providing information about the ping service,
59      * and whether they'd like to opt-out of it.
60      *
61      * Once the dialog has been shown, it sets a preference internally indicating that the user has
62      * viewed this dialog. This setting can be queried using {@link #pingPermissionsSet()}.
63      */
checkUserPermissionForPing(Shell parent)64     public void checkUserPermissionForPing(Shell parent) {
65         if (!mStore.hasPingId()) {
66             askUserPermissionForPing(parent);
67             mStore.generateNewPingId();
68         }
69     }
70 
71     /**
72      * Prompt the user for whether they want to opt out of reporting, and save the user
73      * input in preferences.
74      */
askUserPermissionForPing(final Shell parent)75     private void askUserPermissionForPing(final Shell parent) {
76         final Display display = parent.getDisplay();
77         display.syncExec(new Runnable() {
78             public void run() {
79                 SdkStatsPermissionDialog dialog = new SdkStatsPermissionDialog(parent);
80                 dialog.open();
81                 mStore.setPingOptIn(dialog.getPingUserPreference());
82             }
83         });
84     }
85 
86     // -------
87 
88     /**
89      * Pings the usage stats server, as long as the prefs contain the opt-in boolean
90      *
91      * @param app name to report in the ping
92      * @param version to report in the ping
93      */
doPing(final String app, String version)94     private void doPing(final String app, String version) {
95         // Validate the application and version input.
96         final String normalVersion = normalizeVersion(app, version);
97 
98         // If the user has not opted in, do nothing and quietly return.
99         if (!mStore.isPingOptIn()) {
100             // user opted out.
101             return;
102         }
103 
104         // If the last ping *for this app* was too recent, do nothing.
105         long now = System.currentTimeMillis();
106         long then = mStore.getPingTime(app);
107         if (now - then < PING_INTERVAL_MSEC) {
108             // too soon after a ping.
109             return;
110         }
111 
112         // Record the time of the attempt, whether or not it succeeds.
113         mStore.setPingTime(app, now);
114 
115         // Send the ping itself in the background (don't block if the
116         // network is down or slow or confused).
117         final long id = mStore.getPingId();
118         new Thread() {
119             @Override
120             public void run() {
121                 try {
122                     actuallySendPing(app, normalVersion, id);
123                 } catch (IOException e) {
124                     e.printStackTrace();
125                 }
126             }
127         }.start();
128     }
129 
130 
131     /**
132      * Unconditionally send a "ping" request to the Google toolbar server.
133      *
134      * @param app name to report in the ping
135      * @param version to report in the ping (dotted numbers, no more than four)
136      * @param id of the local installation
137      * @throws IOException if the ping failed
138      */
139     @SuppressWarnings("deprecation")
actuallySendPing(String app, String version, long id)140     private static void actuallySendPing(String app, String version, long id)
141                 throws IOException {
142         // Detect and report the host OS.
143         String os = System.getProperty("os.name");          //$NON-NLS-1$
144         if (os.startsWith("Mac OS")) {                      //$NON-NLS-1$
145             os = "mac";                                     //$NON-NLS-1$
146             String osVers = getVersion();
147             if (osVers != null) {
148                 os = os + "-" + osVers;                     //$NON-NLS-1$
149             }
150         } else if (os.startsWith("Windows")) {              //$NON-NLS-1$
151             os = "win";                                     //$NON-NLS-1$
152             String osVers = getVersion();
153             if (osVers != null) {
154                 os = os + "-" + osVers;                     //$NON-NLS-1$
155             }
156         } else if (os.startsWith("Linux")) {                //$NON-NLS-1$
157             os = "linux";                                   //$NON-NLS-1$
158         } else {
159             // Unknown -- surprising -- send it verbatim so we can see it.
160             os = URLEncoder.encode(os);
161         }
162 
163         // Include the application's name as part of the as= value.
164         // Share the user ID for all apps, to allow unified activity reports.
165 
166         URL url = new URL(
167             "http",                                         //$NON-NLS-1$
168             "tools.google.com",                             //$NON-NLS-1$
169             "/service/update?as=androidsdk_" + app +        //$NON-NLS-1$
170                 "&id=" + Long.toHexString(id) +             //$NON-NLS-1$
171                 "&version=" + version +                     //$NON-NLS-1$
172                 "&os=" + os);                               //$NON-NLS-1$
173 
174         // Discard the actual response, but make sure it reads OK
175         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
176 
177         // Believe it or not, a 404 response indicates success:
178         // the ping was logged, but no update is configured.
179         if (conn.getResponseCode() != HttpURLConnection.HTTP_OK &&
180             conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) {
181             throw new IOException(
182                 conn.getResponseMessage() + ": " + url);    //$NON-NLS-1$
183         }
184     }
185 
186     /**
187      * Returns the version of the os if it is defined as X.Y, or null otherwise.
188      * <p/>
189      * Example of returned versions can be found at http://lopica.sourceforge.net/os.html
190      * <p/>
191      * This method removes any exiting micro versions.
192      */
getVersion()193     private static String getVersion() {
194         Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$
195         String osVers = System.getProperty("os.version"); //$NON-NLS-1$
196         Matcher m = p.matcher(osVers);
197         if (m.matches()) {
198             return m.group(1) + "." + m.group(2);         //$NON-NLS-1$
199         }
200 
201         return null;
202     }
203 
204     /**
205      * Validate the supplied application version, and normalize the version.
206      * @param app to report
207      * @param version supplied by caller
208      * @return normalized dotted quad version
209      */
normalizeVersion(String app, String version)210     private static String normalizeVersion(String app, String version) {
211         // Application name must contain only word characters (no punctuation)
212         if (!app.matches("\\w+")) {
213             throw new IllegalArgumentException("Bad app name: " + app);
214         }
215 
216         // Version must be between 1 and 4 dotted numbers
217         String[] numbers = version.split("\\.");
218         if (numbers.length > 4) {
219             throw new IllegalArgumentException("Bad version: " + version);
220         }
221         for (String part: numbers) {
222             if (!part.matches("\\d+")) {
223                 throw new IllegalArgumentException("Bad version: " + version);
224             }
225         }
226 
227         // Always output 4 numbers, even if fewer were supplied (pad with .0)
228         StringBuffer normal = new StringBuffer(numbers[0]);
229         for (int i = 1; i < 4; i++) {
230             normal.append(".").append(i < numbers.length ? numbers[i] : "0");
231         }
232         return normal.toString();
233     }
234 }
235