1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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.ide.eclipse.adt.internal.welcome;
18 
19 import com.android.SdkConstants;
20 import com.android.annotations.Nullable;
21 import com.android.ide.eclipse.adt.AdtPlugin;
22 import com.android.ide.eclipse.adt.AdtPlugin.CheckSdkErrorHandler;
23 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutWindowCoordinator;
24 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
25 import com.android.ide.eclipse.base.InstallDetails;
26 import com.android.utils.GrabProcessOutput;
27 import com.android.utils.GrabProcessOutput.IProcessOutput;
28 import com.android.utils.GrabProcessOutput.Wait;
29 import com.android.sdkstats.DdmsPreferenceStore;
30 import com.android.sdkstats.SdkStatsService;
31 
32 import org.eclipse.core.runtime.IProgressMonitor;
33 import org.eclipse.core.runtime.IStatus;
34 import org.eclipse.core.runtime.Platform;
35 import org.eclipse.core.runtime.Plugin;
36 import org.eclipse.core.runtime.Status;
37 import org.eclipse.core.runtime.jobs.Job;
38 import org.eclipse.jface.wizard.WizardDialog;
39 import org.eclipse.osgi.service.datalocation.Location;
40 import org.eclipse.ui.IStartup;
41 import org.eclipse.ui.IWindowListener;
42 import org.eclipse.ui.IWorkbench;
43 import org.eclipse.ui.IWorkbenchWindow;
44 import org.eclipse.ui.PlatformUI;
45 import org.osgi.framework.Constants;
46 import org.osgi.framework.Version;
47 
48 import java.io.File;
49 import java.io.IOException;
50 import java.util.concurrent.atomic.AtomicReference;
51 import java.util.regex.Matcher;
52 import java.util.regex.Pattern;
53 
54 /**
55  * ADT startup tasks (other than those performed in {@link AdtPlugin#start(org.osgi.framework.BundleContext)}
56  * when the plugin is initializing.
57  * <p>
58  * The main tasks currently performed are:
59  * <ul>
60  *   <li> See if the user has ever run the welcome wizard, and if not, run it
61  *   <li> Ping the usage statistics server, if enabled by the user. This is done here
62  *       rather than during the plugin start since this task is run later (when the workspace
63  *       is fully initialized) and we want to ask the user for permission for usage
64  *       tracking before running it (and if we don't, then the usage tracking permissions
65  *       dialog will run instead.)
66  * </ul>
67  */
68 public class AdtStartup implements IStartup, IWindowListener {
69 
70     private DdmsPreferenceStore mStore = new DdmsPreferenceStore();
71 
72     @Override
earlyStartup()73     public void earlyStartup() {
74         if (!isSdkSpecified()) {
75             File bundledSdk = getBundledSdk();
76             if (bundledSdk != null) {
77                 AdtPrefs.getPrefs().setSdkLocation(bundledSdk);
78             }
79         }
80 
81         boolean showSdkInstallationPage = !isSdkSpecified() && isFirstTime();
82         boolean showOptInDialogPage = !mStore.hasPingId();
83 
84         if (showSdkInstallationPage || showOptInDialogPage) {
85             showWelcomeWizard(showSdkInstallationPage, showOptInDialogPage);
86         }
87 
88         if (mStore.isPingOptIn()) {
89             sendUsageStats();
90         }
91 
92         initializeWindowCoordinator();
93 
94         AdtPlugin.getDefault().workbenchStarted();
95     }
96 
isSdkSpecified()97     private boolean isSdkSpecified() {
98         String osSdkFolder = AdtPrefs.getPrefs().getOsSdkFolder();
99         return (osSdkFolder != null && !osSdkFolder.isEmpty());
100     }
101 
102     /**
103      * Returns the path to the bundled SDK if this is part of the ADT package.
104      * The ADT package has the following structure:
105      *   root
106      *      |--eclipse
107      *      |--sdk
108      * @return path to bundled SDK, null if no valid bundled SDK detected.
109      */
getBundledSdk()110     private File getBundledSdk() {
111         Location install = Platform.getInstallLocation();
112         if (install != null && install.getURL() != null) {
113             File toolsFolder = new File(install.getURL().getFile()).getParentFile();
114             if (toolsFolder != null) {
115                 File sdkFolder = new File(toolsFolder, "sdk");
116                 if (sdkFolder.exists() && AdtPlugin.getDefault().checkSdkLocationAndId(
117                         sdkFolder.getAbsolutePath(),
118                         new SdkValidator())) {
119                     return sdkFolder;
120                 }
121             }
122         }
123 
124         return null;
125     }
126 
isFirstTime()127     private boolean isFirstTime() {
128         for (int i = 0; i < 2; i++) {
129             String osSdkPath = null;
130 
131             if (i == 0) {
132                 // If we've recorded an SDK location in the .android settings, then the user
133                 // has run ADT before but possibly in a different workspace. We don't want to pop up
134                 // the welcome wizard each time if we can simply use the existing SDK install.
135                 osSdkPath = mStore.getLastSdkPath();
136             } else if (i == 1) {
137                 osSdkPath = getSdkPathFromWindowsRegistry();
138             }
139 
140             if (osSdkPath != null && osSdkPath.length() > 0) {
141                 boolean ok = new File(osSdkPath).isDirectory();
142 
143                 if (!ok) {
144                     osSdkPath = osSdkPath.trim();
145                     ok = new File(osSdkPath).isDirectory();
146                 }
147 
148                 if (ok) {
149                     // Verify that the SDK is valid
150                     ok = AdtPlugin.getDefault().checkSdkLocationAndId(
151                             osSdkPath, new SdkValidator());
152                     if (ok) {
153                         // Yes, we've seen an SDK location before and we can use it again,
154                         // no need to pester the user with the welcome wizard.
155                         // This also implies that the user has responded to the usage statistics
156                         // question.
157                         AdtPrefs.getPrefs().setSdkLocation(new File(osSdkPath));
158                         return false;
159                     }
160                 }
161             }
162         }
163 
164         // Check whether we've run this wizard before.
165         return !mStore.isAdtUsed();
166     }
167 
168     private static class SdkValidator extends AdtPlugin.CheckSdkErrorHandler {
169         @Override
handleError( CheckSdkErrorHandler.Solution solution, String message)170         public boolean handleError(
171                 CheckSdkErrorHandler.Solution solution,
172                 String message) {
173             return false;
174         }
175 
176         @Override
handleWarning( CheckSdkErrorHandler.Solution solution, String message)177         public boolean handleWarning(
178                 CheckSdkErrorHandler.Solution  solution,
179                 String message) {
180             return true;
181         }
182     }
183 
getSdkPathFromWindowsRegistry()184     private String getSdkPathFromWindowsRegistry() {
185         if (SdkConstants.CURRENT_PLATFORM != SdkConstants.PLATFORM_WINDOWS) {
186             return null;
187         }
188 
189         final String valueName = "Path";                                               //$NON-NLS-1$
190         final AtomicReference<String> result = new AtomicReference<String>();
191         final Pattern regexp =
192             Pattern.compile("^\\s+" + valueName + "\\s+REG_SZ\\s+(.*)$");//$NON-NLS-1$ //$NON-NLS-2$
193 
194         for (String key : new String[] {
195                 "HKLM\\Software\\Android SDK Tools",                                   //$NON-NLS-1$
196                 "HKLM\\Software\\Wow6432Node\\Android SDK Tools" }) {                  //$NON-NLS-1$
197 
198             String[] command = new String[] {
199                 "reg", "query", key, "/v", valueName       //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
200             };
201 
202             Process process;
203             try {
204                 process = Runtime.getRuntime().exec(command);
205 
206                 GrabProcessOutput.grabProcessOutput(
207                         process,
208                         Wait.WAIT_FOR_READERS,
209                         new IProcessOutput() {
210                             @Override
211                             public void out(@Nullable String line) {
212                                 if (line != null) {
213                                     Matcher m = regexp.matcher(line);
214                                     if (m.matches()) {
215                                         result.set(m.group(1));
216                                     }
217                                 }
218                             }
219 
220                             @Override
221                             public void err(@Nullable String line) {
222                                 // ignore stderr
223                             }
224                         });
225             } catch (IOException ignore) {
226             } catch (InterruptedException ignore) {
227             }
228 
229             String str = result.get();
230             if (str != null) {
231                 if (new File(str).isDirectory()) {
232                     return str;
233                 }
234                 str = str.trim();
235                 if (new File(str).isDirectory()) {
236                     return str;
237                 }
238             }
239         }
240 
241         return null;
242     }
243 
initializeWindowCoordinator()244     private void initializeWindowCoordinator() {
245         final IWorkbench workbench = PlatformUI.getWorkbench();
246         workbench.addWindowListener(this);
247         workbench.getDisplay().asyncExec(new Runnable() {
248             @Override
249             public void run() {
250                 for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
251                     LayoutWindowCoordinator.get(window, true /*create*/);
252                 }
253             }
254         });
255     }
256 
showWelcomeWizard(final boolean showSdkInstallPage, final boolean showUsageOptInPage)257     private void showWelcomeWizard(final boolean showSdkInstallPage,
258             final boolean showUsageOptInPage) {
259         final IWorkbench workbench = PlatformUI.getWorkbench();
260         workbench.getDisplay().asyncExec(new Runnable() {
261             @Override
262             public void run() {
263                 IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
264                 if (window != null) {
265                     WelcomeWizard wizard = new WelcomeWizard(mStore, showSdkInstallPage,
266                             showUsageOptInPage);
267                     WizardDialog dialog = new WizardDialog(window.getShell(), wizard);
268                     dialog.open();
269                 }
270 
271                 // Record the fact that we've run the wizard so we don't attempt to do it again,
272                 // even if the user just cancels out of the wizard.
273                 mStore.setAdtUsed(true);
274 
275                 if (mStore.isPingOptIn()) {
276                     sendUsageStats();
277                 }
278             }
279         });
280     }
281 
sendUsageStats()282     private void sendUsageStats() {
283         // Ping the usage server and parse the SDK content.
284         // This is deferred in separate jobs to avoid blocking the bundle start.
285         // We also serialize them to avoid too many parallel jobs when Eclipse starts.
286         Job pingJob = createPingUsageServerJob();
287         // build jobs are run after other interactive jobs
288         pingJob.setPriority(Job.BUILD);
289         // Wait another 30 seconds before starting the ping job. This gives other
290         // startup tasks time to finish since it's not vital to get the usage ping
291         // immediately.
292         pingJob.schedule(30000 /*milliseconds*/);
293     }
294 
295     /**
296      * Creates a job than can ping the usage server.
297      */
createPingUsageServerJob()298     private Job createPingUsageServerJob() {
299         // In order to not block the plugin loading, so we spawn another thread.
300         Job job = new Job("Android SDK Ping") {  // Job name, visible in progress view
301             @Override
302             protected IStatus run(IProgressMonitor monitor) {
303                 try {
304                     pingUsageServer();
305 
306                     return Status.OK_STATUS;
307                 } catch (Throwable t) {
308                     AdtPlugin.log(t, "pingUsageServer failed");       //$NON-NLS-1$
309                     return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
310                             "pingUsageServer failed", t);    //$NON-NLS-1$
311                 }
312             }
313         };
314         return job;
315     }
316 
getVersion(Plugin plugin)317     private static Version getVersion(Plugin plugin) {
318         @SuppressWarnings("cast") // Cast required in Eclipse 3.5; prevent auto-removal in 3.7
319         String version = (String) plugin.getBundle().getHeaders().get(Constants.BUNDLE_VERSION);
320         // Parse the string using the Version class.
321         return new Version(version);
322     }
323 
324     /**
325      * Pings the usage start server.
326      */
pingUsageServer()327     private void pingUsageServer() {
328         // Report the version of the ADT plugin to the stat server
329         Version version = getVersion(AdtPlugin.getDefault());
330         String adtVersionString = String.format("%1$d.%2$d.%3$d", version.getMajor(), //$NON-NLS-1$
331                 version.getMinor(), version.getMicro());
332 
333         // Report the version of Eclipse to the stat server.
334         // Get the version of eclipse by getting the version of one of the runtime plugins.
335         Version eclipseVersion = InstallDetails.getPlatformVersion();
336         String eclipseVersionString = String.format("%1$d.%2$d",  //$NON-NLS-1$
337                 eclipseVersion.getMajor(), eclipseVersion.getMinor());
338 
339         SdkStatsService stats = new SdkStatsService();
340         stats.ping("adt", adtVersionString); //$NON-NLS-1$
341         stats.ping("eclipse", eclipseVersionString); //$NON-NLS-1$
342     }
343 
344     // ---- Implements IWindowListener ----
345 
346     @Override
windowActivated(IWorkbenchWindow window)347     public void windowActivated(IWorkbenchWindow window) {
348     }
349 
350     @Override
windowDeactivated(IWorkbenchWindow window)351     public void windowDeactivated(IWorkbenchWindow window) {
352     }
353 
354     @Override
windowClosed(IWorkbenchWindow window)355     public void windowClosed(IWorkbenchWindow window) {
356         LayoutWindowCoordinator listener = LayoutWindowCoordinator.get(window, false /*create*/);
357         if (listener != null) {
358             listener.dispose();
359         }
360     }
361 
362     @Override
windowOpened(IWorkbenchWindow window)363     public void windowOpened(IWorkbenchWindow window) {
364         LayoutWindowCoordinator.get(window, true /*create*/);
365     }
366 }
367