OSDN Git Service

Welcome Wizard
authorTor Norbye <tnorbye@google.com>
Wed, 21 Sep 2011 22:12:08 +0000 (15:12 -0700)
committerTor Norbye <tnorbye@google.com>
Fri, 23 Sep 2011 23:08:14 +0000 (16:08 -0700)
This changeset adds a new "Welcome Wizard" which is shown the first
time a user runs Eclipse with the ADT plugin.

The welcome wizard asks for two pieces of information:

(1) The location of the SDK.
(2) Whether the user agrees to gathering usage statistics.

We've needed this information before, but collection of the data had
been more ad-hoc: The usage data permission dialog would show up on
its own, and the SDK information would be requested the first time
some code path touched it (e.g.  opening a layout or opening the
preference dialog's Android page etc).

In addition, the wizard also offers to *install* SDKs if you don't
already have one. It gives the option between the latest available
platform, and one supported by a large majority of devices (currently
API level 7), or both. If you select this option, then when finishing
the wizard the SDK manager is run in a special mode where it installs
the required packages with a progress dialog.

This changeset also starts recording the chosen SDK location in the
~/.android settings file. This allows us to detect when you're running
Eclipse in a brand new workspace and you've already gone through the
SDK selection before, and we don't need to ask again -- we'll just use
the most recently known location.

The wizard will only be shown once. If you bypass or cancel out of the
wizard, you can still configure your SDK the old way - via the
Preference dialog. Note also that the usage permission page is only
shown if the user has not already opted in via say ddms.

NOTE: If you want to test this, make sure you haven't set the
environment variable ADT_TEST_SDK_PATH (as some of us do for running
unit tests) since it is treated as the user having selected the given
SDK root, and in particular it means the wizard won't be shown even if
you've wiped adtUsed=true from your ~/.android/ddms.cfg etc.

Change-Id: I0a4e2c4efce84aca9beae394ce67e4c145cbb000

eclipse/dictionary.txt
eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AndroidPreferencePage.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/AdtStartup.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/UsagePermissionPage.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizard.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java [new file with mode: 0644]
sdkstats/src/com/android/sdkstats/DdmsPreferenceStore.java
sdkstats/src/com/android/sdkstats/SdkStatsPermissionDialog.java

index ac9ff9a..b9f3106 100644 (file)
@@ -169,6 +169,7 @@ pings
 placeholder
 placeholders
 plugin
+plugins
 popup
 popups
 pre
@@ -226,6 +227,7 @@ snip
 spec
 standalone
 stash
+stat
 stateful
 stateless
 stderr
index 9f5584c..9ceccae 100644 (file)
             priority="high">
         </content-type>
     </extension>
+    <extension point="org.eclipse.ui.startup">
+        <startup class="com.android.ide.eclipse.adt.internal.welcome.AdtStartup"/>
+    </extension>
 
    <!-- workaround for bug 15003. -->
     <extension
index 2ada800..f00518c 100644 (file)
@@ -39,9 +39,9 @@ import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerIni
 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
+import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
-import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
 import com.android.ide.eclipse.adt.internal.ui.EclipseUiHelper;
@@ -50,7 +50,6 @@ import com.android.io.StreamException;
 import com.android.resources.ResourceFolderType;
 import com.android.sdklib.IAndroidTarget;
 import com.android.sdklib.SdkConstants;
-import com.android.sdkstats.SdkStatsService;
 
 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IFolder;
@@ -65,9 +64,7 @@ import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.QualifiedName;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.SubMonitor;
-import org.eclipse.core.runtime.jobs.IJobChangeEvent;
 import org.eclipse.core.runtime.jobs.Job;
-import org.eclipse.core.runtime.jobs.JobChangeAdapter;
 import org.eclipse.jdt.core.IJavaElement;
 import org.eclipse.jdt.core.IJavaProject;
 import org.eclipse.jdt.core.JavaCore;
@@ -101,8 +98,6 @@ import org.eclipse.ui.part.FileEditorInput;
 import org.eclipse.ui.plugin.AbstractUIPlugin;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.Constants;
-import org.osgi.framework.Version;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedReader;
@@ -270,36 +265,21 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
         // load preferences.
         AdtPrefs.getPrefs().loadValues(null /*event*/);
 
-        // check the location of SDK
-        final boolean isSdkLocationValid = checkSdkLocationAndId();
-
         // initialize editors
         startEditors();
 
         // Listen on resource file edits for updates to file inclusion
         IncludeFinder.start();
 
-        // Ping the usage server and parse the SDK content.
+        // Parse the SDK content.
         // This is deferred in separate jobs to avoid blocking the bundle start.
-        // We also serialize them to avoid too many parallel jobs when Eclipse starts.
-        Job pingJob = createPingUsageServerJob();
-        pingJob.addJobChangeListener(new JobChangeAdapter() {
-           @Override
-            public void done(IJobChangeEvent event) {
-                super.done(event);
-
-                // Once the ping job is finished, start the SDK parser
-                if (isSdkLocationValid) {
-                    // parse the SDK resources.
-                    parseSdkContent();
-                }
-            }
-        });
-        // build jobs are run after other interactive jobs
-        pingJob.setPriority(Job.BUILD);
-        // Wait 2 seconds before starting the ping job. This leaves some time to the
-        // other bundles to initialize.
-        pingJob.schedule(2000 /*milliseconds*/);
+        final boolean isSdkLocationValid = checkSdkLocationAndId();
+        if (isSdkLocationValid) {
+            // parse the SDK resources.
+            // Wait 2 seconds before starting the job. This leaves some time to the
+            // other bundles to initialize.
+            parseSdkContent(2000 /*milliseconds*/);
+        }
     }
 
     /*
@@ -1208,31 +1188,9 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
     }
 
     /**
-     * Creates a job than can ping the usage server.
-     */
-    private Job createPingUsageServerJob() {
-        // In order to not block the plugin loading, so we spawn another thread.
-        Job job = new Job("Android SDK Ping") {  // Job name, visible in progress view
-            @Override
-            protected IStatus run(IProgressMonitor monitor) {
-                try {
-                    pingUsageServer();
-
-                    return Status.OK_STATUS;
-                } catch (Throwable t) {
-                    log(t, "pingUsageServer failed");       //$NON-NLS-1$
-                    return new Status(IStatus.ERROR, PLUGIN_ID,
-                            "pingUsageServer failed", t);    //$NON-NLS-1$
-                }
-            }
-        };
-        return job;
-    }
-
-    /**
      * Parses the SDK resources.
      */
