1 /*
2  * Copyright (C) 2009 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.actions;
18 
19 import com.android.SdkConstants;
20 import com.android.annotations.NonNull;
21 import com.android.annotations.Nullable;
22 import com.android.ide.eclipse.adt.AdtPlugin;
23 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
24 import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog;
25 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
26 import com.android.sdklib.io.FileOp;
27 import com.android.sdklib.repository.ISdkChangeListener;
28 import com.android.utils.GrabProcessOutput;
29 import com.android.utils.GrabProcessOutput.IProcessOutput;
30 import com.android.utils.GrabProcessOutput.Wait;
31 import com.android.sdkuilib.repository.SdkUpdaterWindow;
32 import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext;
33 
34 import org.eclipse.core.runtime.IProgressMonitor;
35 import org.eclipse.jface.action.IAction;
36 import org.eclipse.jface.dialogs.IDialogConstants;
37 import org.eclipse.jface.dialogs.ProgressMonitorDialog;
38 import org.eclipse.jface.operation.IRunnableWithProgress;
39 import org.eclipse.jface.viewers.ISelection;
40 import org.eclipse.swt.widgets.Display;
41 import org.eclipse.swt.widgets.Shell;
42 import org.eclipse.ui.IObjectActionDelegate;
43 import org.eclipse.ui.IWorkbenchPart;
44 import org.eclipse.ui.IWorkbenchWindow;
45 import org.eclipse.ui.IWorkbenchWindowActionDelegate;
46 
47 import java.io.File;
48 import java.lang.reflect.InvocationTargetException;
49 import java.util.concurrent.atomic.AtomicBoolean;
50 
51 /**
52  * Delegate for the toolbar/menu action "Android SDK Manager".
53  * It displays the Android SDK Manager.
54  */
55 public class SdkManagerAction implements IWorkbenchWindowActionDelegate, IObjectActionDelegate {
56 
57     @Override
dispose()58     public void dispose() {
59         // nothing to dispose.
60     }
61 
62     @Override
init(IWorkbenchWindow window)63     public void init(IWorkbenchWindow window) {
64         // no init
65     }
66 
67     @Override
run(IAction action)68     public void run(IAction action) {
69         // Although orthogonal to the sdk manager action, this is a good time
70         // to check whether the SDK has changed on disk.
71         AdtPlugin.getDefault().refreshSdk();
72 
73         if (!openExternalSdkManager()) {
74             // If we failed to execute the sdk manager, check the SDK location.
75             // If it's not properly set, the check will display a dialog to state
76             // so to the user and a link to the prefs.
77             // Here's it's ok to call checkSdkLocationAndId() since it will not try
78             // to run the SdkManagerAction (it might run openExternalSdkManager though.)
79             // If checkSdkLocationAndId tries to open the SDK Manager, it end up using
80             // the internal one.
81             if (AdtPlugin.getDefault().checkSdkLocationAndId()) {
82                 // The SDK check was successful, yet the sdk manager fail to launch anyway.
83                 AdtPlugin.displayError(
84                         "Android SDK",
85                         "Failed to run the Android SDK Manager. Check the Android Console View for details.");
86             }
87         }
88     }
89 
90     /**
91      * A custom implementation of {@link ProgressMonitorDialog} that allows us
92      * to rename the "Cancel" button to "Close" from the internal task.
93      */
94     private static class CloseableProgressMonitorDialog extends ProgressMonitorDialog {
95 
CloseableProgressMonitorDialog(Shell parent)96         public CloseableProgressMonitorDialog(Shell parent) {
97             super(parent);
98         }
99 
changeCancelToClose()100         public void changeCancelToClose() {
101             if (cancel != null && !cancel.isDisposed()) {
102                 Display display = getShell() == null ? null : getShell().getDisplay();
103                 if (display != null) {
104                     display.syncExec(new Runnable() {
105                         @Override
106                         public void run() {
107                             if (cancel != null && !cancel.isDisposed()) {
108                                 cancel.setText(IDialogConstants.CLOSE_LABEL);
109                             }
110                         }
111                     });
112                 }
113             }
114         }
115     }
116 
117     /**
118      * Opens the SDK Manager as an external application.
119      * This call is asynchronous, it doesn't wait for the manager to be closed.
120      * <p/>
121      * Important: this method must NOT invoke {@link AdtPlugin#checkSdkLocationAndId}
122      * (in any of its variations) since the dialog uses this method to invoke the sdk
123      * manager if needed.
124      *
125      * @return True if the application was found and executed. False if it could not
126      *   be located or could not be launched.
127      */
openExternalSdkManager()128     public static boolean openExternalSdkManager() {
129 
130         // On windows this takes a couple seconds and it's not clear the launch action
131         // has been invoked. To prevent the user from randomly clicking the "open sdk manager"
132         // button multiple times, show a progress window that will automatically close
133         // after a couple seconds.
134 
135         // By default openExternalSdkManager will return false.
136         final AtomicBoolean returnValue = new AtomicBoolean(false);
137 
138         final CloseableProgressMonitorDialog p =
139             new CloseableProgressMonitorDialog(AdtPlugin.getShell());
140         p.setOpenOnRun(true);
141         try {
142             p.run(true /*fork*/, true /*cancelable*/, new IRunnableWithProgress() {
143                 @Override
144                 public void run(IProgressMonitor monitor)
145                         throws InvocationTargetException, InterruptedException {
146 
147                     // Get the SDK locatiom from the current SDK or as fallback
148                     // directly from the ADT preferences.
149                     Sdk sdk = Sdk.getCurrent();
150                     String osSdkLocation = sdk == null ? null : sdk.getSdkOsLocation();
151                     if (osSdkLocation == null || !new File(osSdkLocation).isDirectory()) {
152                         osSdkLocation = AdtPrefs.getPrefs().getOsSdkFolder();
153                     }
154 
155                     // If there's no SDK location or it's not a valid directory,
156                     // there's nothing we can do. When this is invoked from run()
157                     // the checkSdkLocationAndId method call should display a dialog
158                     // telling the user to configure the preferences.
159                     if (osSdkLocation == null || !new File(osSdkLocation).isDirectory()) {
160                         return;
161                     }
162 
163                     final int numIter = 30;  //30*100=3s to wait for window
164                     final int sleepMs = 100;
165                     monitor.beginTask("Starting Android SDK Manager", numIter);
166 
167                     File androidBat = FileOp.append(
168                             osSdkLocation,
169                             SdkConstants.FD_TOOLS,
170                             SdkConstants.androidCmdName());
171 
172                     if (!androidBat.exists()) {
173                         AdtPlugin.printErrorToConsole("SDK Manager",
174                                 "Missing %s file in Android SDK.", SdkConstants.androidCmdName());
175                         return;
176                     }
177 
178                     if (monitor.isCanceled()) {
179                         // Canceled by user; return true as if it succeeded.
180                         returnValue.set(true);
181                         return;
182                     }
183 
184                     p.changeCancelToClose();
185 
186                     try {
187                         final AdtConsoleSdkLog logger = new AdtConsoleSdkLog();
188 
189                         String command[] = new String[] {
190                                 androidBat.getAbsolutePath(),
191                                 "sdk"   //$NON-NLS-1$
192                         };
193                         Process process = Runtime.getRuntime().exec(command);
194                         GrabProcessOutput.grabProcessOutput(
195                                 process,
196                                 Wait.ASYNC,
197                                 new IProcessOutput() {
198                                     @Override
199                                     public void out(@Nullable String line) {
200                                         // Ignore stdout
201                                     }
202 
203                                     @Override
204                                     public void err(@Nullable String line) {
205                                         if (line != null) {
206                                             logger.info("[SDK Manager] %s", line);
207                                         }
208                                     }
209                                 });
210 
211                         // Set openExternalSdkManager to return true.
212                         returnValue.set(true);
213                     } catch (Exception ignore) {
214                     }
215 
216                     // This small wait prevents the progress dialog from closing too fast.
217                     for (int i = 0; i < numIter; i++) {
218                         if (monitor.isCanceled()) {
219                             // Canceled by user; return true as if it succeeded.
220                             returnValue.set(true);
221                             return;
222                         }
223                         if (i == 10) {
224                             monitor.subTask("Initializing... SDK Manager will show up shortly.");
225                         }
226                         try {
227                             Thread.sleep(sleepMs);
228                             monitor.worked(1);
229                         } catch (InterruptedException e) {
230                             // ignore
231                         }
232                     }
233 
234                     monitor.done();
235                 }
236             });
237         } catch (Exception e) {
238             AdtPlugin.log(e, "SDK Manager exec failed");    //$NON-NLS-1#
239             return false;
240         }
241 
242         return returnValue.get();
243     }
244 
245     /**
246      * Opens the SDK Manager bundled within ADT.
247      * The call is blocking and does not return till the SD Manager window is closed.
248      *
249      * @return True if the SDK location is known and the SDK Manager was started.
250      *   False if the SDK location is not set and we can't open a SDK Manager to
251      *   manage files in an unknown location.
252      */
openAdtSdkManager()253     public static boolean openAdtSdkManager() {
254         final Sdk sdk = Sdk.getCurrent();
255         if (sdk == null) {
256             return false;
257         }
258 
259         // Runs the updater window, directing only warning/errors logs to the ADT console
260         // (normal log is just dropped, which is fine since the SDK Manager has its own
261         // log window now.)
262 
263         SdkUpdaterWindow window = new SdkUpdaterWindow(
264                 AdtPlugin.getShell(),
265                 new AdtConsoleSdkLog() {
266                     @Override
267                     public void info(@NonNull String msgFormat, Object... args) {
268                         // Do not show non-error/warning log in Eclipse.
269                     };
270                     @Override
271                     public void verbose(@NonNull String msgFormat, Object... args) {
272                         // Do not show non-error/warning log in Eclipse.
273                     };
274                 },
275                 sdk.getSdkOsLocation(),
276                 SdkInvocationContext.IDE);
277 
278         ISdkChangeListener listener = new ISdkChangeListener() {
279             @Override
280             public void onSdkLoaded() {
281                 // Ignore initial load of the SDK.
282             }
283 
284             /**
285              * Unload all we can from the SDK before new packages are installed.
286              * Typically we need to get rid of references to dx from platform-tools
287              * and to any platform resource data.
288              * <p/>
289              * {@inheritDoc}
290              */
291             @Override
292             public void preInstallHook() {
293 
294                 // TODO we need to unload as much of as SDK as possible. Otherwise
295                 // on Windows we end up with Eclipse locking some files and we can't
296                 // replace them.
297                 //
298                 // At this point, we know what the user wants to install so it would be
299                 // possible to pass in flags to know what needs to be unloaded. Typically
300                 // we need to:
301                 // - unload dex if platform-tools is going to be updated. There's a vague
302                 //   attempt below at removing any references to dex and GCing. Seems
303                 //   to do the trick.
304                 // - unload any target that is going to be updated since it may have
305                 //   resource data used by a current layout editor (e.g. data/*.ttf
306                 //   and various data/res/*.xml).
307                 //
308                 // Most important we need to make sure there isn't a build going on
309                 // and if there is one, either abort it or wait for it to complete and
310                 // then we want to make sure we don't get any attempt to use the SDK
311                 // before the postInstallHook is called.
312 
313                 if (sdk != null) {
314                     sdk.unloadTargetData(true /*preventReload*/);
315                     sdk.unloadDexWrappers();
316                 }
317             }
318 
319             /**
320              * Nothing to do. We'll reparse the SDK later in onSdkReload.
321              * <p/>
322              * {@inheritDoc}
323              */
324             @Override
325             public void postInstallHook() {
326             }
327 
328             /**
329              * Reparse the SDK in case anything was add/removed.
330              * <p/>
331              * {@inheritDoc}
332              */
333             @Override
334             public void onSdkReload() {
335                 AdtPlugin.getDefault().reparseSdk();
336             }
337         };
338 
339         window.addListener(listener);
340         window.open();
341 
342         return true;
343     }
344 
345     @Override
selectionChanged(IAction action, ISelection selection)346     public void selectionChanged(IAction action, ISelection selection) {
347         // nothing related to the current selection.
348     }
349 
350     @Override
setActivePart(IAction action, IWorkbenchPart targetPart)351     public void setActivePart(IAction action, IWorkbenchPart targetPart) {
352         // nothing to do.
353     }
354 }
355