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