-    private void parseSdkContent() {
+    private void parseSdkContent(long delay) {
         // Perform the update in a thread (here an Eclipse runtime job)
         // since this should never block the caller (especially the start method)
         Job job = new Job(Messages.AdtPlugin_Android_SDK_Content_Loader) {
@@ -1338,7 +1296,11 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
             }
         };
         job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
-        job.schedule();
+        if (delay > 0) {
+            job.schedule(delay);
+        } else {
+            job.schedule();
+        }
     }
 
     /** Returns the global android console */
@@ -1698,37 +1660,6 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
     }
 
     /**
-     * Pings the usage start server.
-     */
-    private void pingUsageServer() {
-
-        // Report the version of the ADT plugin to the stat server
-        String versionString = (String) getBundle().getHeaders().get(
-                Constants.BUNDLE_VERSION);
-        Version version = new Version(versionString);
-
-        versionString = String.format("%1$d.%2$d.%3$d", version.getMajor(), //$NON-NLS-1$
-                version.getMinor(), version.getMicro());
-
-        SdkStatsService stats = new SdkStatsService();
-        stats.ping("adt", versionString); //$NON-NLS-1$
-
-        // Report the version of Eclipse to the stat server.
-        // Get the version of eclipse by getting the version of one of the runtime plugins.
-        ResourcesPlugin resPlugin = ResourcesPlugin.getPlugin();
-
-        String eclipseVersionString = (String) resPlugin.getBundle().getHeaders().get(
-                Constants.BUNDLE_VERSION);
-
-        // parse the string using the Version class.
-        Version eclipseVersion = new Version(eclipseVersionString);
-        eclipseVersionString = String.format("%1$d.%2$d",  //$NON-NLS-1$
-                eclipseVersion.getMajor(), eclipseVersion.getMinor());
-
-        stats.ping("eclipse", eclipseVersionString); //$NON-NLS-1$
-    }
-
-    /**
      * Reparses the content of the SDK and updates opened projects.
      */
     public void reparseSdk() {
@@ -1741,7 +1672,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
         }
 
         // parse the SDK resources at the new location
