aboutsummaryrefslogtreecommitdiffstats
path: root/sdkstats
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit1506a206c0a5e3b593c4c61a62b8805b64e98daf (patch)
treee20fe3eb0f693e87649fff1ce75e3f23330f69f8 /sdkstats
downloadsdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.zip
sdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.tar.gz
sdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.tar.bz2
Initial Contribution
Diffstat (limited to 'sdkstats')
-rw-r--r--sdkstats/.classpath8
-rw-r--r--sdkstats/.project17
-rw-r--r--sdkstats/Android.mk4
-rw-r--r--sdkstats/README11
-rw-r--r--sdkstats/src/Android.mk15
-rw-r--r--sdkstats/src/com/android/sdkstats/SdkStatsService.java416
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();
+ }
+}