diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 1506a206c0a5e3b593c4c61a62b8805b64e98daf (patch) | |
tree | e20fe3eb0f693e87649fff1ce75e3f23330f69f8 /sdkstats | |
download | sdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.zip sdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.tar.gz sdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.tar.bz2 |
Initial Contribution
Diffstat (limited to 'sdkstats')
-rw-r--r-- | sdkstats/.classpath | 8 | ||||
-rw-r--r-- | sdkstats/.project | 17 | ||||
-rw-r--r-- | sdkstats/Android.mk | 4 | ||||
-rw-r--r-- | sdkstats/README | 11 | ||||
-rw-r--r-- | sdkstats/src/Android.mk | 15 | ||||
-rw-r--r-- | sdkstats/src/com/android/sdkstats/SdkStatsService.java | 416 |
6 files changed, 471 insertions, 0 deletions
diff --git a/sdkstats/.classpath b/sdkstats/.classpath new file mode 100644 index 0000000..73b1af5 --- /dev/null +++ b/sdkstats/.classpath @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/> + <classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/sdkstats/.project b/sdkstats/.project new file mode 100644 index 0000000..4dbfa87 --- /dev/null +++ b/sdkstats/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>SdkStatsService</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/sdkstats/Android.mk b/sdkstats/Android.mk new file mode 100644 index 0000000..f4cabdc --- /dev/null +++ b/sdkstats/Android.mk @@ -0,0 +1,4 @@ +# Copyright 2007 The Android Open Source Project +# +SDKSTATS_LOCAL_DIR := $(call my-dir) +include $(SDKSTATS_LOCAL_DIR)/src/Android.mk diff --git a/sdkstats/README b/sdkstats/README new file mode 100644 index 0000000..8ed0880 --- /dev/null +++ b/sdkstats/README @@ -0,0 +1,11 @@ +How to use the Eclipse projects for SdkStats. + +SdkStats requires SWT to compile. + +SWT is available in the depot under //device/prebuild/<platform>/swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at //device/prebuild/<platform>/swt. diff --git a/sdkstats/src/Android.mk b/sdkstats/src/Android.mk new file mode 100644 index 0000000..bff43f3 --- /dev/null +++ b/sdkstats/src/Android.mk @@ -0,0 +1,15 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_LIBRARIES := \ + androidprefs \ + swt \ + org.eclipse.jface_3.2.0.I20060605-1400 \ + org.eclipse.equinox.common_3.2.0.v20060603 \ + org.eclipse.core.commands_3.2.0.I20060605-1400 +LOCAL_MODULE := sdkstats + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/sdkstats/src/com/android/sdkstats/SdkStatsService.java b/sdkstats/src/com/android/sdkstats/SdkStatsService.java new file mode 100644 index 0000000..0b3d41b --- /dev/null +++ b/sdkstats/src/com/android/sdkstats/SdkStatsService.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkstats; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.program.Program; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Random; + +/** Utility class to send "ping" usage reports to the server. */ +public class SdkStatsService { + + /** Minimum interval between ping, in milliseconds. */ + private static final long PING_INTERVAL_MSEC = 86400 * 1000; // 1 day + + /* Text strings displayed in the opt-out dialog. */ + private static final String WINDOW_TITLE_TEXT = + "Android SDK"; + + private static final String HEADER_TEXT = + "Thanks for using the Android SDK!"; + + private static final String NOTICE_TEXT = + "We know you just want to get started but please read this first."; + + /** Used in the preference pane (PrefsDialog) as well. */ + public static final String BODY_TEXT = + "By choosing to send certain usage statistics to Google, you can " + + "help us improve the Android SDK. These usage statistics let us " + + "measure things like active usage of the SDK and let us know things " + + "like which versions of the SDK are in use and which tools are the " + + "most popular with developers. This limited data is not associated " + + "with personal information about you, is examined on an aggregate " + + "basis, and is maintained in accordance with the " + + "<a href=\"http://www.google.com/intl/en/privacy.html\">Google " + + "Privacy Policy</a>."; + + /** Used in the preference pane (PrefsDialog) as well. */ + public static final String CHECKBOX_TEXT = + "Send usage statistics to Google."; + + private static final String FOOTER_TEXT = + "If you later decide to change this setting, you can do so in the " + + "\"ddms\" tool under \"File\" > \"Preferences\" > \"Usage Stats\"."; + + private static final String BUTTON_TEXT = + " Proceed "; + + /** List of Linux browser commands to try, in order (see openUrl). */ + private static final String[] LINUX_BROWSERS = new String[] { + "firefox -remote openurl(%URL%,new-window)", // $NON-NLS-1$ running FF + "mozilla -remote openurl(%URL%,new-window)", // $NON-NLS-1$ running Moz + "firefox %URL%", // $NON-NLS-1$ new FF + "mozilla %URL%", // $NON-NLS-1$ new Moz + "kfmclient openURL %URL%", // $NON-NLS-1$ Konqueror + "opera -newwindow %URL%", // $NON-NLS-1$ Opera + }; + + public final static String PING_OPT_IN = "pingOptIn"; //$NON-NLS-1$ + public final static String PING_TIME = "pingTime"; //$NON-NLS-1$ + public final static String PING_ID = "pingId"; //$NON-NLS-1$ + + + private static PreferenceStore sPrefStore; + + /** + * Send a "ping" to the Google toolbar server, if enough time has + * elapsed since the last ping, and if the user has not opted out. + * If this is the first time, notify the user and offer an opt-out. + * Note: UI operations (if any) are synchronous, but the actual ping + * (if any) is sent in a <i>non-daemon</i> background thread. + * + * @param app name to report in the ping + * @param version to report in the ping + */ + public static void ping(final String app, final String version) { + // Validate the application and version input. + final String normalVersion = normalizeVersion(app, version); + + // Unique, randomly assigned ID for this installation. + PreferenceStore prefs = getPreferenceStore(); + if (prefs != null) { + if (!prefs.contains(PING_ID)) { + // First time: make up a new ID. TODO: Use something more random? + prefs.setValue(PING_ID, new Random().nextLong()); + + // Also give them a chance to opt out. + prefs.setValue(PING_OPT_IN, getUserPermission()); + try { + prefs.save(); + } + catch (IOException ioe) { + } + } + + // If the user has not opted in, do nothing and quietly return. + if (!prefs.getBoolean(PING_OPT_IN)) { + // user opted out. + return; + } + + // If the last ping *for this app* was too recent, do nothing. + String timePref = PING_TIME + "." + app; // $NON-NLS-1$ + long now = System.currentTimeMillis(); + long then = prefs.getLong(timePref); + if (now - then < PING_INTERVAL_MSEC) { + // too soon after a ping. + return; + } + + // Record the time of the attempt, whether or not it succeeds. + prefs.setValue(timePref, now); + try { + prefs.save(); + } + catch (IOException ioe) { + } + + // Send the ping itself in the background (don't block if the + // network is down or slow or confused). + final long id = prefs.getLong(PING_ID); + new Thread() { + @Override + public void run() { + try { + actuallySendPing(app, normalVersion, id); + } catch (IOException e) { + e.printStackTrace(); + } + } + }.start(); + } + } + + /** + * Returns the DDMS {@link PreferenceStore}. + */ + public static synchronized PreferenceStore getPreferenceStore() { + if (sPrefStore == null) { + // get the location of the preferences + String homeDir = null; + try { + homeDir = AndroidLocation.getFolder(); + } catch (AndroidLocationException e1) { + // pass, we'll do a dummy store since homeDir is null + } + + if (homeDir != null) { + String rcFileName = homeDir + "ddms.cfg"; //$NON-NLS-1$ + + // also look for an old pref file in the previous location + String oldPrefPath = System.getProperty("user.home") //$NON-NLS-1$ + + File.separator + ".ddmsrc"; //$NON-NLS-1$ + File oldPrefFile = new File(oldPrefPath); + if (oldPrefFile.isFile()) { + try { + PreferenceStore oldStore = new PreferenceStore(oldPrefPath); + oldStore.load(); + + oldStore.save(new FileOutputStream(rcFileName), ""); + oldPrefFile.delete(); + + PreferenceStore newStore = new PreferenceStore(rcFileName); + newStore.load(); + sPrefStore = newStore; + } catch (IOException e) { + // create a new empty store. + sPrefStore = new PreferenceStore(rcFileName); + } + } else { + sPrefStore = new PreferenceStore(rcFileName); + + try { + sPrefStore.load(); + } catch (IOException e) { + System.err.println("Error Loading Preferences"); + } + } + } else { + sPrefStore = new PreferenceStore(); + } + } + + return sPrefStore; + } + + /** + * Unconditionally send a "ping" request to the Google toolbar server. + * + * @param app name to report in the ping + * @param version to report in the ping (dotted numbers, no more than four) + * @param id of the local installation + * @throws IOException if the ping failed + */ + @SuppressWarnings("deprecation") + private static void actuallySendPing(String app, String version, long id) + throws IOException { + // Detect and report the host OS. + String os = System.getProperty("os.name"); // $NON-NLS-1$ + if (os.startsWith("Mac OS")) { // $NON-NLS-1$ + os = "mac"; // $NON-NLS-1$ + } else if (os.startsWith("Windows")) { // $NON-NLS-1$ + os = "win"; // $NON-NLS-1$ + } else if (os.startsWith("Linux")) { // $NON-NLS-1$ + os = "linux"; // $NON-NLS-1$ + } else { + // Unknown -- surprising -- send it verbatim so we can see it. + os = URLEncoder.encode(os); + } + + // Include the application's name as part of the as= value. + // Share the user ID for all apps, to allow unified activity reports. + + URL url = new URL( + "http", // $NON-NLS-1$ + "tools.google.com", // $NON-NLS-1$ + "/service/update?as=androidsdk_" + app + // $NON-NLS-1$ + "&id=" + Long.toHexString(id) + // $NON-NLS-1$ + "&version=" + version + // $NON-NLS-1$ + "&os=" + os); // $NON-NLS-1$ + + // Discard the actual response, but make sure it reads OK + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + // Believe it or not, a 404 response indicates success: + // the ping was logged, but no update is configured. + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK && + conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) { + throw new IOException( + conn.getResponseMessage() + ": " + url); // $NON-NLS-1$ + } + } + + /** + * Prompt the user for whether they want to opt out of reporting. + * @return whether the user allows reporting (they do not opt out). + */ + private static boolean getUserPermission() { + // Use dialog trim for the shell, but without a close button. + final Display display = new Display(); + final Shell shell = new Shell(display, SWT.TITLE | SWT.BORDER); + shell.setText(WINDOW_TITLE_TEXT); + shell.setLayout(new GridLayout(1, false)); // 1 column + + // Take the default font and scale it up for the title. + final Label title = new Label(shell, SWT.CENTER | SWT.WRAP); + final FontData[] fontdata = title.getFont().getFontData(); + for (int i = 0; i < fontdata.length; i++) { + fontdata[i].setHeight(fontdata[i].getHeight() * 4 / 3); + } + title.setFont(new Font(display, fontdata)); + title.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + title.setText(HEADER_TEXT); + + final Label notice = new Label(shell, SWT.WRAP); + notice.setFont(title.getFont()); + notice.setForeground(new Color(display, 255, 0, 0)); + notice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + notice.setText(NOTICE_TEXT); + + final Link text = new Link(shell, SWT.WRAP); + text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + text.setText(BODY_TEXT); + text.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + openUrl(event.text); + } + }); + + final Button checkbox = new Button(shell, SWT.CHECK); + checkbox.setSelection(true); // Opt-in by default. + checkbox.setText(CHECKBOX_TEXT); + + final Link footer = new Link(shell, SWT.WRAP); + footer.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + footer.setText(FOOTER_TEXT); + + // Whether the user gave permission (size-1 array for writing to). + // Initialize to false, set when the user clicks the button. + final boolean[] permission = new boolean[] { false }; + + final Button button = new Button(shell, SWT.PUSH); + button.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + button.setText(BUTTON_TEXT); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + permission[0] = checkbox.getSelection(); + shell.close(); + } + }); + + // Size the window to a fixed width, as high as necessary, centered. + final Point size = shell.computeSize(450, SWT.DEFAULT, true); + final Rectangle screen = display.getClientArea(); + shell.setBounds( + screen.x + screen.width / 2 - size.x / 2, + screen.y + screen.height / 2 - size.y / 2, + size.x, size.y); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + display.dispose(); // Otherwise ddms' own Display can't be created + return permission[0]; + } + + /** + * Open a URL in an external browser. + * @param url to open - MUST be sanitized and properly formed! + */ + public static void openUrl(final String url) { + // TODO: consider using something like BrowserLauncher2 + // (http://browserlaunch2.sourceforge.net/) instead of these hacks. + + // SWT's Program.launch() should work on Mac, Windows, and GNOME + // (because the OS shell knows how to launch a default browser). + if (!Program.launch(url)) { + // Must be Linux non-GNOME (or something else broke). + // Try a few Linux browser commands in the background. + new Thread() { + @Override + public void run() { + for (String cmd : LINUX_BROWSERS) { + cmd = cmd.replaceAll("%URL%", url); // $NON-NLS-1$ + try { + Process proc = Runtime.getRuntime().exec(cmd); + if (proc.waitFor() == 0) break; // Success! + } catch (InterruptedException e) { + // Should never happen! + throw new RuntimeException(e); + } catch (IOException e) { + // Swallow the exception and try the next browser. + } + } + + // TODO: Pop up some sort of error here? + // (We're in a new thread; can't use the existing Display.) + } + }.start(); + } + } + + /** + * Validate the supplied application version, and normalize the version. + * @param app to report + * @param version supplied by caller + * @return normalized dotted quad version + */ + private static String normalizeVersion(String app, String version) { + // Application name must contain only word characters (no punctuaation) + if (!app.matches("\\w+")) { + throw new IllegalArgumentException("Bad app name: " + app); + } + + // Version must be between 1 and 4 dotted numbers + String[] numbers = version.split("\\."); + if (numbers.length > 4) { + throw new IllegalArgumentException("Bad version: " + version); + } + for (String part: numbers) { + if (!part.matches("\\d+")) { + throw new IllegalArgumentException("Bad version: " + version); + } + } + + // Always output 4 numbers, even if fewer were supplied (pad with .0) + StringBuffer normal = new StringBuffer(numbers[0]); + for (int i = 1; i < 4; i++) { + normal.append(".").append(i < numbers.length ? numbers[i] : "0"); + } + return normal.toString(); + } +} |