-        parseSdkContent();
+        parseSdkContent(0 /*immediately*/);
     }
 
     /**
index 6df5fb1..577c431 100644 (file)
@@ -22,6 +22,7 @@ import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle;
 import com.android.prefs.AndroidLocation.AndroidLocationException;
 import com.android.sdklib.internal.build.DebugKeyProvider;
 import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
+import com.android.sdkstats.DdmsPreferenceStore;
 
 import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer;
 import org.eclipse.jface.preference.IPreferenceStore;
@@ -365,6 +366,27 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
         store.setValue(PREFS_MONITOR_DENSITY, density);
     }
 
+    /**
+     * Sets the new location of the SDK
+     *
+     * @param location the location of the SDK
+     */
+    public void setSdkLocation(File location) {
+        mOsSdkLocation = location != null ? location.getPath() : null;
+
+        // TODO: Also store this location in the .android settings directory
+        // such that we can support using multiple workspaces without asking
+        // over and over.
+        if (mOsSdkLocation != null && mOsSdkLocation.length() > 0) {
+            DdmsPreferenceStore ddmsStore = new DdmsPreferenceStore();
+            ddmsStore.setLastSdkPath(mOsSdkLocation);
+        }
+
+        // need to save this new value to the store
+        IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+        store.setValue(PREFS_SDK_DIR, mOsSdkLocation);
+    }
+
     @Override
     public void initializeDefaultPreferences() {
         IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
index c67afdb..c700145 100644 (file)
@@ -20,6 +20,7 @@ import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
 import com.android.sdklib.IAndroidTarget;
+import com.android.sdkstats.DdmsPreferenceStore;
 import com.android.sdkstats.SdkStatsService;
 import com.android.sdkuilib.internal.widgets.SdkTargetSelector;
 
@@ -154,6 +155,19 @@ public class AndroidPreferencePage extends FieldEditorPreferencePage implements
         }
 
         @Override
