diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java | 2045 |
1 files changed, 2045 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java new file mode 100644 index 000000000..f7ef41fe3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -0,0 +1,2045 @@ +/* + * Copyright (C) 2007 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; + +import static com.android.SdkConstants.CURRENT_PLATFORM; +import static com.android.SdkConstants.PLATFORM_DARWIN; +import static com.android.SdkConstants.PLATFORM_LINUX; +import static com.android.SdkConstants.PLATFORM_WINDOWS; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.ResourceFile; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.eclipse.adt.AdtPlugin.CheckSdkErrorHandler.Solution; +import com.android.ide.eclipse.adt.internal.VersionCheck; +import com.android.ide.eclipse.adt.internal.actions.SdkManagerAction; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder; +import com.android.ide.eclipse.adt.internal.lint.LintDeltaProcessor; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; +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.GlobalProjectMonitor.IProjectListener; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +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; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.io.StreamException; +import com.android.resources.ResourceFolderType; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.ILogger; +import com.google.common.collect.Sets; +import com.google.common.io.Closeables; + +import org.eclipse.core.commands.Command; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +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.Job; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.browser.IWebBrowser; +import org.eclipse.ui.browser.IWorkbenchBrowserSupport; +import org.eclipse.ui.commands.ICommandService; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleConstants; +import org.eclipse.ui.console.MessageConsole; +import org.eclipse.ui.console.MessageConsoleStream; +import org.eclipse.ui.dialogs.PreferencesUtil; +import org.eclipse.ui.handlers.IHandlerService; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.eclipse.ui.texteditor.AbstractTextEditor; +import org.eclipse.wb.internal.core.DesignerPlugin; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * The activator class controls the plug-in life cycle + */ +public class AdtPlugin extends AbstractUIPlugin implements ILogger { + /** The plug-in ID */ + public static final String PLUGIN_ID = "com.android.ide.eclipse.adt"; //$NON-NLS-1$ + + /** singleton instance */ + private static AdtPlugin sPlugin; + + private static Image sAndroidLogo; + private static ImageDescriptor sAndroidLogoDesc; + + /** The global android console */ + private MessageConsole mAndroidConsole; + + /** Stream to write in the android console */ + private MessageConsoleStream mAndroidConsoleStream; + + /** Stream to write error messages to the android console */ + private MessageConsoleStream mAndroidConsoleErrorStream; + + /** Color used in the error console */ + private Color mRed; + + /** Load status of the SDK. Any access MUST be in a synchronized(mPostLoadProjects) block */ + private LoadStatus mSdkLoadedStatus = LoadStatus.LOADING; + /** Project to update once the SDK is loaded. + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final Set<IJavaProject> mPostLoadProjectsToResolve = Sets.newHashSet(); + /** Project to check validity of cache vs actual once the SDK is loaded. + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final ArrayList<IJavaProject> mPostLoadProjectsToCheck = new ArrayList<IJavaProject>(); + + private GlobalProjectMonitor mResourceMonitor; + private ArrayList<ITargetChangeListener> mTargetChangeListeners = + new ArrayList<ITargetChangeListener>(); + + /** + * This variable indicates that the job inside parseSdkContent() is currently + * trying to load the SDK, to avoid re-entrance. + * To check whether this succeeds or not, please see {@link #getSdkLoadStatus()}. + */ + private volatile boolean mParseSdkContentIsRunning; + + /** + * An error handler for checkSdkLocationAndId() that will handle the generated error + * or warning message. Each method must return a boolean that will in turn be returned by + * checkSdkLocationAndId. + */ + public static abstract class CheckSdkErrorHandler { + + public enum Solution { + NONE, + OPEN_SDK_MANAGER, + OPEN_ANDROID_PREFS, + OPEN_P2_UPDATE + } + + /** + * Handle an error message during sdk location check. Returns whatever + * checkSdkLocationAndId() should returns. + */ + public abstract boolean handleError(Solution solution, String message); + + /** + * Handle a warning message during sdk location check. Returns whatever + * checkSdkLocationAndId() should returns. + */ + public abstract boolean handleWarning(Solution solution, String message); + } + + /** + * The constructor + */ + public AdtPlugin() { + sPlugin = this; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + + // set the default android console. + mAndroidConsole = new MessageConsole("Android", null); //$NON-NLS-1$ + ConsolePlugin.getDefault().getConsoleManager().addConsoles( + new IConsole[] { mAndroidConsole }); + + // get the stream to write in the android console. + mAndroidConsoleStream = mAndroidConsole.newMessageStream(); + mAndroidConsoleErrorStream = mAndroidConsole.newMessageStream(); + + // get the eclipse store + IPreferenceStore eclipseStore = getPreferenceStore(); + AdtPrefs.init(eclipseStore); + + // set the listener for the preference change + eclipseStore.addPropertyChangeListener(new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + // load the new preferences + AdtPrefs.getPrefs().loadValues(event); + + // if the SDK changed, we have to do some extra work + if (AdtPrefs.PREFS_SDK_DIR.equals(event.getProperty())) { + + // finally restart adb, in case it's a different version + DdmsPlugin.setToolsLocation(getOsAbsoluteAdb(), true /* startAdb */, + getOsAbsoluteHprofConv(), getOsAbsoluteTraceview()); + + // get the SDK location and build id. + if (checkSdkLocationAndId()) { + // if sdk if valid, reparse it + + reparseSdk(); + } + } + } + }); + + // load preferences. + AdtPrefs.getPrefs().loadValues(null /*event*/); + + // initialize property-sheet library + DesignerPlugin.initialize( + this, + PLUGIN_ID, + CURRENT_PLATFORM == PLATFORM_WINDOWS, + CURRENT_PLATFORM == PLATFORM_DARWIN, + CURRENT_PLATFORM == PLATFORM_LINUX); + + // initialize editors + startEditors(); + + // Listen on resource file edits for updates to file inclusion + IncludeFinder.start(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + @Override + public void stop(BundleContext context) throws Exception { + super.stop(context); + + stopEditors(); + IncludeFinder.stop(); + + DesignerPlugin.dispose(); + + if (mRed != null) { + mRed.dispose(); + mRed = null; + } + + synchronized (AdtPlugin.class) { + sPlugin = null; + } + } + + /** Called when the workbench has been started */ + public void workbenchStarted() { + // Parse the SDK content. + // This is deferred in separate jobs to avoid blocking the bundle start. + 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*/); + } + + Display display = getDisplay(); + mRed = new Color(display, 0xFF, 0x00, 0x00); + + // because this can be run, in some cases, by a non ui thread, and because + // changing the console properties update the ui, we need to make this change + // in the ui thread. + display.asyncExec(new Runnable() { + @Override + public void run() { + mAndroidConsoleErrorStream.setColor(mRed); + } + }); + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static synchronized AdtPlugin getDefault() { + return sPlugin; + } + + /** + * Returns the current display, if any + * + * @return the display + */ + @NonNull + public static Display getDisplay() { + synchronized (AdtPlugin.class) { + if (sPlugin != null) { + IWorkbench bench = sPlugin.getWorkbench(); + if (bench != null) { + Display display = bench.getDisplay(); + if (display != null) { + return display; + } + } + } + } + + Display display = Display.getCurrent(); + if (display != null) { + return display; + } + + return Display.getDefault(); + } + + /** + * Returns the shell, if any + * + * @return the shell, if any + */ + @Nullable + public static Shell getShell() { + Display display = AdtPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + if (shell == null) { + Shell[] shells = display.getShells(); + if (shells.length > 0) { + shell = shells[0]; + } + } + + return shell; + } + + /** Returns the adb path relative to the sdk folder */ + public static String getOsRelativeAdb() { + return SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER + SdkConstants.FN_ADB; + } + + /** Returns the emulator path relative to the sdk folder */ + public static String getOsRelativeEmulator() { + return SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_EMULATOR; + } + + /** Returns the adb path relative to the sdk folder */ + public static String getOsRelativeProguard() { + return SdkConstants.OS_SDK_TOOLS_PROGUARD_BIN_FOLDER + SdkConstants.FN_PROGUARD; + } + + /** Returns the absolute adb path */ + public static String getOsAbsoluteAdb() { + return getOsSdkFolder() + getOsRelativeAdb(); + } + + /** Returns the absolute traceview path */ + public static String getOsAbsoluteTraceview() { + return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER + + AdtConstants.FN_TRACEVIEW; + } + + /** Returns the absolute emulator path */ + public static String getOsAbsoluteEmulator() { + return getOsSdkFolder() + getOsRelativeEmulator(); + } + + public static String getOsAbsoluteHprofConv() { + return getOsSdkFolder() + SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER + + AdtConstants.FN_HPROF_CONV; + } + + /** Returns the absolute proguard path */ + public static String getOsAbsoluteProguard() { + return getOsSdkFolder() + getOsRelativeProguard(); + } + + /** + * Returns a Url file path to the javaDoc folder. + */ + public static String getUrlDoc() { + return ProjectHelper.getJavaDocPath( + getOsSdkFolder() + AdtConstants.WS_JAVADOC_FOLDER_LEAF); + } + + /** + * Returns the SDK folder. + * Guaranteed to be terminated by a platform-specific path separator. + */ + public static synchronized String getOsSdkFolder() { + if (sPlugin == null) { + return null; + } + + return AdtPrefs.getPrefs().getOsSdkFolder(); + } + + public static String getOsSdkToolsFolder() { + return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER; + } + + /** + * Returns an image descriptor for the image file at the given + * plug-in relative path + * + * @param path the path + * @return the image descriptor + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } + + /** + * Reads the contents of an {@link IFile} and return it as a String + * + * @param file the file to be read + * @return the String read from the file, or null if there was an error + */ + @SuppressWarnings("resource") // Eclipse doesn't understand Closeables.closeQuietly yet + @Nullable + public static String readFile(@NonNull IFile file) { + InputStream contents = null; + InputStreamReader reader = null; + try { + contents = file.getContents(); + String charset = file.getCharset(); + reader = new InputStreamReader(contents, charset); + return readFile(reader); + } catch (CoreException e) { + // pass -- ignore files we can't read + } catch (IOException e) { + // pass -- ignore files we can't read. + + // Note that IFile.getContents() indicates it throws a CoreException but + // experience shows that if the file does not exists it really throws + // IOException. + // New InputStreamReader() throws UnsupportedEncodingException + // which is handled by this IOException catch. + + } finally { + Closeables.closeQuietly(reader); + Closeables.closeQuietly(contents); + } + + return null; + } + + /** + * Reads the contents of an {@link File} and return it as a String + * + * @param file the file to be read + * @return the String read from the file, or null if there was an error + */ + public static String readFile(File file) { + try { + return readFile(new FileReader(file)); + } catch (FileNotFoundException e) { + AdtPlugin.log(e, "Can't read file %1$s", file); //$NON-NLS-1$ + } + + return null; + } + + /** + * Writes the given content out to the given {@link File}. The file will be deleted if + * it already exists. + * + * @param file the target file + * @param content the content to be written into the file + */ + public static void writeFile(File file, String content) { + if (file.exists()) { + file.delete(); + } + FileWriter fw = null; + try { + fw = new FileWriter(file); + fw.write(content); + } catch (IOException e) { + AdtPlugin.log(e, null); + } finally { + if (fw != null) { + try { + fw.close(); + } catch (IOException e) { + AdtPlugin.log(e, null); + } + } + } + } + + /** + * Returns true iff the given file contains the given String. + * + * @param file the file to look for the string in + * @param string the string to be searched for + * @return true if the file is found and contains the given string anywhere within it + */ + @SuppressWarnings("resource") // Closed by streamContains + public static boolean fileContains(IFile file, String string) { + InputStream contents = null; + try { + contents = file.getContents(); + String charset = file.getCharset(); + return streamContains(new InputStreamReader(contents, charset), string); + } catch (Exception e) { + AdtPlugin.log(e, "Can't read file %1$s", file); //$NON-NLS-1$ + } + + return false; + } + + /** + * Returns true iff the given file contains the given String. + * + * @param file the file to look for the string in + * @param string the string to be searched for + * @return true if the file is found and contains the given string anywhere within it + */ + public static boolean fileContains(File file, String string) { + try { + return streamContains(new FileReader(file), string); + } catch (Exception e) { + AdtPlugin.log(e, "Can't read file %1$s", file); //$NON-NLS-1$ + } + + return false; + } + + /** + * Returns true iff the given input stream contains the given String. + * + * @param r the stream to look for the string in + * @param string the string to be searched for + * @return true if the file is found and contains the given string anywhere within it + */ + public static boolean streamContains(Reader r, String string) { + if (string.length() == 0) { + return true; + } + + PushbackReader reader = null; + try { + reader = new PushbackReader(r, string.length()); + char first = string.charAt(0); + while (true) { + int c = reader.read(); + if (c == -1) { + return false; + } else if (c == first) { + boolean matches = true; + for (int i = 1; i < string.length(); i++) { + c = reader.read(); + if (c == -1) { + return false; + } else if (string.charAt(i) != (char)c) { + matches = false; + // Back up the characters that did not match + reader.backup(i-1); + break; + } + } + if (matches) { + return true; + } + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't read stream"); //$NON-NLS-1$ + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't read stream"); //$NON-NLS-1$ + } + } + + return false; + + } + + /** + * A special reader that allows backing up in the input (up to a predefined maximum + * number of characters) + * <p> + * NOTE: This class ONLY works with the {@link #read()} method!! + */ + private static class PushbackReader extends BufferedReader { + /** + * Rolling/circular buffer. Can be a char rather than int since we never store EOF + * in it. + */ + private char[] mStorage; + + /** Points to the head of the queue. When equal to the tail, the queue is empty. */ + private int mHead; + + /** + * Points to the tail of the queue. This will move with each read of the actual + * wrapped reader, and the characters previous to it in the circular buffer are + * the most recently read characters. + */ + private int mTail; + + /** + * Creates a new reader with a given maximum number of backup characters + * + * @param reader the reader to wrap + * @param max the maximum number of characters to allow rollback for + */ + public PushbackReader(Reader reader, int max) { + super(reader); + mStorage = new char[max + 1]; + } + + @Override + public int read() throws IOException { + // Have we backed up? If so we should serve characters + // from the storage + if (mHead != mTail) { + char c = mStorage[mHead]; + mHead = (mHead + 1) % mStorage.length; + return c; + } + assert mHead == mTail; + + // No backup -- read the next character, but stash it into storage + // as well such that we can retrieve it if we must. + int c = super.read(); + mStorage[mHead] = (char) c; + mHead = mTail = (mHead + 1) % mStorage.length; + return c; + } + + /** + * Backs up the reader a given number of characters. The next N reads will yield + * the N most recently read characters prior to this backup. + * + * @param n the number of characters to be backed up + */ + public void backup(int n) { + if (n >= mStorage.length) { + throw new IllegalArgumentException("Exceeded backup limit"); + } + assert n < mStorage.length; + mHead -= n; + if (mHead < 0) { + mHead += mStorage.length; + } + } + } + + /** + * Reads the contents of a {@link ResourceFile} and returns it as a String + * + * @param file the file to be read + * @return the contents as a String, or null if reading failed + */ + public static String readFile(ResourceFile file) { + InputStream contents = null; + try { + contents = file.getFile().getContents(); + return readFile(new InputStreamReader(contents)); + } catch (StreamException e) { + // pass -- ignore files we can't read + } finally { + try { + if (contents != null) { + contents.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't read layout file"); //$NON-NLS-1$ + } + } + + return null; + } + + /** + * Reads the contents of a {@link Reader} and return it as a String. This + * method will close the input reader. + * + * @param reader the reader to be read from + * @return the String read from reader, or null if there was an error + */ + public static String readFile(Reader reader) { + BufferedReader bufferedReader = null; + try { + bufferedReader = new BufferedReader(reader); + StringBuilder sb = new StringBuilder(2000); + while (true) { + int c = bufferedReader.read(); + if (c == -1) { + return sb.toString(); + } else { + sb.append((char)c); + } + } + } catch (IOException e) { + // pass -- ignore files we can't read + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Can't read input stream"); //$NON-NLS-1$ + } + } + + return null; + } + + /** + * Reads and returns the content of a text file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static String readEmbeddedTextFile(String filepath) { + try { + InputStream is = readEmbeddedFileAsStream(filepath); + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + try { + String line; + StringBuilder total = new StringBuilder(reader.readLine()); + while ((line = reader.readLine()) != null) { + total.append('\n'); + total.append(line); + } + + return total.toString(); + } finally { + reader.close(); + } + } + } catch (IOException e) { + // we'll just return null + AdtPlugin.log(e, "Failed to read text file '%s'", filepath); //$NON-NLS-1$ + } + + return null; + } + + /** + * Reads and returns the content of a binary file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static byte[] readEmbeddedFile(String filepath) { + try { + InputStream is = readEmbeddedFileAsStream(filepath); + if (is != null) { + // create a buffered reader to facilitate reading. + BufferedInputStream stream = new BufferedInputStream(is); + try { + // get the size to read. + int avail = stream.available(); + + // create the buffer and reads it. + byte[] buffer = new byte[avail]; + stream.read(buffer); + + // and return. + return buffer; + } finally { + stream.close(); + } + } + } catch (IOException e) { + // we'll just return null;. + AdtPlugin.log(e, "Failed to read binary file '%s'", filepath); //$NON-NLS-1$ + } + + return null; + } + + /** + * Reads and returns the content of a binary file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static InputStream readEmbeddedFileAsStream(String filepath) { + // attempt to read an embedded file + try { + URL url = getEmbeddedFileUrl(AdtConstants.WS_SEP + filepath); + if (url != null) { + return url.openStream(); + } + } catch (MalformedURLException e) { + // we'll just return null. + AdtPlugin.log(e, "Failed to read stream '%s'", filepath); //$NON-NLS-1$ + } catch (IOException e) { + // we'll just return null;. + AdtPlugin.log(e, "Failed to read stream '%s'", filepath); //$NON-NLS-1$ + } + + return null; + } + + /** + * Returns the URL of a binary file embedded in the plugin jar file. + * @param filepath the file path to the text file + * @return null if the file was not found. + */ + public static URL getEmbeddedFileUrl(String filepath) { + Bundle bundle = null; + synchronized (AdtPlugin.class) { + if (sPlugin != null) { + bundle = sPlugin.getBundle(); + } else { + AdtPlugin.log(IStatus.WARNING, "ADT Plugin is missing"); //$NON-NLS-1$ + return null; + } + } + + // attempt to get a file to one of the template. + String path = filepath; + if (!path.startsWith(AdtConstants.WS_SEP)) { + path = AdtConstants.WS_SEP + path; + } + + URL url = bundle.getEntry(path); + + if (url == null) { + AdtPlugin.log(IStatus.INFO, "Bundle file URL not found at path '%s'", path); //$NON-NLS-1$ + } + + return url; + } + + /** + * Displays an error dialog box. This dialog box is ran asynchronously in the ui thread, + * therefore this method can be called from any thread. + * @param title The title of the dialog box + * @param message The error message + */ + public final static void displayError(final String title, final String message) { + // get the current Display + final Display display = getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, title, message); + } + }); + } + + /** + * Displays a warning dialog box. This dialog box is ran asynchronously in the ui thread, + * therefore this method can be called from any thread. + * @param title The title of the dialog box + * @param message The warning message + */ + public final static void displayWarning(final String title, final String message) { + // get the current Display + final Display display = getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openWarning(shell, title, message); + } + }); + } + + /** + * Display a yes/no question dialog box. This dialog is opened synchronously in the ui thread, + * therefore this message can be called from any thread. + * @param title The title of the dialog box + * @param message The error message + * @return true if OK was clicked. + */ + public final static boolean displayPrompt(final String title, final String message) { + // get the current Display and Shell + final Display display = getDisplay(); + + // we need to ask the user what he wants to do. + final boolean[] result = new boolean[1]; + display.syncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + result[0] = MessageDialog.openQuestion(shell, title, message); + } + }); + return result[0]; + } + + /** + * Logs a message to the default Eclipse log. + * + * @param severity The severity code. Valid values are: {@link IStatus#OK}, + * {@link IStatus#ERROR}, {@link IStatus#INFO}, {@link IStatus#WARNING} or + * {@link IStatus#CANCEL}. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static void log(int severity, String format, Object ... args) { + if (format == null) { + return; + } + + String message = String.format(format, args); + Status status = new Status(severity, PLUGIN_ID, message); + + if (getDefault() != null) { + getDefault().getLog().log(status); + } else { + // During UnitTests, we generally don't have a plugin object. It's ok + // to log to stdout or stderr in this case. + (severity < IStatus.ERROR ? System.out : System.err).println(status.toString()); + } + } + + /** + * Logs an exception to the default Eclipse log. + * <p/> + * The status severity is always set to ERROR. + * + * @param exception the exception to log. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static void log(Throwable exception, String format, Object ... args) { + String message = null; + if (format != null) { + message = String.format(format, args); + } else { + message = ""; + } + Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception); + + if (getDefault() != null) { + getDefault().getLog().log(status); + } else { + // During UnitTests, we generally don't have a plugin object. It's ok + // to log to stderr in this case. + System.err.println(status.toString()); + } + } + + /** + * This is a mix between log(Throwable) and printErrorToConsole. + * <p/> + * This logs the exception with an ERROR severity and the given printf-like format message. + * The same message is then printed on the Android error console with the associated tag. + * + * @param exception the exception to log. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static synchronized void logAndPrintError(Throwable exception, String tag, + String format, Object ... args) { + if (sPlugin != null) { + String message = String.format(format, args); + Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception); + getDefault().getLog().log(status); + printToStream(sPlugin.mAndroidConsoleErrorStream, tag, message); + showAndroidConsole(); + } + } + + /** + * Prints one or more error message to the android console. + * @param tag A tag to be associated with the message. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static synchronized void printErrorToConsole(String tag, Object... objects) { + if (sPlugin != null) { + printToStream(sPlugin.mAndroidConsoleErrorStream, tag, objects); + + showAndroidConsole(); + } + } + + /** + * Prints one or more error message to the android console. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static void printErrorToConsole(Object... objects) { + printErrorToConsole((String)null, objects); + } + + /** + * Prints one or more error message to the android console. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static void printErrorToConsole(IProject project, Object... objects) { + String tag = project != null ? project.getName() : null; + printErrorToConsole(tag, objects); + } + + /** + * Prints one or more build messages to the android console, filtered by Build output verbosity. + * @param level {@link BuildVerbosity} level of the message. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + * @see BuildVerbosity#ALWAYS + * @see BuildVerbosity#NORMAL + * @see BuildVerbosity#VERBOSE + */ + public static synchronized void printBuildToConsole(BuildVerbosity level, IProject project, + Object... objects) { + if (sPlugin != null) { + if (level.getLevel() <= AdtPrefs.getPrefs().getBuildVerbosity().getLevel()) { + String tag = project != null ? project.getName() : null; + printToStream(sPlugin.mAndroidConsoleStream, tag, objects); + } + } + } + + /** + * Prints one or more message to the android console. + * @param tag The tag to be associated with the message. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static synchronized void printToConsole(String tag, Object... objects) { + if (sPlugin != null) { + printToStream(sPlugin.mAndroidConsoleStream, tag, objects); + } + } + + /** + * Prints one or more message to the android console. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static void printToConsole(IProject project, Object... objects) { + String tag = project != null ? project.getName() : null; + printToConsole(tag, objects); + } + + /** Force the display of the android console */ + public static void showAndroidConsole() { + // first make sure the console is in the workbench + EclipseUiHelper.showView(IConsoleConstants.ID_CONSOLE_VIEW, true); + + // now make sure it's not docked. + ConsolePlugin.getDefault().getConsoleManager().showConsoleView( + AdtPlugin.getDefault().getAndroidConsole()); + } + + /** + * Returns whether the {@link IAndroidTarget}s have been loaded from the SDK. + */ + public final LoadStatus getSdkLoadStatus() { + synchronized (Sdk.getLock()) { + return mSdkLoadedStatus; + } + } + + /** + * Sets the given {@link IJavaProject} to have its target resolved again once the SDK finishes + * to load. + */ + public final void setProjectToResolve(IJavaProject javaProject) { + synchronized (Sdk.getLock()) { + mPostLoadProjectsToResolve.add(javaProject); + } + } + + /** + * Sets the given {@link IJavaProject} to have its target checked for consistency + * once the SDK finishes to load. This is used if the target is resolved using cached + * information while the SDK is loading. + */ + public final void setProjectToCheck(IJavaProject javaProject) { + // only lock on + synchronized (Sdk.getLock()) { + mPostLoadProjectsToCheck.add(javaProject); + } + } + + /** + * Checks the location of the SDK in the prefs is valid. + * If it is not, display a warning dialog to the user and try to display + * some useful link to fix the situation (setup the preferences, perform an + * update, etc.) + * + * @return True if the SDK location points to an SDK. + * If false, the user has already been presented with a modal dialog explaining that. + */ + public boolean checkSdkLocationAndId() { + String sdkLocation = AdtPrefs.getPrefs().getOsSdkFolder(); + + return checkSdkLocationAndId(sdkLocation, new CheckSdkErrorHandler() { + private String mTitle = "Android SDK"; + + /** + * Handle an error, which is the case where the check did not find any SDK. + * This returns false to {@link AdtPlugin#checkSdkLocationAndId()}. + */ + @Override + public boolean handleError(Solution solution, String message) { + displayMessage(solution, message, MessageDialog.ERROR); + return false; + } + + /** + * Handle an warning, which is the case where the check found an SDK + * but it might need to be repaired or is missing an expected component. + * + * This returns true to {@link AdtPlugin#checkSdkLocationAndId()}. + */ + @Override + public boolean handleWarning(Solution solution, String message) { + displayMessage(solution, message, MessageDialog.WARNING); + return true; + } + + private void displayMessage( + final Solution solution, + final String message, + final int dialogImageType) { + final Display disp = getDisplay(); + disp.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = disp.getActiveShell(); + if (shell == null) { + shell = AdtPlugin.getShell(); + } + if (shell == null) { + return; + } + + String customLabel = null; + switch(solution) { + case OPEN_ANDROID_PREFS: + customLabel = "Open Preferences"; + break; + case OPEN_P2_UPDATE: + customLabel = "Check for Updates"; + break; + case OPEN_SDK_MANAGER: + customLabel = "Open SDK Manager"; + break; + } + + String btnLabels[] = new String[customLabel == null ? 1 : 2]; + btnLabels[0] = customLabel; + btnLabels[btnLabels.length - 1] = IDialogConstants.CLOSE_LABEL; + + MessageDialog dialog = new MessageDialog( + shell, // parent + mTitle, + null, // dialogTitleImage + message, + dialogImageType, + btnLabels, + btnLabels.length - 1); + int index = dialog.open(); + + if (customLabel != null && index == 0) { + switch(solution) { + case OPEN_ANDROID_PREFS: + openAndroidPrefs(); + break; + case OPEN_P2_UPDATE: + openP2Update(); + break; + case OPEN_SDK_MANAGER: + openSdkManager(); + break; + } + } + } + }); + } + + private void openSdkManager() { + // Open the standalone external SDK Manager since we know + // that ADT on Windows is bound to be locking some SDK folders. + // + // Also when this is invoked because SdkManagerAction.run() fails, this + // test will fail and we'll fallback on using the internal one. + if (SdkManagerAction.openExternalSdkManager()) { + return; + } + + // Otherwise open the regular SDK Manager bundled within ADT + if (!SdkManagerAction.openAdtSdkManager()) { + // We failed because the SDK location is undefined. In this case + // let's open the preferences instead. + openAndroidPrefs(); + } + } + + private void openP2Update() { + Display disp = getDisplay(); + if (disp == null) { + return; + } + disp.asyncExec(new Runnable() { + @Override + public void run() { + String cmdId = "org.eclipse.equinox.p2.ui.sdk.update"; //$NON-NLS-1$ + IWorkbench wb = PlatformUI.getWorkbench(); + if (wb == null) { + return; + } + + ICommandService cs = (ICommandService) wb.getService(ICommandService.class); + IHandlerService is = (IHandlerService) wb.getService(IHandlerService.class); + if (cs == null || is == null) { + return; + } + + Command cmd = cs.getCommand(cmdId); + if (cmd != null && cmd.isDefined()) { + try { + is.executeCommand(cmdId, null/*event*/); + } catch (Exception ignore) { + AdtPlugin.log(ignore, "Failed to execute command %s", cmdId); + } + } + } + }); + } + + private void openAndroidPrefs() { + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn( + getDisplay().getActiveShell(), + "com.android.ide.eclipse.preferences.main", //$NON-NLS-1$ preferencePageId + null, // displayedIds + null); // data + dialog.open(); + } + }); + } + + /** + * Internal helper to perform the actual sdk location and id check. + * <p/> + * This is useful for callers who want to override what happens when the check + * fails. Otherwise consider calling {@link #checkSdkLocationAndId()} that will + * present a modal dialog to the user in case of failure. + * + * @param osSdkLocation The sdk directory, an OS path. Can be null. + * @param errorHandler An checkSdkErrorHandler that can display a warning or an error. + * @return False if there was an error or the result from the errorHandler invocation. + */ + public boolean checkSdkLocationAndId(@Nullable String osSdkLocation, + @NonNull CheckSdkErrorHandler errorHandler) { + if (osSdkLocation == null || osSdkLocation.trim().length() == 0) { + return errorHandler.handleError( + Solution.OPEN_ANDROID_PREFS, + "Location of the Android SDK has not been setup in the preferences."); + } + + if (!osSdkLocation.endsWith(File.separator)) { + osSdkLocation = osSdkLocation + File.separator; + } + + File osSdkFolder = new File(osSdkLocation); + if (osSdkFolder.isDirectory() == false) { + return errorHandler.handleError( + Solution.OPEN_ANDROID_PREFS, + String.format(Messages.Could_Not_Find_Folder, osSdkLocation)); + } + + String osTools = osSdkLocation + SdkConstants.OS_SDK_TOOLS_FOLDER; + File toolsFolder = new File(osTools); + if (toolsFolder.isDirectory() == false) { + return errorHandler.handleError( + Solution.OPEN_ANDROID_PREFS, + String.format(Messages.Could_Not_Find_Folder_In_SDK, + SdkConstants.FD_TOOLS, osSdkLocation)); + } + + // first check the min plug-in requirement as its error message is easier to figure + // out for the user + if (VersionCheck.checkVersion(osSdkLocation, errorHandler) == false) { + return false; + } + + // check that we have both the tools component and the platform-tools component. + String platformTools = osSdkLocation + SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER; + if (checkFolder(platformTools) == false) { + return errorHandler.handleWarning( + Solution.OPEN_SDK_MANAGER, + "SDK Platform Tools component is missing!\n" + + "Please use the SDK Manager to install it."); + } + + String tools = osSdkLocation + SdkConstants.OS_SDK_TOOLS_FOLDER; + if (checkFolder(tools) == false) { + return errorHandler.handleError( + Solution.OPEN_SDK_MANAGER, + "SDK Tools component is missing!\n" + + "Please use the SDK Manager to install it."); + } + + // check the path to various tools we use to make sure nothing is missing. This is + // not meant to be exhaustive. + String[] filesToCheck = new String[] { + osSdkLocation + getOsRelativeAdb(), + osSdkLocation + getOsRelativeEmulator() + }; + for (String file : filesToCheck) { + if (checkFile(file) == false) { + return errorHandler.handleError( + Solution.OPEN_ANDROID_PREFS, + String.format(Messages.Could_Not_Find, file)); + } + } + + return true; + } + + /** + * Checks if a path reference a valid existing file. + * @param osPath the os path to check. + * @return true if the file exists and is, in fact, a file. + */ + private boolean checkFile(String osPath) { + File file = new File(osPath); + if (file.isFile() == false) { + return false; + } + + return true; + } + + /** + * Checks if a path reference a valid existing folder. + * @param osPath the os path to check. + * @return true if the folder exists and is, in fact, a folder. + */ + private boolean checkFolder(String osPath) { + File file = new File(osPath); + if (file.isDirectory() == false) { + return false; + } + + return true; + } + + /** + * Parses the SDK resources. + */ + 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) { + @SuppressWarnings("unchecked") + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + + if (mParseSdkContentIsRunning) { + return new Status(IStatus.WARNING, PLUGIN_ID, + "An Android SDK is already being loaded. Please try again later."); + } + + mParseSdkContentIsRunning = true; + + SubMonitor progress = SubMonitor.convert(monitor, + "Initialize SDK Manager", 100); + + Sdk sdk = Sdk.loadSdk(AdtPrefs.getPrefs().getOsSdkFolder()); + + if (sdk != null) { + ArrayList<IJavaProject> list = new ArrayList<IJavaProject>(); + synchronized (Sdk.getLock()) { + mSdkLoadedStatus = LoadStatus.LOADED; + + progress.setTaskName("Check Projects"); + + for (IJavaProject javaProject : mPostLoadProjectsToResolve) { + IProject iProject = javaProject.getProject(); + if (iProject.isOpen()) { + // project that have been resolved before the sdk was loaded + // will have a ProjectState where the IAndroidTarget is null + // so we load the target now that the SDK is loaded. + sdk.loadTargetAndBuildTools(Sdk.getProjectState(iProject)); + list.add(javaProject); + } + } + + // done with this list. + mPostLoadProjectsToResolve.clear(); + } + + // check the projects that need checking. + // The method modifies the list (it removes the project that + // do not need to be resolved again). + AndroidClasspathContainerInitializer.checkProjectsCache( + mPostLoadProjectsToCheck); + + list.addAll(mPostLoadProjectsToCheck); + + // update the project that needs recompiling. + if (list.size() > 0) { + IJavaProject[] array = list.toArray( + new IJavaProject[list.size()]); + ProjectHelper.updateProjects(array); + } + + progress.worked(10); + } else { + // SDK failed to Load! + // Sdk#loadSdk() has already displayed an error. + synchronized (Sdk.getLock()) { + mSdkLoadedStatus = LoadStatus.FAILED; + } + } + + // Notify resource changed listeners + progress.setTaskName("Refresh UI"); + progress.setWorkRemaining(mTargetChangeListeners.size()); + + // Clone the list before iterating, to avoid ConcurrentModification + // exceptions + final List<ITargetChangeListener> listeners = + (List<ITargetChangeListener>)mTargetChangeListeners.clone(); + final SubMonitor progress2 = progress; + AdtPlugin.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onSdkLoaded(); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } finally { + progress2.worked(1); + } + } + } + }); + } catch (Throwable t) { + log(t, "Unknown exception in parseSdkContent."); //$NON-NLS-1$ + return new Status(IStatus.ERROR, PLUGIN_ID, + "parseSdkContent failed", t); //$NON-NLS-1$ + + } finally { + mParseSdkContentIsRunning = false; + if (monitor != null) { + monitor.done(); + } + } + + return Status.OK_STATUS; + } + }; + job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs + job.setRule(ResourcesPlugin.getWorkspace().getRoot()); + if (delay > 0) { + job.schedule(delay); + } else { + job.schedule(); + } + } + + /** Returns the global android console */ + public MessageConsole getAndroidConsole() { + return mAndroidConsole; + } + + // ----- Methods for Editors ------- + + public void startEditors() { + sAndroidLogoDesc = imageDescriptorFromPlugin(AdtPlugin.PLUGIN_ID, + "/icons/android.png"); //$NON-NLS-1$ + sAndroidLogo = sAndroidLogoDesc.createImage(); + + // Add a resource listener to handle compiled resources. + IWorkspace ws = ResourcesPlugin.getWorkspace(); + mResourceMonitor = GlobalProjectMonitor.startMonitoring(ws); + + if (mResourceMonitor != null) { + try { + setupEditors(mResourceMonitor); + ResourceManager.setup(mResourceMonitor); + LintDeltaProcessor.startListening(mResourceMonitor); + } catch (Throwable t) { + log(t, "ResourceManager.setup failed"); //$NON-NLS-1$ + } + } + } + + /** + * The <code>AbstractUIPlugin</code> implementation of this <code>Plugin</code> + * method saves this plug-in's preference and dialog stores and shuts down + * its image registry (if they are in use). Subclasses may extend this + * method, but must send super <b>last</b>. A try-finally statement should + * be used where necessary to ensure that <code>super.shutdown()</code> is + * always done. + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + public void stopEditors() { + sAndroidLogo.dispose(); + + IconFactory.getInstance().dispose(); + + LintDeltaProcessor.stopListening(mResourceMonitor); + + // Remove the resource listener that handles compiled resources. + IWorkspace ws = ResourcesPlugin.getWorkspace(); + GlobalProjectMonitor.stopMonitoring(ws); + + if (mRed != null) { + mRed.dispose(); + mRed = null; + } + } + + /** + * Returns an Image for the small Android logo. + * + * Callers should not dispose it. + */ + public static Image getAndroidLogo() { + return sAndroidLogo; + } + + /** + * Returns an {@link ImageDescriptor} for the small Android logo. + * + * Callers should not dispose it. + */ + public static ImageDescriptor getAndroidLogoDesc() { + return sAndroidLogoDesc; + } + + /** + * Returns the ResourceMonitor object. + */ + public GlobalProjectMonitor getResourceMonitor() { + return mResourceMonitor; + } + + /** + * Sets up the editor resource listener. + * <p> + * The listener handles: + * <ul> + * <li> Discovering newly created files, and ensuring that if they are in an Android + * project, they default to the right XML editor. + * <li> Discovering deleted files, and closing the corresponding editors if necessary. + * This is only done for XML files, since other editors such as Java editors handles + * it on their own. + * <ul> + * + * This is called by the {@link AdtPlugin} during initialization. + * + * @param monitor The main Resource Monitor object. + */ + public void setupEditors(GlobalProjectMonitor monitor) { + monitor.addFileListener(new IFileListener() { + @Override + public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (!isAndroidProject) { + return; + } + if (flags == IResourceDelta.MARKERS || !SdkConstants.EXT_XML.equals(extension)) { + // ONLY the markers changed, or not XML file: not relevant to this listener + return; + } + + if (kind == IResourceDelta.REMOVED) { + AdtUtils.closeEditors(file, false /*save*/); + return; + } + + // The resources files must have a file path similar to + // project/res/.../*.xml + // There is no support for sub folders, so the segment count must be 4 + if (file.getFullPath().segmentCount() == 4) { + // check if we are inside the res folder. + String segment = file.getFullPath().segment(1); + if (segment.equalsIgnoreCase(SdkConstants.FD_RESOURCES)) { + // we are inside a res/ folder, get the ResourceFolderType of the + // parent folder. + String[] folderSegments = file.getParent().getName().split( + SdkConstants.RES_QUALIFIER_SEP); + + // get the enum for the resource type. + ResourceFolderType type = ResourceFolderType.getTypeByName( + folderSegments[0]); + + if (type != null) { + if (kind == IResourceDelta.ADDED) { + // A new file {@code /res/type-config/some.xml} was added. + // All the /res XML files are handled by the same common editor now. + IDE.setDefaultEditor(file, CommonXmlEditor.ID); + } + } else { + // if the res folder is null, this means the name is invalid, + // in this case we remove whatever android editors that was set + // as the default editor. + IEditorDescriptor desc = IDE.getDefaultEditor(file); + String editorId = desc.getId(); + if (editorId.startsWith(AdtConstants.EDITORS_NAMESPACE)) { + // reset the default editor. + IDE.setDefaultEditor(file, null); + } + } + } + } + } + }, IResourceDelta.ADDED | IResourceDelta.REMOVED); + + monitor.addProjectListener(new IProjectListener() { + @Override + public void projectClosed(IProject project) { + // Close any editors referencing this project + AdtUtils.closeEditors(project, true /*save*/); + } + + @Override + public void projectDeleted(IProject project) { + // Close any editors referencing this project + AdtUtils.closeEditors(project, false /*save*/); + } + + @Override + public void projectOpenedWithWorkspace(IProject project) { + } + + @Override + public void allProjectsOpenedWithWorkspace() { + } + + @Override + public void projectOpened(IProject project) { + } + + @Override + public void projectRenamed(IProject project, IPath from) { + } + }); + } + + /** + * Adds a new {@link ITargetChangeListener} to be notified when a new SDK is loaded, or when + * a project has its target changed. + */ + public void addTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.add(listener); + } + + /** + * Removes an existing {@link ITargetChangeListener}. + * @see #addTargetListener(ITargetChangeListener) + */ + public void removeTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.remove(listener); + } + + /** + * Updates all the {@link ITargetChangeListener}s that a target has changed for a given project. + * <p/>Only editors related to that project should reload. + */ + @SuppressWarnings("unchecked") + public void updateTargetListeners(final IProject project) { + final List<ITargetChangeListener> listeners = + (List<ITargetChangeListener>)mTargetChangeListeners.clone(); + + AdtPlugin.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onProjectTargetChange(project); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } + } + } + }); + } + + /** + * Updates all the {@link ITargetChangeListener}s that a target data was loaded. + * <p/>Only editors related to a project using this target should reload. + */ + @SuppressWarnings("unchecked") + public void updateTargetListeners(final IAndroidTarget target) { + final List<ITargetChangeListener> listeners = + (List<ITargetChangeListener>)mTargetChangeListeners.clone(); + + Display display = AdtPlugin.getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + display.asyncExec(new Runnable() { + @Override + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onTargetLoaded(target); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } + } + } + }); + } + + public static synchronized OutputStream getOutStream() { + return sPlugin.mAndroidConsoleStream; + } + + public static synchronized OutputStream getErrorStream() { + return sPlugin.mAndroidConsoleErrorStream; + } + + /** + * Sets the named persistent property for the given file to the given value + * + * @param file the file to associate the property with + * @param qname the name of the property + * @param value the new value, or null to clear the property + */ + public static void setFileProperty(IFile file, QualifiedName qname, String value) { + try { + file.setPersistentProperty(qname, value); + } catch (CoreException e) { + log(e, "Cannot set property %1$s to %2$s", qname, value); + } + } + + /** + * Gets the named persistent file property from the given file + * + * @param file the file to look up properties for + * @param qname the name of the property to look up + * @return the property value, or null + */ + public static String getFileProperty(IFile file, QualifiedName qname) { + try { + return file.getPersistentProperty(qname); + } catch (CoreException e) { + log(e, "Cannot get property %1$s", qname); + } + + return null; + } + + /** + * Conditionally reparses the content of the SDK if it has changed on-disk + * and updates opened projects. + * <p/> + * The operation is asynchronous and happens in a background eclipse job. + * <p/> + * This operation is called in multiple places and should be reasonably + * cheap and conservative. The goal is to automatically refresh the SDK + * when it is obvious it has changed so when not sure the code should + * tend to not reload and avoid reloading too often (which is an expensive + * operation that has a lot of user impact.) + */ + public void refreshSdk() { + // SDK can't have changed if we haven't loaded it yet. + final Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + return; + } + + Job job = new Job("Check Android SDK") { + @Override + protected IStatus run(IProgressMonitor monitor) { + // SDK has changed if its location path is different. + File location = sdk.getSdkFileLocation(); + boolean changed = location == null || !location.isDirectory(); + + if (!changed) { + assert location != null; + File prefLocation = new File(AdtPrefs.getPrefs().getOsSdkFolder()); + changed = !location.equals(prefLocation); + + if (changed) { + // Basic file path comparison indicates they are not the same. + // Let's dig a bit deeper. + try { + location = location.getCanonicalFile(); + prefLocation = prefLocation.getCanonicalFile(); + changed = !location.equals(prefLocation); + } catch (IOException ignore) { + // There's no real reason for the canonicalization to fail + // if the paths map to actual directories. And if they don't + // this should have been caught above. + } + } + } + + if (!changed) { + // Check whether the target directories has potentially changed. + changed = sdk.haveTargetsChanged(); + } + + if (changed) { + monitor.setTaskName("Reload Android SDK"); + reparseSdk(); + } + + monitor.done(); + return Status.OK_STATUS; + } + }; + job.setRule(ResourcesPlugin.getWorkspace().getRoot()); + job.setPriority(Job.SHORT); // a short background job, not interactive. + job.schedule(); + } + + /** + * Reparses the content of the SDK and updates opened projects. + * The operation is asynchronous and happens in a background eclipse job. + * <p/> + * This reloads the SDK all the time. To only perform this when it has potentially + * changed, call {@link #refreshSdk()} instead. + */ + public void reparseSdk() { + // add all the opened Android projects to the list of projects to be updated + // after the SDK is reloaded + synchronized (Sdk.getLock()) { + // get the project to refresh. + IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(null /*filter*/); + mPostLoadProjectsToResolve.addAll(Arrays.asList(androidProjects)); + } + + // parse the SDK resources at the new location + parseSdkContent(0 /*immediately*/); + } + + /** + * Prints messages, associated with a project to the specified stream + * @param stream The stream to write to + * @param tag The tag associated to the message. Can be null + * @param objects The objects to print through their toString() method (or directly for + * {@link String} objects. + */ + public static synchronized void printToStream(MessageConsoleStream stream, String tag, + Object... objects) { + String dateTag = AndroidPrintStream.getMessageTag(tag); + + for (Object obj : objects) { + stream.print(dateTag); + stream.print(" "); //$NON-NLS-1$ + if (obj instanceof String) { + stream.println((String)obj); + } else if (obj == null) { + stream.println("(null)"); //$NON-NLS-1$ + } else { + stream.println(obj.toString()); + } + } + } + + // --------- ILogger methods ----------- + + @Override + public void error(@Nullable Throwable t, @Nullable String format, Object... args) { + if (t != null) { + log(t, format, args); + } else { + log(IStatus.ERROR, format, args); + } + } + + @Override + public void info(@NonNull String format, Object... args) { + log(IStatus.INFO, format, args); + } + + @Override + public void verbose(@NonNull String format, Object... args) { + log(IStatus.INFO, format, args); + } + + @Override + public void warning(@NonNull String format, Object... args) { + log(IStatus.WARNING, format, args); + } + + /** + * Opens the given URL in a browser tab + * + * @param url the URL to open in a browser + */ + public static void openUrl(URL url) { + IWorkbenchBrowserSupport support = PlatformUI.getWorkbench().getBrowserSupport(); + IWebBrowser browser; + try { + browser = support.createBrowser(PLUGIN_ID); + browser.openURL(url); + } catch (PartInitException e) { + log(e, null); + } + } + + /** + * Opens a Java class for the given fully qualified class name + * + * @param project the project containing the class + * @param fqcn the fully qualified class name of the class to be opened + * @return true if the class was opened, false otherwise + */ + public static boolean openJavaClass(IProject project, String fqcn) { + if (fqcn == null) { + return false; + } + + // Handle inner classes + if (fqcn.indexOf('$') != -1) { + fqcn = fqcn.replaceAll("\\$", "."); //$NON-NLS-1$ //$NON-NLS-2$ + } + + try { + if (project.hasNature(JavaCore.NATURE_ID)) { + IJavaProject javaProject = JavaCore.create(project); + IJavaElement result = javaProject.findType(fqcn); + if (result != null) { + return JavaUI.openInEditor(result) != null; + } + } + } catch (Throwable e) { + log(e, "Can't open class %1$s", fqcn); //$NON-NLS-1$ + } + + return false; + } + + /** + * For a stack trace entry, specifying a class, method, and optionally + * fileName and line number, open the corresponding line in the editor. + * + * @param fqcn the fully qualified name of the class + * @param method the method name + * @param fileName the file name, or null + * @param lineNumber the line number or -1 + * @return true if the target location could be opened, false otherwise + */ + public static boolean openStackTraceLine(@Nullable String fqcn, + @Nullable String method, @Nullable String fileName, int lineNumber) { + return new SourceRevealer().revealMethod(fqcn + '.' + method, fileName, lineNumber, null); + } + + /** + * Opens the given file and shows the given (optional) region in the editor (or + * if no region is specified, opens the editor tab.) + * + * @param file the file to be opened + * @param region an optional region which if set will be selected and shown to the + * user + * @throws PartInitException if something goes wrong + */ + public static void openFile(IFile file, IRegion region) throws PartInitException { + openFile(file, region, true); + } + + // TODO: Make an openEditor which does the above, and make the above pass false for showEditor + + /** + * Opens the given file and shows the given (optional) region + * + * @param file the file to be opened + * @param region an optional region which if set will be selected and shown to the + * user + * @param showEditorTab if true, front the editor tab after opening the file + * @return the editor that was opened, or null if no editor was opened + * @throws PartInitException if something goes wrong + */ + public static IEditorPart openFile(IFile file, IRegion region, boolean showEditorTab) + throws PartInitException { + IWorkbenchPage page = AdtUtils.getActiveWorkbenchPage(); + if (page == null) { + return null; + } + IEditorPart targetEditor = IDE.openEditor(page, file, true); + if (targetEditor instanceof AndroidXmlEditor) { + AndroidXmlEditor editor = (AndroidXmlEditor) targetEditor; + if (region != null) { + editor.show(region.getOffset(), region.getLength(), showEditorTab); + } else if (showEditorTab) { + editor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID); + } + } else if (targetEditor instanceof AbstractTextEditor) { + AbstractTextEditor editor = (AbstractTextEditor) targetEditor; + if (region != null) { + editor.setHighlightRange(region.getOffset(), region.getLength(), + true /* moveCursor*/); + } + } + + return targetEditor; + } +} |