/* * Copyright (C) 2009 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.actions; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.build.DexWrapper; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.io.FileOp; import com.android.sdklib.util.GrabProcessOutput; import com.android.sdklib.util.GrabProcessOutput.IProcessOutput; import com.android.sdklib.util.GrabProcessOutput.Wait; import com.android.sdkuilib.repository.ISdkChangeListener; import com.android.sdkuilib.repository.SdkUpdaterWindow; import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.viewers.ISelection; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IObjectActionDelegate; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.IWorkbenchWindowActionDelegate; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.util.concurrent.atomic.AtomicBoolean; /** * Delegate for the toolbar/menu action "Android SDK Manager". * It displays the Android SDK Manager. */ public class SdkManagerAction implements IWorkbenchWindowActionDelegate, IObjectActionDelegate { @Override public void dispose() { // nothing to dispose. } @Override public void init(IWorkbenchWindow window) { // no init } @Override public void run(IAction action) { // Although orthogonal to the sdk manager action, this is a good time // to check whether the SDK has changed on disk. AdtPlugin.getDefault().refreshSdk(); if (!openExternalSdkManager()) { // If we failed to execute the sdk manager, check the SDK location. // If it's not properly set, the check will display a dialog to state // so to the user and a link to the prefs. // Here's it's ok to call checkSdkLocationAndId() since it will not try // to run the SdkManagerAction (it might run openExternalSdkManager though.) // If checkSdkLocationAndId tries to open the SDK Manager, it end up using // the internal one. if (AdtPlugin.getDefault().checkSdkLocationAndId()) { // The SDK check was successful, yet the sdk manager fail to launch anyway. AdtPlugin.displayError( "Android SDK", "Failed to run the Android SDK Manager. Check the Android Console View for details."); } } } /** * A custom implementation of {@link ProgressMonitorDialog} that allows us * to rename the "Cancel" button to "Close" from the internal task. */ private static class CloseableProgressMonitorDialog extends ProgressMonitorDialog { public CloseableProgressMonitorDialog(Shell parent) { super(parent); } public void changeCancelToClose() { if (cancel != null && !cancel.isDisposed()) { Display display = getShell() == null ? null : getShell().getDisplay(); if (display != null) { display.syncExec(new Runnable() { @Override public void run() { if (cancel != null && !cancel.isDisposed()) { cancel.setText(IDialogConstants.CLOSE_LABEL); } } }); } } } } /** * Opens the SDK Manager as an external application. * This call is asynchronous, it doesn't wait for the manager to be closed. * <p/> * Important: this method must NOT invoke {@link AdtPlugin#checkSdkLocationAndId} * (in any of its variations) since the dialog uses this method to invoke the sdk * manager if needed. * * @return True if the application was found and executed. False if it could not * be located or could not be launched. */ public static boolean openExternalSdkManager() { // On windows this takes a couple seconds and it's not clear the launch action // has been invoked. To prevent the user from randomly clicking the "open sdk manager" // button multiple times, show a progress window that will automatically close // after a couple seconds. // By default openExternalSdkManager will return false. final AtomicBoolean returnValue = new AtomicBoolean(false); final CloseableProgressMonitorDialog p = new CloseableProgressMonitorDialog(AdtPlugin.getShell()); p.setOpenOnRun(true); try { p.run(true /*fork*/, true /*cancelable*/, new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { // Get the SDK locatiom from the current SDK or as fallback // directly from the ADT preferences. Sdk sdk = Sdk.getCurrent(); String osSdkLocation = sdk == null ? null : sdk.getSdkLocation(); if (osSdkLocation == null || !new File(osSdkLocation).isDirectory()) { osSdkLocation = AdtPrefs.getPrefs().getOsSdkFolder(); } // If there's no SDK location or it's not a valid directory, // there's nothing we can do. When this is invoked from run() // the checkSdkLocationAndId method call should display a dialog // telling the user to configure the preferences. if (osSdkLocation == null || !new File(osSdkLocation).isDirectory()) { return; } final int numIter = 30; //30*100=3s to wait for window final int sleepMs = 100; monitor.beginTask("Starting Android SDK Manager", numIter); File androidBat = FileOp.append( osSdkLocation, SdkConstants.FD_TOOLS, SdkConstants.androidCmdName()); if (!androidBat.exists()) { AdtPlugin.printErrorToConsole("SDK Manager", "Missing %s file in Android SDK.", SdkConstants.androidCmdName()); return; } if (monitor.isCanceled()) { // Canceled by user; return true as if it succeeded. returnValue.set(true); return; } p.changeCancelToClose(); try { final AdtConsoleSdkLog logger = new AdtConsoleSdkLog(); String command[] = new String[] { androidBat.getAbsolutePath(), "sdk" //$NON-NLS-1$ }; Process process = Runtime.getRuntime().exec(command); GrabProcessOutput.grabProcessOutput( process, Wait.ASYNC, new IProcessOutput() { @Override public void out(@Nullable String line) { // Ignore stdout } @Override public void err(@Nullable String line) { if (line != null) { logger.info("[SDK Manager] %s", line); } } }); // Set openExternalSdkManager to return true. returnValue.set(true); } catch (Exception ignore) { } // This small wait prevents the progress dialog from closing too fast. for (int i = 0; i < numIter; i++) { if (monitor.isCanceled()) { // Canceled by user; return true as if it succeeded. returnValue.set(true); return; } if (i == 10) { monitor.subTask("Initializing... SDK Manager will show up shortly."); } try { Thread.sleep(sleepMs); monitor.worked(1); } catch (InterruptedException e) { // ignore } } monitor.done(); } }); } catch (Exception e) { AdtPlugin.log(e, "SDK Manager exec failed"); //$NON-NLS-1# return false; } return returnValue.get(); } /** * Opens the SDK Manager bundled within ADT. * The call is blocking and does not return till the SD Manager window is closed. * * @return True if the SDK location is known and the SDK Manager was started. * False if the SDK location is not set and we can't open a SDK Manager to * manage files in an unknown location. */ public static boolean openAdtSdkManager() { final Sdk sdk = Sdk.getCurrent(); if (sdk == null) { return false; } // Runs the updater window, directing only warning/errors logs to the ADT console // (normal log is just dropped, which is fine since the SDK Manager has its own // log window now.) SdkUpdaterWindow window = new SdkUpdaterWindow( AdtPlugin.getShell(), new AdtConsoleSdkLog() { @Override public void info(@NonNull String msgFormat, Object... args) { // Do not show non-error/warning log in Eclipse. }; @Override public void verbose(@NonNull String msgFormat, Object... args) { // Do not show non-error/warning log in Eclipse. }; }, sdk.getSdkLocation(), SdkInvocationContext.IDE); ISdkChangeListener listener = new ISdkChangeListener() { @Override public void onSdkLoaded() { // Ignore initial load of the SDK. } /** * Unload all we can from the SDK before new packages are installed. * Typically we need to get rid of references to dx from platform-tools * and to any platform resource data. * <p/> * {@inheritDoc} */ @Override public void preInstallHook() { // TODO we need to unload as much of as SDK as possible. Otherwise // on Windows we end up with Eclipse locking some files and we can't // replace them. // // At this point, we know what the user wants to install so it would be // possible to pass in flags to know what needs to be unloaded. Typically // we need to: // - unload dex if platform-tools is going to be updated. There's a vague // attempt below at removing any references to dex and GCing. Seems // to do the trick. // - unload any target that is going to be updated since it may have // resource data used by a current layout editor (e.g. data/*.ttf // and various data/res/*.xml). // // Most important we need to make sure there isn't a build going on // and if there is one, either abort it or wait for it to complete and // then we want to make sure we don't get any attempt to use the SDK // before the postInstallHook is called. if (sdk != null) { sdk.unloadTargetData(true /*preventReload*/); DexWrapper dx = sdk.getDexWrapper(); dx.unload(); } } /** * Nothing to do. We'll reparse the SDK later in onSdkReload. * <p/> * {@inheritDoc} */ @Override public void postInstallHook() { } /** * Reparse the SDK in case anything was add/removed. * <p/> * {@inheritDoc} */ @Override public void onSdkReload() { AdtPlugin.getDefault().reparseSdk(); } }; window.addListener(listener); window.open(); return true; } @Override public void selectionChanged(IAction action, ISelection selection) { // nothing related to the current selection. } @Override public void setActivePart(IAction action, IWorkbenchPart targetPart) { // nothing to do. } }