+        protected void doStore() {
+            super.doStore();
+
+            // Also sync the value to the ~/.android preference settings such that we can
+            // share it with future new workspaces
+            String path = AdtPrefs.getPrefs().getOsSdkFolder();
+            if (path != null && path.length() > 0 && new File(path).exists()) {
+                DdmsPreferenceStore ddmsStore = new DdmsPreferenceStore();
+                ddmsStore.setLastSdkPath(path);
+            }
+        }
+
+        @Override
         public Text getTextControl(Composite parent) {
             setValidateStrategy(VALIDATE_ON_KEY_STROKE);
             return super.getTextControl(parent);
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/AdtStartup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/AdtStartup.java
new file mode 100644 (file)
index 0000000..6dfc17b
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.welcome;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.sdkstats.DdmsPreferenceStore;
+import com.android.sdkstats.SdkStatsService;
+
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Plugin;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.ui.IStartup;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+import java.io.File;
+
+/**
+ * ADT startup tasks (other than those performed in {@link AdtPlugin#start(org.osgi.framework.BundleContext)}
+ * when the plugin is initializing.
+ * <p>
+ * The main tasks currently performed are:
+ * <ul>
+ *   <li> See if the user has ever run the welcome wizard, and if not, run it
+ *   <li> Ping the usage statistics server, if enabled by the user. This is done here
+ *       rather than during the plugin start since this task is run later (when the workspace
+ *       is fully initialized) and we want to ask the user for permission for usage
+ *       tracking before running it (and if we don't, then the usage tracking permissions
+ *       dialog will run instead.)
+ * </ul>
+ */
+public class AdtStartup implements IStartup {
+
+    private DdmsPreferenceStore mStore = new DdmsPreferenceStore();
+
+    public void earlyStartup() {
+        if (isFirstTime()) {
+            showWelcomeWizard();
+            // Usage statistics are sent after the wizard has run asynchronously (provided the
+            // user opted in)
+        } else if (mStore.isPingOptIn()) {
+            sendUsageStats();
+        }
+    }
+
+    private boolean isFirstTime() {
+        // If we already have a known SDK location in our workspace then we know this
+        // is not the first time this user is running ADT.
+        String osSdkFolder = AdtPrefs.getPrefs().getOsSdkFolder();
+        if (osSdkFolder != null && osSdkFolder.length() > 0) {
+            return false;
+        }
+
+        // If we've recorded an SDK location in the .android settings, then the user
+        // has run ADT before but possibly in a different workspace. We don't want to pop up
+        // the welcome wizard each time if we can simply use the existing SDK install.
+        String osSdkPath = mStore.getLastSdkPath();
+        if (osSdkPath != null && osSdkPath.length() > 0 && new File(osSdkPath).isDirectory()) {
+            // Verify that the SDK is valid
+            boolean ok = AdtPlugin.getDefault().checkSdkLocationAndId(osSdkPath,
+                    new AdtPlugin.CheckSdkErrorHandler() {
+                @Override
+                public boolean handleError(String message) {
+                    return false;
+                }
+
+                @Override
+                public boolean handleWarning(String message) {
+                    return true;
+                }
+            });
+            if (ok) {
+                // Yes, we've seen an SDK location before and we can use it again, no need to
+                // pester the user with the welcome wizard. (This also implies that the user
+                // has responded to the usage statistics question.)
+                AdtPrefs.getPrefs().setSdkLocation(new File(osSdkPath));
+                return false;
+            }
+        }
+
+        // Check whether we've run this wizard before.
+        return !mStore.isAdtUsed();
+    }
+
+    private void showWelcomeWizard() {
+        final IWorkbench workbench = PlatformUI.getWorkbench();
+        workbench.getDisplay().asyncExec(new Runnable() {
+            public void run() {
+                IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
+                if (window != null) {
+                    WelcomeWizard wizard = new WelcomeWizard(mStore);
+                    WizardDialog dialog = new WizardDialog(window.getShell(), wizard);
+                    dialog.open();
+                }
+
+                // Record the fact that we've run the wizard so we don't attempt to do it again,
+                // even if the user just cancels out of the wizard.
+                mStore.setAdtUsed(true);
+
+                if (mStore.isPingOptIn()) {
+                    sendUsageStats();
+                }
+            }
+        });
+    }
+
+    private void sendUsageStats() {
+        // Ping the usage server and parse the SDK content.
+        // This is deferred in separate jobs to avoid blocking the bundle start.
+        // We also serialize them to avoid too many parallel jobs when Eclipse starts.
+        Job pingJob = createPingUsageServerJob();
+        // build jobs are run after other interactive jobs
+        pingJob.setPriority(Job.BUILD);
+        // Wait another 30 seconds before starting the ping job. This gives other
+        // startup tasks time to finish since it's not vital to get the usage ping
+        // immediately.
+        pingJob.schedule(30000 /*milliseconds*/);
+    }
+
+    /**
+     * Creates a job than can ping the usage server.
+     */
+    private Job createPingUsageServerJob() {
+        // In order to not block the plugin loading, so we spawn another thread.
+        Job job = new Job("Android SDK Ping") {  // Job name, visible in progress view
+            @Override
+            protected IStatus run(IProgressMonitor monitor) {
+                try {
+                    pingUsageServer();
+
+                    return Status.OK_STATUS;
+                } catch (Throwable t) {
+                    AdtPlugin.log(t, "pingUsageServer failed");       //$NON-NLS-1$
+                    return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+                            "pingUsageServer failed", t);    //$NON-NLS-1$
+                }
+            }
+        };
+        return job;
+    }
+
+    private static Version getVersion(Plugin plugin) {
+        @SuppressWarnings("cast") // Cast required in Eclipse 3.5; prevent auto-removal in 3.7
+        String version = (String) plugin.getBundle().getHeaders().get(Constants.BUNDLE_VERSION);
+        // Parse the string using the Version class.
+        return new Version(version);
+    }
+
+    /**
+     * Pings the usage start server.
+     */
+    private void pingUsageServer() {
+        // Report the version of the ADT plugin to the stat server
+        Version version = getVersion(AdtPlugin.getDefault());
+        String adtVersionString = String.format("%1$d.%2$d.%3$d", version.getMajor(), //$NON-NLS-1$
+                version.getMinor(), version.getMicro());
+
+        // Report the version of Eclipse to the stat server.
+        // Get the version of eclipse by getting the version of one of the runtime plugins.
+        Version eclipseVersion = getVersion(ResourcesPlugin.getPlugin());
+        String eclipseVersionString = String.format("%1$d.%2$d",  //$NON-NLS-1$
+                eclipseVersion.getMajor(), eclipseVersion.getMinor());
+
+        SdkStatsService stats = new SdkStatsService();
+        stats.ping("adt", adtVersionString); //$NON-NLS-1$
+        stats.ping("eclipse", eclipseVersionString); //$NON-NLS-1$
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/UsagePermissionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/UsagePermissionPage.java
new file mode 100644 (file)
index 0000000..906e725
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.welcome;
+
+import com.android.sdkstats.SdkStatsPermissionDialog;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.browser.IWebBrowser;
+
+import java.net.URL;
+
+/** Page which displays the permission dialog for collecting usage statistics */
+public class UsagePermissionPage extends WizardPage implements SelectionListener {
+    private Button mSendCheckbox;
+    private Link mLink;
+
+    /**
+     * Create the wizard.
+     */
+    public UsagePermissionPage() {
+        super("usageData");
+        setTitle("Contribute Usage Statistics?");
+        setDescription(SdkStatsPermissionDialog.NOTICE_TEXT);
+    }
+
+    /**
+     * Create contents of the wizard.
+     *
+     * @param parent parent to create page into
+     */
+    public void createControl(Composite parent) {
+        Composite container = new Composite(parent, SWT.NULL);
+
+        setControl(container);
+        container.setLayout(new GridLayout(1, false));
+
+        Label label = new Label(container, SWT.WRAP);
+        GridData gd_lblByChoosingTo = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1);
+        gd_lblByChoosingTo.widthHint = 580;
+        label.setLayoutData(gd_lblByChoosingTo);
+        label.setText(SdkStatsPermissionDialog.BODY_TEXT);
+
+        mSendCheckbox = new Button(container, SWT.CHECK);
+        mSendCheckbox.setText(SdkStatsPermissionDialog.CHECKBOX_TEXT);
+
+        Label laterLabel = new Label(container, SWT.WRAP);
+        GridData gdLaterLabel = new GridData(SWT.FILL, SWT.BOTTOM, false, true, 1, 1);
+        gdLaterLabel.widthHint = 580;
+        laterLabel.setLayoutData(gdLaterLabel);
+        laterLabel.setText(SdkStatsPermissionDialog.FOOTER_TEXT);
+
+        mLink = new Link(container, SWT.NONE);
+        mLink.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+        mLink.setText(SdkStatsPermissionDialog.PRIVACY_POLICY_LINK_TEXT);
+        mLink.addSelectionListener(this);
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        super.setVisible(visible);
+        mSendCheckbox.setFocus();
+    }
+
+    boolean isUsageCollectionApproved() {
+        return mSendCheckbox.getSelection();
+    }
+
+    public void widgetSelected(SelectionEvent event) {
+        if (event.getSource() == mLink) {
+            try {
+                IWorkbench workbench = PlatformUI.getWorkbench();
+                IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser();
+                browser.openURL(new URL(event.text));
+            } catch (Exception e) {
+                String message = String.format("Could not open browser. Vist\n%1$s\ninstead.",
+                        event.text);
+                MessageDialog.openError(getWizard().getContainer().getShell(),
+                        "Browser Error", message);
+            }
+        }
+    }
+
+    public void widgetDefaultSelected(SelectionEvent e) {
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizard.java
new file mode 100644 (file)
index 0000000..9aa5ead
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.welcome;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog;
+import com.android.sdkstats.DdmsPreferenceStore;
+import com.android.sdkuilib.internal.repository.sdkman2.AdtUpdateDialog;
+import com.android.util.Pair;
+
+import org.eclipse.jface.wizard.Wizard;
+
+import java.io.File;
+
+/**
+ * Wizard shown on first start for new users: configure SDK location, accept or
+ * reject usage data collection, etc
+ */
+public class WelcomeWizard extends Wizard {
+    private final DdmsPreferenceStore mStore;
+    private WelcomeWizardPage mWelcomePage;
+    private UsagePermissionPage mUsagePage;
+
+    /**
+     * Creates a new {@link WelcomeWizard}
+     *
+     * @param store preferences for usage statistics collection etc
+     */
+    public WelcomeWizard(DdmsPreferenceStore store) {
+        mStore = store;
+
+        setWindowTitle("Welcome to Android Development");
+    }
+
+    @Override
+    public void addPages() {
+        mWelcomePage = new WelcomeWizardPage();
+        addPage(mWelcomePage);
+
+        // It's possible that the user has already run the command line tools
+        // such as ddms and has agreed to usage statistics collection, but has never
+        // run ADT which is why the wizard was opened. No need to ask again.
+        if (!mStore.isPingOptIn()) {
+            mUsagePage = new UsagePermissionPage();
+            addPage(mUsagePage);
+        }
+    }
+
+    @Override
+    public boolean performFinish() {
+        if (mUsagePage != null) {
+            boolean isUsageCollectionApproved = mUsagePage.isUsageCollectionApproved();
+            DdmsPreferenceStore store = new DdmsPreferenceStore();
+            store.setPingOptIn(isUsageCollectionApproved);
+        }
+
+        // Read out wizard settings immediately; we will perform the actual work
+        // after the wizard window has been taken down and it's too late to read the
+        // settings then
+        final File path = mWelcomePage.getPath();
+        final boolean installCommon = mWelcomePage.isInstallCommon();
+        final boolean installLatest = mWelcomePage.isInstallLatest();
+        final boolean createNew = mWelcomePage.isCreateNew();
+
+        // Perform installation asynchronously since it takes a while.
+        getShell().getDisplay().asyncExec(new Runnable() {
+            public void run() {
+                if (createNew) {
+                    try {
+                        if (installCommon) {
+                            installSdk(path, 7);
+                        }
+                        if (installLatest) {
+                            installSdk(path, AdtUpdateDialog.USE_MAX_REMOTE_API_LEVEL);
+                        }
+                    } catch (Exception e) {
+                        AdtPlugin.logAndPrintError(e, "ADT Welcome Wizard", "Installation failed");
+                    }
+                }
+
+                // Set SDK path after installation since this will trigger a SDK refresh.
+                AdtPrefs.getPrefs().setSdkLocation(path);
+            }
+        });
+
+        // The wizard always succeeds, even if installation fails or is aborted
+        return true;
+    }
+
+    /**
+     * Trigger the install window. It will connect to the repository, display
+     * a confirmation window showing which packages are selected for install
+     * and display a progress dialog during installation.
+     */
+    private boolean installSdk(File path, int apiLevel) {
+        if (!path.isDirectory()) {
+            if (!path.mkdirs()) {
+                AdtPlugin.logAndPrintError(null, "ADT Welcome Wizard",
+                        "Failed to create directory %1$s",
+                        path.getAbsolutePath());
+                return false;
+            }
+        }
+
+        AdtUpdateDialog updater = new AdtUpdateDialog(
+                AdtPlugin.getDisplay().getActiveShell(),
+                new AdtConsoleSdkLog(),
+                path.getAbsolutePath());
+        // Note: we don't have to specify tools & platform-tools since they
+        // are required dependencies of any platform.
+        Pair<Boolean, File> result = updater.installNewSdk(apiLevel);
+
+        if (!result.getFirst().booleanValue()) {
+            AdtPlugin.printErrorToConsole("Failed to install Android SDK.");
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java
new file mode 100644 (file)
index 0000000..59429de
--- /dev/null
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.welcome;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.DirectoryDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Main page shown in the {@link WelcomeWizard} */
+public class WelcomeWizardPage extends WizardPage implements ModifyListener, SelectionListener {
+    private Text mExistingDirText;
+    private Button mExistingDirButton;
+    private Button mInstallLatestCheckbox;
+    private Button mInstallCommonCheckbox;
+    private Button mInstallNewRadio;
+    private Button mUseExistingRadio;
+    private Text mNewDirText;
+    private Button mNewDirButton;
+
+    /**
+     * Create the wizard.
+     */
+    public WelcomeWizardPage() {
+        super("welcomePage");
+        setTitle("Welcome to Android Development");
+        setDescription("Configure SDK");
+    }
+
+    /**
+     * Create contents of the wizard.
+     * @param parent parent widget to add page to
+     */
+    @SuppressWarnings("unused") // SWT constructors have side effects so "new Label" is not unused
+    public void createControl(Composite parent) {
+        Composite container = new Composite(parent, SWT.NULL);
+
+        setControl(container);
+        container.setLayout(new GridLayout(4, false));
+
+        Label overviewLabel = new Label(container, SWT.WRAP | SWT.SHADOW_NONE);
+        GridData gdOverviewLabel = new GridData(SWT.FILL, SWT.CENTER, false, false, 4, 1);
+        gdOverviewLabel.widthHint = 580;
+        overviewLabel.setLayoutData(gdOverviewLabel);
+        overviewLabel.setText("To develop for Android, you need an Android SDK, and at least one version of the Android APIs to compile against. You may also want additional versions of Android to test with.");
+
+        Label spacing = new Label(container, SWT.NONE);
+        spacing.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+        mInstallNewRadio = new Button(container, SWT.RADIO);
+        mInstallNewRadio.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+        mInstallNewRadio.setSelection(true);
+        mInstallNewRadio.setText("Install new SDK");
+        mInstallNewRadio.addSelectionListener(this);
+
+        Label indentLabel = new Label(container, SWT.NONE);
+        GridData gdIndentLabel = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1);
+        gdIndentLabel.widthHint = 20;
+        indentLabel.setLayoutData(gdIndentLabel);
+
+        mInstallLatestCheckbox = new Button(container, SWT.CHECK);
+        mInstallLatestCheckbox.setSelection(true);
+        mInstallLatestCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3,
+                1));
+        mInstallLatestCheckbox.setText("Install Android 4.0, the latest available version (supports all the latest features)");
+        mInstallLatestCheckbox.addSelectionListener(this);
+
+        new Label(container, SWT.NONE);
+        mInstallCommonCheckbox = new Button(container, SWT.CHECK);
+        mInstallCommonCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3,
+                1));
+        mInstallCommonCheckbox.setText("Install Android 2.1, a version which is supported by ~97% phones and tablets");
+
+        new Label(container, SWT.NONE);
+        Label addHintLabel = new Label(container, SWT.NONE);
+        addHintLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1));
+        addHintLabel.setText("     (You can add additional platforms using the SDK Manager.)");
+
+        new Label(container, SWT.NONE);
+        Label targetLabel = new Label(container, SWT.NONE);
+        targetLabel.setText("Target Location:");
+
+        mNewDirText = new Text(container, SWT.BORDER);
+        mNewDirText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        String defaultPath = System.getProperty("user.home") + File.separator + "android-sdks"; //$NON-NLS-1$
+        mNewDirText.setText(defaultPath);
+        mNewDirText.addModifyListener(this);
+
+        mNewDirButton = new Button(container, SWT.FLAT);
+        mNewDirButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+        mNewDirButton.setText("Browse...");
+        mNewDirButton.addSelectionListener(this);
+
+        Label spacing2 = new Label(container, SWT.NONE);
+        spacing2.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+        mUseExistingRadio = new Button(container, SWT.RADIO);
+        mUseExistingRadio.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+        mUseExistingRadio.setText("Use existing SDKs");
+        mUseExistingRadio.addSelectionListener(this);
+
+        new Label(container, SWT.NONE);
+        Label installationLabel = new Label(container, SWT.NONE);
+        installationLabel.setText("Existing Location:");
+
+        mExistingDirText = new Text(container, SWT.BORDER);
+        mExistingDirText.setEnabled(false);
+        mExistingDirText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mExistingDirText.addModifyListener(this);
+
+        mExistingDirButton = new Button(container, SWT.FLAT);
+        mExistingDirButton.setEnabled(false);
+        mExistingDirButton.setText("Browse...");
+        mExistingDirButton.addSelectionListener(this);
+    }
+
+    boolean isCreateNew() {
+        return mInstallNewRadio.getSelection();
+    }
+
+    boolean isInstallLatest() {
+        return mInstallLatestCheckbox.getSelection();
+    }
+
+    boolean isInstallCommon() {
+        return mInstallCommonCheckbox.getSelection();
+    }
+
+    File getPath() {
+        Text text = isCreateNew() ? mNewDirText : mExistingDirText;
+        return new File(text.getText());
+    }
+
+    public void widgetSelected(SelectionEvent e) {
+        Object source = e.getSource();
+
+        if (source == mExistingDirButton) {
+            DirectoryDialog dialog = new DirectoryDialog(mExistingDirButton.getShell(), SWT.OPEN);
+            String file = dialog.open();
+            String path = mExistingDirText.getText().trim();
+            if (path.length() > 0) {
+                dialog.setFilterPath(path);
+            }
+            if (file != null) {
+                mExistingDirText.setText(file);
+            }
+        } else if (source == mNewDirButton) {
+            DirectoryDialog dialog = new DirectoryDialog(mNewDirButton.getShell(), SWT.OPEN);
+            String path = mNewDirText.getText().trim();
+            if (path.length() > 0) {
+                dialog.setFilterPath(path);
+            }
+            String file = dialog.open();
+            if (file != null) {
+                mNewDirText.setText(file);
+            }
+        } else if (source == mInstallNewRadio) {
+            mExistingDirButton.setEnabled(false);
+            mExistingDirText.setEnabled(false);
+            mNewDirButton.setEnabled(true);
+            mNewDirText.setEnabled(true);
+        } else if (source == mUseExistingRadio) {
+            mExistingDirButton.setEnabled(true);
+            mExistingDirText.setEnabled(true);
+            mNewDirButton.setEnabled(false);
+            mNewDirText.setEnabled(false);
+        }
+
+        validatePage();
+    }
+
+    public void widgetDefaultSelected(SelectionEvent e) {
+    }
+
+    public void modifyText(ModifyEvent e) {
+        validatePage();
+    }
+
+    private void validatePage() {
+        String error = null;
+        String warning = null;
+
+        if (isCreateNew()) {
+            // Make sure that the target installation directory is empty or doesn't exist
+            // (and that it can be created)
+            String path = mNewDirText.getText().trim();
+            if (path.length() == 0) {
+                error = "Please enter a new directory to install the SDK into";
+            } else {
+                File file = new File(path);
+                if (file.exists()) {
+                    if (file.isDirectory()) {
+                        if (!file.canWrite()) {
+                            error = "Missing write permission in target directory";
+                        }
+                        File[] children = file.listFiles();
+                        if (children != null && children.length > 0) {
+                            warning = "The directory is not empty";
+                        }
+                    } else {
+                        error = "The target must be a directory";
+                    }
+                } else {
+                    File parent = file.getParentFile();
+                    if (parent == null || !parent.exists()) {
+                        error = "The parent directory does not exist";
+                    } else if (!parent.canWrite()) {
+                        error = "No write permission in parent directory";
+                    }
+                }
+            }
+
+            if (error == null && !mInstallLatestCheckbox.getSelection()
+                    && !mInstallCommonCheckbox.getSelection()) {
+                error = "You must choose at least one Android version to install";
+            }
+        } else {
+            // Make sure that the existing installation directory exists and is valid
+            String path = mExistingDirText.getText().trim();
+            if (path.length() == 0) {
+                error = "Please enter an existing SDK installation directory";
+            } else {
+                File file = new File(path);
+                if (!file.exists()) {
+                    error = "The chosen installation directory does not exist";
+                } else {
+                    final AtomicReference<String> errorReference = new AtomicReference<String>();
+                    final AtomicReference<String> warningReference = new AtomicReference<String>();
+                    AdtPlugin.getDefault().checkSdkLocationAndId(path,
+                            new AdtPlugin.CheckSdkErrorHandler() {
+                        @Override
+                        public boolean handleError(String message) {
+                            message = message.replaceAll("\n", " "); //$NON-NLS-1$ //$NON-NLS-2$
+                            errorReference.set(message);
+                            return false;  // Apply/OK must be disabled
+                        }
+
+                        @Override
+                        public boolean handleWarning(String message) {
+                            message = message.replaceAll("\n", " "); //$NON-NLS-1$ //$NON-NLS-2$
+                            warningReference.set(message);
+                            return true;  // Apply/OK must be enabled
+                        }
+                    });
+                    error = errorReference.get();
+                    if (warning == null) {
+                        warning = warningReference.get();
+                    }
+                }
+            }
+        }
+
+        setPageComplete(error == null);
+        if (error != null) {
+            setMessage(error, IMessageProvider.ERROR);
+        } else if (warning != null) {
+            setMessage(warning, IMessageProvider.WARNING);
+        } else {
+            setErrorMessage(null);
+            setMessage(null);
+        }
+    }
+}
index 4374793..2a34ded 100755 (executable)
@@ -38,7 +38,7 @@ public class DdmsPreferenceStore {
     private final static String PING_TIME   = "pingTime";           //$NON-NLS-1$
     private final static String PING_ID     = "pingId";             //$NON-NLS-1$
 
-    private final static String ADT_FIRST_TIME = "adt1stTime";      //$NON-NLS-1$
+    private final static String ADT_USED = "adtUsed";               //$NON-NLS-1$
     private final static String LAST_SDK_PATH = "lastSdkPath";      //$NON-NLS-1$
 
     /**
@@ -146,7 +146,7 @@ public class DdmsPreferenceStore {
      * Retrieves the current ping ID, if set.
      * To know if the ping ID is set, use {@link #hasPingId()}.
      * <p/>
-     * There is no magic value reserved for "missing pind id or invalid store".
+     * There is no magic value reserved for "missing ping id or invalid store".
      * The only proper way to know if the ping id is missing is to use {@link #hasPingId()}.
      */
     public long getPingId() {
@@ -234,7 +234,7 @@ public class DdmsPreferenceStore {
      *
      * @param app The app name identifier.
      * @param timeStamp The time stamp from the store.
-     *                   0L is a sepcial value that should not be used.
+     *                   0L is a special value that should not be used.
      */
     public void setPingTime(String app, long timeStamp) {
         PreferenceStore prefs = getPreferenceStore();
@@ -251,29 +251,33 @@ public class DdmsPreferenceStore {
 
     /**
      * True if this is the first time the users runs ADT, which is detected by
-     * the lack of the setting set using {@link #setAdtFirstTimeUser(boolean)}
+     * the lack of the setting set using {@link #setAdtUsed(boolean)}
      * or this value being set to true.
      *
-     * @see #setAdtFirstTimeUser(boolean)
+     * @return true if ADT has been used  before
+     *
+     * @see #setAdtUsed(boolean)
      */
-    public boolean isAdtFirstTimeUser() {
+    public boolean isAdtUsed() {
         PreferenceStore prefs = getPreferenceStore();
         synchronized (DdmsPreferenceStore.class) {
-            if (prefs == null || !prefs.contains(ADT_FIRST_TIME)) {
-                return true;
+            if (prefs == null || !prefs.contains(ADT_USED)) {
+                return false;
             }
-            return prefs.getBoolean(ADT_FIRST_TIME);
+            return prefs.getBoolean(ADT_USED);
         }
     }
 
     /**
      * Sets whether the ADT startup wizard has been shown.
      * ADT sets first to false once the welcome wizard has been shown once.
+     *
+     * @param used true if ADT has been used
      */
-    public void setAdtFirstTimeUser(boolean shown) {
+    public void setAdtUsed(boolean used) {
         PreferenceStore prefs = getPreferenceStore();
         synchronized (DdmsPreferenceStore.class) {
-            prefs.setValue(ADT_FIRST_TIME, shown);
+            prefs.setValue(ADT_USED, used);
             try {
                 prefs.save();
             } catch (IOException ioe) {
index 9f6fdf6..4452493 100644 (file)
@@ -43,7 +43,8 @@ public class SdkStatsPermissionDialog extends Dialog {
     private static final String HEADER_TEXT =
         "Thanks for using the Android SDK!";
 
-    private static final String NOTICE_TEXT =
+    /** Used in the ADT welcome wizard as well. */
+    public 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. */
@@ -56,6 +57,7 @@ public class SdkStatsPermissionDialog extends Dialog {
         "with personal information about you, and is examined on an aggregate " +
         "basis, and is maintained in accordance with the Google Privacy Policy.";
 
+    /** Used in the ADT welcome wizard as well. */
     public static final String PRIVACY_POLICY_LINK_TEXT =
         "<a href=\"http://www.google.com/intl/en/privacy.html\">Google " +
         "Privacy Policy</a>";
@@ -64,7 +66,8 @@ public class SdkStatsPermissionDialog extends Dialog {
     public static final String CHECKBOX_TEXT =
         "Send usage statistics to Google.";
 
-    private static final String FOOTER_TEXT =
+    /** Used in the ADT welcome wizard as well. */
+    public 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\".";