/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.sdklib.internal.repository.updater; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; import com.android.annotations.VisibleForTesting.Visibility; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.SdkManager; import com.android.sdklib.internal.avd.AvdManager; import com.android.sdklib.internal.repository.AdbWrapper; import com.android.sdklib.internal.repository.DownloadCache; import com.android.sdklib.internal.repository.ITask; import com.android.sdklib.internal.repository.ITaskFactory; import com.android.sdklib.internal.repository.ITaskMonitor; import com.android.sdklib.internal.repository.LocalSdkParser; import com.android.sdklib.internal.repository.NullTaskMonitor; import com.android.sdklib.internal.repository.archives.Archive; import com.android.sdklib.internal.repository.archives.ArchiveInstaller; import com.android.sdklib.internal.repository.packages.AddonPackage; import com.android.sdklib.repository.License; import com.android.sdklib.internal.repository.packages.Package; import com.android.sdklib.internal.repository.packages.PlatformToolPackage; import com.android.sdklib.internal.repository.packages.ToolPackage; import com.android.sdklib.internal.repository.sources.SdkRepoSource; import com.android.sdklib.internal.repository.sources.SdkSource; import com.android.sdklib.internal.repository.sources.SdkSourceCategory; import com.android.sdklib.internal.repository.sources.SdkSources; import com.android.sdklib.internal.repository.updater.SettingsController.OnChangedListener; import com.android.sdklib.repository.ISdkChangeListener; import com.android.sdklib.repository.SdkAddonConstants; import com.android.sdklib.repository.SdkRepoConstants; import com.android.sdklib.util.LineUtil; import com.android.utils.ILogger; import com.android.utils.IReaderLogger; import com.android.utils.SparseIntArray; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; /** * Data shared by the SDK Manager updaters. * * @deprecated * com.android.sdklib.internal.repository has moved into Studio as * com.android.tools.idea.sdk.remote.internal. */ @Deprecated public class UpdaterData implements IUpdaterData { public static final int NO_TOOLS_MSG = 0; public static final int TOOLS_MSG_UPDATED_FROM_ADT = 1; public static final int TOOLS_MSG_UPDATED_FROM_SDKMAN = 2; private String mOsSdkRoot; private final LocalSdkParser mLocalSdkParser = new LocalSdkParser(); /** Holds all sources. Do not use this directly. * Instead use {@link #getSources()} so that unit tests can override this as needed. */ private final SdkSources mSources = new SdkSources(); /** Holds settings. Do not use this directly. * Instead use {@link #getSettingsController()} so that unit tests can override this. */ private final SettingsController mSettingsController; private final ArrayList<ISdkChangeListener> mListeners = new ArrayList<ISdkChangeListener>(); private final ILogger mSdkLog; private ITaskFactory mTaskFactory; private SdkManager mSdkManager; private AvdManager mAvdManager; /** * The current {@link PackageLoader} to use. * Lazily created in {@link #getPackageLoader()}. */ private PackageLoader mPackageLoader; /** * The current {@link DownloadCache} to use. * Lazily created in {@link #getDownloadCache()}. */ private DownloadCache mDownloadCache; private AndroidLocationException mAvdManagerInitError; /** * Creates a new updater data. * * @param sdkLog Logger. Cannot be null. * @param osSdkRoot The OS path to the SDK root. */ public UpdaterData(String osSdkRoot, ILogger sdkLog) { mOsSdkRoot = osSdkRoot; mSdkLog = sdkLog; mSettingsController = initSettingsController(); initSdk(); } // ----- getters, setters ---- public String getOsSdkRoot() { return mOsSdkRoot; } @Override public DownloadCache getDownloadCache() { if (mDownloadCache == null) { mDownloadCache = new DownloadCache( getSettingsController().getSettings().getUseDownloadCache() ? DownloadCache.Strategy.FRESH_CACHE : DownloadCache.Strategy.DIRECT); } return mDownloadCache; } public void setTaskFactory(ITaskFactory taskFactory) { mTaskFactory = taskFactory; } @Override public ITaskFactory getTaskFactory() { return mTaskFactory; } public SdkSources getSources() { return mSources; } public LocalSdkParser getLocalSdkParser() { return mLocalSdkParser; } @Override public ILogger getSdkLog() { return mSdkLog; } @Override public SdkManager getSdkManager() { return mSdkManager; } @Override public AvdManager getAvdManager() { return mAvdManager; } @Override public SettingsController getSettingsController() { return mSettingsController; } /** Adds a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ public void addListeners(ISdkChangeListener listener) { if (mListeners.contains(listener) == false) { mListeners.add(listener); } } /** Removes a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ public void removeListener(ISdkChangeListener listener) { mListeners.remove(listener); } public PackageLoader getPackageLoader() { // The package loader is lazily initialized here. if (mPackageLoader == null) { mPackageLoader = new PackageLoader(this); } return mPackageLoader; } /** * Check if any error occurred during initialization. * If it did, display an error message. * * @return True if an error occurred, false if we should continue. */ public boolean checkIfInitFailed() { if (mAvdManagerInitError != null) { String example; if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { example = "%USERPROFILE%"; //$NON-NLS-1$ } else { example = "~"; //$NON-NLS-1$ } String error = String.format( "The AVD manager normally uses the user's profile directory to store " + "AVD files. However it failed to find the default profile directory. " + "\n" + "To fix this, please set the environment variable ANDROID_SDK_HOME to " + "a valid path such as \"%s\".", example); displayInitError(error); return true; } return false; } protected void displayInitError(String error) { mSdkLog.error(null /* Throwable */, "%s", error); //$NON-NLS-1$ } // ----- /** * Runs a runnable on the UI thread. * The base implementation just runs the runnable right away. * * @param r Non-null runnable. */ protected void runOnUiThread(@NonNull Runnable r) { r.run(); } /** * Initializes the {@link SdkManager} and the {@link AvdManager}. * Extracted so that we can override this in unit tests. */ @VisibleForTesting(visibility=Visibility.PRIVATE) protected void initSdk() { setSdkManager(SdkManager.createManager(mOsSdkRoot, mSdkLog)); try { mAvdManager = null; mAvdManager = AvdManager.getInstance(mSdkManager.getLocalSdk(), mSdkLog); } catch (AndroidLocationException e) { mSdkLog.error(e, "Unable to read AVDs: " + e.getMessage()); //$NON-NLS-1$ // Note: we used to continue here, but the thing is that // mAvdManager==null so nothing is really going to work as // expected. Let's just display an error later in checkIfInitFailed() // and abort right there. This step is just too early in the SWT // setup process to display a message box yet. mAvdManagerInitError = e; } // notify listeners. broadcastOnSdkReload(); } /** * Initializes the {@link SettingsController} * Extracted so that we can override this in unit tests. */ @VisibleForTesting(visibility=Visibility.PRIVATE) protected SettingsController initSettingsController() { SettingsController settingsController = new SettingsController(mSdkLog); settingsController.registerOnChangedListener(new OnChangedListener() { @Override public void onSettingsChanged( SettingsController controller, SettingsController.Settings oldSettings) { // Reset the download cache if it doesn't match the right strategy. // The cache instance gets lazily recreated later in getDownloadCache(). if (mDownloadCache != null) { if (controller.getSettings().getUseDownloadCache() && mDownloadCache.getStrategy() != DownloadCache.Strategy.FRESH_CACHE) { mDownloadCache = null; } else if (!controller.getSettings().getUseDownloadCache() && mDownloadCache.getStrategy() != DownloadCache.Strategy.DIRECT) { mDownloadCache = null; } } } }); return settingsController; } @VisibleForTesting(visibility=Visibility.PRIVATE) protected void setSdkManager(SdkManager sdkManager) { mSdkManager = sdkManager; } /** * Reloads the SDK content (targets). * <p/> * This also reloads the AVDs in case their status changed. * <p/> * This does not notify the listeners ({@link ISdkChangeListener}). */ public void reloadSdk() { // reload SDK mSdkManager.reloadSdk(mSdkLog); // reload AVDs if (mAvdManager != null) { try { mAvdManager.reloadAvds(mSdkLog); } catch (AndroidLocationException e) { // FIXME } } mLocalSdkParser.clearPackages(); // notify listeners broadcastOnSdkReload(); } /** * Reloads the AVDs. * <p/> * This does not notify the listeners. */ public void reloadAvds() { // reload AVDs if (mAvdManager != null) { try { mAvdManager.reloadAvds(mSdkLog); } catch (AndroidLocationException e) { mSdkLog.error(e, null); } } } /** * Sets up the default sources: <br/> * - the default google SDK repository, <br/> * - the user sources from prefs <br/> * - the extra repo URLs from the environment, <br/> * - and finally the extra user repo URLs from the environment. */ public void setupDefaultSources() { SdkSources sources = getSources(); // Load the conventional sources. // For testing, the env var can be set to replace the default root download URL. // It must end with a / and its the location where the updater will look for // the repository.xml, addons_list.xml and such files. String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ if (baseUrl == null || baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$ baseUrl = SdkRepoConstants.URL_GOOGLE_SDK_SITE; } sources.add(SdkSourceCategory.ANDROID_REPO, new SdkRepoSource(baseUrl, SdkSourceCategory.ANDROID_REPO.getUiName())); // Load user sources (this will also notify change listeners but this operation is // done early enough that there shouldn't be any anyway.) sources.loadUserAddons(getSdkLog()); } /** * Returns the list of installed packages, parsing them if this has not yet been done. * <p/> * The package list is cached in the {@link LocalSdkParser} and will be reset when * {@link #reloadSdk()} is invoked. */ public Package[] getInstalledPackages(ITaskMonitor monitor) { LocalSdkParser parser = getLocalSdkParser(); Package[] packages = parser.getPackages(); if (packages == null) { // load on demand the first time packages = parser.parseSdk(getOsSdkRoot(), getSdkManager(), monitor); } return packages; } /** * Install the list of given {@link Archive}s. This is invoked by the user selecting some * packages in the remote page and then clicking "install selected". * * @param archives The archives to install. Incompatible ones will be skipped. * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. * @return A list of archives that have been installed. Can be empty but not null. */ @VisibleForTesting(visibility=Visibility.PRIVATE) protected List<Archive> installArchives(final List<ArchiveInfo> archives, final int flags) { if (mTaskFactory == null) { throw new IllegalArgumentException("Task Factory is null"); } // this will accumulate all the packages installed. final List<Archive> newlyInstalledArchives = new ArrayList<Archive>(); final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); // sort all archives based on their dependency level. Collections.sort(archives, new InstallOrderComparator()); mTaskFactory.start("Installing Archives", new ITask() { @Override public void run(ITaskMonitor monitor) { final int progressPerArchive = 2 * ArchiveInstaller.NUM_MONITOR_INC; monitor.setProgressMax(1 + archives.size() * progressPerArchive); monitor.setDescription("Preparing to install archives"); boolean installedAddon = false; boolean installedTools = false; boolean installedPlatformTools = false; boolean preInstallHookInvoked = false; // Mark all current local archives as already installed. HashSet<Archive> installedArchives = new HashSet<Archive>(); for (Package p : getInstalledPackages(monitor.createSubMonitor(1))) { for (Archive a : p.getArchives()) { installedArchives.add(a); } } int numInstalled = 0; nextArchive: for (ArchiveInfo ai : archives) { Archive archive = ai.getNewArchive(); if (archive == null) { // This is not supposed to happen. continue nextArchive; } int nextProgress = monitor.getProgress() + progressPerArchive; try { if (monitor.isCancelRequested()) { break; } ArchiveInfo[] adeps = ai.getDependsOn(); if (adeps != null) { for (ArchiveInfo adep : adeps) { Archive na = adep.getNewArchive(); if (na == null) { // This archive depends on a missing archive. // We shouldn't get here. // Skip it. monitor.log("Skipping '%1$s'; it depends on a missing package.", archive.getParentPackage().getShortDescription()); continue nextArchive; } else if (!installedArchives.contains(na)) { // This archive depends on another one that was not installed. // We shouldn't get here. // Skip it. monitor.logError("Skipping '%1$s'; it depends on '%2$s' which was not installed.", archive.getParentPackage().getShortDescription(), adep.getShortDescription()); continue nextArchive; } } } if (!preInstallHookInvoked) { preInstallHookInvoked = true; broadcastPreInstallHook(); } ArchiveInstaller installer = createArchiveInstaler(); if (installer.install(ai, mOsSdkRoot, forceHttp, mSdkManager, getDownloadCache(), monitor)) { // We installed this archive. newlyInstalledArchives.add(archive); installedArchives.add(archive); numInstalled++; // If this package was replacing an existing one, the old one // is no longer installed. installedArchives.remove(ai.getReplaced()); // Check if we successfully installed a platform-tool or add-on package. if (archive.getParentPackage() instanceof AddonPackage) { installedAddon = true; } else if (archive.getParentPackage() instanceof ToolPackage) { installedTools = true; } else if (archive.getParentPackage() instanceof PlatformToolPackage) { installedPlatformTools = true; } } } catch (Throwable t) { // Display anything unexpected in the monitor. String msg = t.getMessage(); if (msg != null) { msg = String.format("Unexpected Error installing '%1$s': %2$s: %3$s", archive.getParentPackage().getShortDescription(), t.getClass().getCanonicalName(), msg); } else { // no error info? get the stack call to display it // At least that'll give us a better bug report. ByteArrayOutputStream baos = new ByteArrayOutputStream(); t.printStackTrace(new PrintStream(baos)); msg = String.format("Unexpected Error installing '%1$s'\n%2$s", archive.getParentPackage().getShortDescription(), baos.toString()); } monitor.log( "%1$s", msg); //$NON-NLS-1$ mSdkLog.error(t, "%1$s", msg); //$NON-NLS-1$ } finally { // Always move the progress bar to the desired position. // This allows internal methods to not have to care in case // they abort early monitor.incProgress(nextProgress - monitor.getProgress()); } } if (installedAddon) { // Update the USB vendor ids for adb try { mSdkManager.updateAdb(); monitor.log("Updated ADB to support the USB devices declared in the SDK add-ons."); } catch (Exception e) { mSdkLog.error(e, "Update ADB failed"); monitor.logError("failed to update adb to support the USB devices declared in the SDK add-ons."); } } if (preInstallHookInvoked) { broadcastPostInstallHook(); } if (installedAddon || installedPlatformTools) { // We need to restart ADB. Actually since we don't know if it's even // running, maybe we should just kill it and not start it. // Note: it turns out even under Windows we don't need to kill adb // before updating the tools folder, as adb.exe is (surprisingly) not // locked. askForAdbRestart(monitor); } if (installedTools) { notifyToolsNeedsToBeRestarted(flags); } if (numInstalled == 0) { monitor.setDescription("Done. Nothing was installed."); } else { monitor.setDescription("Done. %1$d %2$s installed.", numInstalled, numInstalled == 1 ? "package" : "packages"); //notify listeners something was installed, so that they can refresh reloadSdk(); } } }); return newlyInstalledArchives; } /** * A comparator to sort all the {@link ArchiveInfo} based on their * dependency level. This forces the installer to install first all packages * with no dependency, then those with one level of dependency, etc. */ private static class InstallOrderComparator implements Comparator<ArchiveInfo> { private final Map<ArchiveInfo, Integer> mOrders = new HashMap<ArchiveInfo, Integer>(); @Override public int compare(ArchiveInfo o1, ArchiveInfo o2) { int n1 = getDependencyOrder(o1); int n2 = getDependencyOrder(o2); return n1 - n2; } private int getDependencyOrder(ArchiveInfo ai) { if (ai == null) { return 0; } // reuse cached value, if any Integer cached = mOrders.get(ai); if (cached != null) { return cached.intValue(); } ArchiveInfo[] deps = ai.getDependsOn(); if (deps == null) { return 0; } // compute dependencies, recursively int n = deps.length; for (ArchiveInfo dep : deps) { n += getDependencyOrder(dep); } // cache it mOrders.put(ai, Integer.valueOf(n)); return n; } } /** * Attempts to restart ADB. * <p/> * If the "ask before restart" setting is set (the default), prompt the user whether * now is a good time to restart ADB. */ protected void askForAdbRestart(ITaskMonitor monitor) { // Restart ADB if we don't need to ask. if (!getSettingsController().getSettings().getAskBeforeAdbRestart()) { AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor); adb.stopAdb(); adb.startAdb(); } } protected void notifyToolsNeedsToBeRestarted(int flags) { String msg = null; if ((flags & TOOLS_MSG_UPDATED_FROM_ADT) == TOOLS_MSG_UPDATED_FROM_ADT) { msg = "The Android SDK and AVD Manager that you are currently using has been updated. " + "Please also run Eclipse > Help > Check for Updates to see if the Android " + "plug-in needs to be updated."; } else if ((flags & TOOLS_MSG_UPDATED_FROM_SDKMAN) == TOOLS_MSG_UPDATED_FROM_SDKMAN) { msg = "The Android SDK and AVD Manager that you are currently using has been updated. " + "It is recommended that you now close the manager window and re-open it. " + "If you use Eclipse, please run Help > Check for Updates to see if the Android " + "plug-in needs to be updated."; } else if ((flags & NO_TOOLS_MSG) == NO_TOOLS_MSG) { return; } mSdkLog.info("%s", msg); //$NON-NLS-1$ } /** * Fetches all archives available on the known remote sources. * * Used by {@link UpdaterData#listRemotePackages_NoGUI} and * {@link UpdaterData#updateOrInstallAll_NoGUI}. * * @param includeAll True to list and install all packages, including obsolete ones. * @return A list of potential {@link ArchiveInfo} to install. */ private List<ArchiveInfo> getRemoteArchives_NoGUI(boolean includeAll) { refreshSources(true); getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); List<ArchiveInfo> archives; SdkUpdaterLogic ul = new SdkUpdaterLogic(this); if (includeAll) { archives = ul.getAllRemoteArchives( getSources(), getLocalSdkParser().getPackages(), includeAll); } else { archives = ul.computeUpdates( null /*selectedArchives*/, getSources(), getLocalSdkParser().getPackages(), includeAll); ul.addNewPlatforms( archives, getSources(), getLocalSdkParser().getPackages(), includeAll); } Collections.sort(archives); return archives; } /** * Lists remote packages available for install using * {@link UpdaterData#updateOrInstallAll_NoGUI}. * * @param includeAll True to list and install all packages, including obsolete ones. * @param extendedOutput True to display more details on each package. */ public void listRemotePackages_NoGUI(boolean includeAll, boolean extendedOutput) { List<ArchiveInfo> archives = getRemoteArchives_NoGUI(includeAll); mSdkLog.info("Packages available for installation or update: %1$d\n", archives.size()); int index = 1; for (ArchiveInfo ai : archives) { Archive a = ai.getNewArchive(); if (a != null) { Package p = a.getParentPackage(); if (p != null) { if (extendedOutput) { mSdkLog.info("----------\n"); mSdkLog.info("id: %1$d or \"%2$s\"\n", index, p.installId()); mSdkLog.info(" Type: %1$s\n", p.getClass().getSimpleName().replaceAll("Package", "")); //$NON-NLS-1$ //$NON-NLS-2$ String desc = LineUtil.reformatLine(" Desc: %s\n", p.getLongDescription()); mSdkLog.info("%s", desc); //$NON-NLS-1$ } else { mSdkLog.info("%1$ 4d- %2$s\n", index, p.getShortDescription()); } index++; } } } } /** * Tries to update all the *existing* local packages. * This version *requires* to be run with a GUI. * <p/> * There are two modes of operation: * <ul> * <li>If selectedArchives is null, refreshes all sources, compares the available remote * packages with the current local ones and suggest updates to be done to the user (including * new platforms that the users doesn't have yet). * <li>If selectedArchives is not null, this represents a list of archives/packages that * the user wants to install or update, so just process these. * </ul> * * @param selectedArchives The list of remote archives to consider for the update. * This can be null, in which case a list of remote archive is fetched from all * available sources. * @param includeObsoletes True if obsolete packages should be used when resolving what * to update. * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. * @return A list of archives that have been installed. Can be null if nothing was done. */ public List<Archive> updateOrInstallAll_WithGUI( Collection<Archive> selectedArchives, boolean includeObsoletes, int flags) { // FIXME revisit this logic. This is just an transitional implementation // while I refactor the way the sdk manager works internally. SdkUpdaterLogic ul = new SdkUpdaterLogic(this); List<ArchiveInfo> archives = ul.computeUpdates( selectedArchives, getSources(), getLocalSdkParser().getPackages(), includeObsoletes); if (selectedArchives == null) { getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); ul.addNewPlatforms( archives, getSources(), getLocalSdkParser().getPackages(), includeObsoletes); } Collections.sort(archives); if (!archives.isEmpty()) { return installArchives(archives, flags); } return null; } /** * Tries to update all the *existing* local packages. * This version is intended to run without a GUI and * only outputs to the current {@link ILogger}. * * @param pkgFilter A list of {@link SdkRepoConstants#NODES} or {@link Package#installId()} * or package indexes to limit the packages we can update or install. * A null or empty list means to update everything possible. * @param includeAll True to list and install all packages, including obsolete ones. * @param dryMode True to check what would be updated/installed but do not actually * download or install anything. * @param acceptLicense SDK licenses to automatically accept. * @return A list of archives that have been installed. Can be null if nothing was done. * @deprecated Use {@link #updateOrInstallAll_NoGUI(java.util.Collection, boolean, boolean, String, boolean)} * instead */ @Deprecated public List<Archive> updateOrInstallAll_NoGUI( Collection<String> pkgFilter, boolean includeAll, boolean dryMode, String acceptLicense) { return updateOrInstallAll_NoGUI(pkgFilter, includeAll, dryMode, acceptLicense, false); } /** * Tries to update all the *existing* local packages. * This version is intended to run without a GUI and * only outputs to the current {@link ILogger}. * * @param pkgFilter A list of {@link SdkRepoConstants#NODES} or {@link Package#installId()} * or package indexes to limit the packages we can update or install. * A null or empty list means to update everything possible. * @param includeAll True to list and install all packages, including obsolete ones. * @param dryMode True to check what would be updated/installed but do not actually * download or install anything. * @param acceptLicense SDK licenses to automatically accept. * @param includeDependencies If true, also include any required dependencies * @return A list of archives that have been installed. Can be null if nothing was done. */ public List<Archive> updateOrInstallAll_NoGUI( Collection<String> pkgFilter, boolean includeAll, boolean dryMode, String acceptLicense, boolean includeDependencies) { List<ArchiveInfo> archives = getRemoteArchives_NoGUI(includeAll); // Filter the selected archives to only keep the ones matching the filter if (pkgFilter != null && !pkgFilter.isEmpty() && archives != null && !archives.isEmpty()) { // Map filter types to an SdkRepository Package type, // e.g. create a map "platform" => PlatformPackage.class HashMap<String, Class<? extends Package>> pkgMap = new HashMap<String, Class<? extends Package>>(); mapFilterToPackageClass(pkgMap, SdkRepoConstants.NODES); mapFilterToPackageClass(pkgMap, SdkAddonConstants.NODES); // Prepare a map install-id => package instance HashMap<String, Package> installIdMap = new HashMap<String, Package>(); for (ArchiveInfo ai : archives) { Archive a = ai.getNewArchive(); if (a != null) { Package p = a.getParentPackage(); if (p != null) { String iid = p.installId().toLowerCase(Locale.US); if (iid != null && !iid.isEmpty() && !installIdMap.containsKey(iid)) { installIdMap.put(iid, p); } } } } // Now intersect this with the pkgFilter requested by the user, in order to // only keep the classes that the user wants to install. // We also create a set with the package indices requested by the user // and a set of install-ids requested by the user. HashSet<Class<? extends Package>> userFilteredClasses = new HashSet<Class<? extends Package>>(); SparseIntArray userFilteredIndices = new SparseIntArray(); Set<String> userFilteredInstallIds = new HashSet<String>(); for (String iid : pkgFilter) { // The install-id is not case-sensitive. iid = iid.toLowerCase(Locale.US); if (installIdMap.containsKey(iid)) { userFilteredInstallIds.add(iid); } else if (iid.replaceAll("[0-9]+", "").isEmpty()) {//$NON-NLS-1$ //$NON-NLS-2$ // An all-digit number is a package index requested by the user. int index = Integer.parseInt(iid); userFilteredIndices.put(index, index); } else if (pkgMap.containsKey(iid)) { userFilteredClasses.add(pkgMap.get(iid)); } else { // This should not happen unless there's a mismatch in the package map. mSdkLog.error(null, "Ignoring unknown package filter '%1$s'", iid); } } // we don't need the maps anymore pkgMap = null; installIdMap = null; // Now filter the remote archives list to keep: // - any package which class matches userFilteredClasses // - any package index which matches userFilteredIndices // - any package install id which matches userFilteredInstallIds int index = 1; for (Iterator<ArchiveInfo> it = archives.iterator(); it.hasNext(); ) { boolean keep = false; ArchiveInfo ai = it.next(); Archive a = ai.getNewArchive(); if (a != null) { Package p = a.getParentPackage(); if (p != null) { if (userFilteredInstallIds.contains(p.installId().toLowerCase(Locale.US)) || userFilteredClasses.contains(p.getClass()) || userFilteredIndices.get(index) > 0) { keep = true; } index++; } } if (!keep) { it.remove(); } } if (archives.isEmpty()) { mSdkLog.info(LineUtil.reflowLine( "Warning: The package filter removed all packages. There is nothing to install.\nPlease consider trying to update again without a package filter.\n")); return null; } } if (archives != null && !archives.isEmpty()) { if (includeDependencies) { List<ArchiveInfo> dependencies = getDependencies(archives); if (!dependencies.isEmpty()) { List<ArchiveInfo> combined = Lists.newArrayList(); combined.addAll(dependencies); combined.addAll(archives); archives = combined; } } if (dryMode) { mSdkLog.info("Packages selected for install:\n"); for (ArchiveInfo ai : archives) { Archive a = ai.getNewArchive(); if (a != null) { Package p = a.getParentPackage(); if (p != null) { mSdkLog.info("- %1$s\n", p.getShortDescription()); } } } mSdkLog.info("\nDry mode is on so nothing is actually being installed.\n"); } else { if (acceptLicense(archives, acceptLicense, 100 /* numRetries */)) { return installArchives(archives, NO_TOOLS_MSG); } } } else { mSdkLog.info("There is nothing to install or update.\n"); } return null; } /** * Computes the transitive dependencies of the given list of archives. This will only * include dependencies that also need to be installed, not satisfied dependencies. */ private static List<ArchiveInfo> getDependencies(@NonNull List<ArchiveInfo> archives) { List<ArchiveInfo> dependencies = Lists.newArrayList(); for (ArchiveInfo archive : archives) { addDependencies(dependencies, archive, Sets.<ArchiveInfo>newHashSet()); } return dependencies; } private static void addDependencies(@NonNull List<ArchiveInfo> dependencies, @NonNull ArchiveInfo archive, @NonNull Set<ArchiveInfo> visited) { if (visited.contains(archive)) { return; } visited.add(archive); ArchiveInfo[] dependsOn = archive.getDependsOn(); if (dependsOn != null) { for (ArchiveInfo dependency : dependsOn) { if (!dependencies.contains(dependency)) { dependencies.add(dependency); addDependencies(dependencies, dependency, visited); } } } } /** * Validates that all archive licenses are accepted. * <p/> * There are 2 cases: <br/> * - When {@code acceptLicenses} is given, the licenses specified are automatically * accepted and all those not specified are automatically rejected. <br/> * - When {@code acceptLicenses} is empty or null, licenses are collected and there's * an input prompt on StdOut to ask a yes/no question. To output, this uses the * current {@link #mSdkLog} which should be configured to send * {@link ILogger#info(String, Object...)} directly to {@link System#out}. <br/> * * Finally only accepted licenses are kept in the archive list. * * @param archives The archives to validate. * @param acceptLicenseIds A comma-separated list of licenses ids already approved. * @param numRetries The number of times the command-line will ask to accept a given * license when the input doesn't match the expected y/n/yes/no answer. * Use 0 for infinite. Useful for unit-tests. Once the number of retries * is reached, the license is assumed as rejected. * @return True if there are any archives left to install. */ @VisibleForTesting(visibility=Visibility.PRIVATE) boolean acceptLicense( List<ArchiveInfo> archives, String acceptLicenseIds, final int numRetries) { TreeSet<String> acceptedLids = new TreeSet<String>(); if (acceptLicenseIds != null) { acceptedLids.addAll(Arrays.asList(acceptLicenseIds.split(","))); //$NON-NLS-1$ } boolean automated = !acceptedLids.isEmpty(); TreeSet<String> rejectedLids = new TreeSet<String>(); TreeMap<String, License> lidToAccept = new TreeMap<String, License>(); TreeMap<String, List<String>> lidPkgNames = new TreeMap<String, List<String>>(); // Find the licenses needed. Include those already accepted. for (ArchiveInfo ai : archives) { License lic = getArchiveInfoLicense(ai); if (lic == null) { continue; } String lid = getLicenseId(lic); if (!acceptedLids.contains(lid)) { if (automated) { // Automatically reject those not already accepted rejectedLids.add(lid); } else { // Queue it to ask for it to be accepted lidToAccept.put(lid, lic); List<String> list = lidPkgNames.get(lid); if (list == null) { list = new ArrayList<String>(); lidPkgNames.put(lid, list); } list.add(ai.getShortDescription()); } } } // Ask for each license that needs to be asked manually for confirmation nextEntry: for (Map.Entry<String, License> entry : lidToAccept.entrySet()) { String lid = entry.getKey(); License lic = entry.getValue(); mSdkLog.info("-------------------------------\n"); mSdkLog.info("License id: %1$s\n", lid); mSdkLog.info("Used by: \n - %1$s\n", Joiner.on("\n - ").skipNulls().join(lidPkgNames.get(lid))); mSdkLog.info("-------------------------------\n\n"); mSdkLog.info("%1$s\n", lic.getLicense()); int retries = numRetries; tryAgain: while(true) { try { mSdkLog.info("Do you accept the license '%1$s' [y/n]: ", lid); byte[] buffer = new byte[256]; if (mSdkLog instanceof IReaderLogger) { ((IReaderLogger) mSdkLog).readLine(buffer); } else { System.in.read(buffer); } mSdkLog.info("\n"); String reply = new String(buffer, Charsets.UTF_8); reply = reply.trim().toLowerCase(Locale.US); if ("y".equals(reply) || "yes".equals(reply)) { acceptedLids.add(lid); continue nextEntry; } else if ("n".equals(reply) || "no".equals(reply)) { break tryAgain; } else { mSdkLog.info("Unknown response '%1$s'.\n", reply); if (--retries == 0) { mSdkLog.info("Max number of retries exceeded. Rejecting '%1$s'\n", lid); break tryAgain; } continue tryAgain; } } catch (IOException e) { // Panic. Don't install anything. e.printStackTrace(); return false; } } rejectedLids.add(lid); } // Finally remove all archive which license is rejected or not accepted. for (Iterator<ArchiveInfo> it = archives.iterator(); it.hasNext(); ) { ArchiveInfo ai = it.next(); License lic = getArchiveInfoLicense(ai); if (lic == null) { continue; } String lid = getLicenseId(lic); if (rejectedLids.contains(lid) || !acceptedLids.contains(lid)) { mSdkLog.info("Package %1$s not installed due to rejected license '%2$s'.\n", ai.getShortDescription(), lid); it.remove(); } } return !archives.isEmpty(); } private License getArchiveInfoLicense(ArchiveInfo ai) { Archive a = ai.getNewArchive(); if (a != null) { Package p = a.getParentPackage(); if (p != null) { License lic = p.getLicense(); if (lic != null && lic.getLicenseRef() != null && !lic.getLicense().isEmpty() && lic.getLicense() != null && !lic.getLicense().isEmpty()) { return lic; } } } return null; } private String getLicenseId(License lic) { return String.format("%1$s-%2$08x", //$NON-NLS-1$ lic.getLicenseRef(), lic.getLicense().hashCode()); } @SuppressWarnings("unchecked") private void mapFilterToPackageClass( HashMap<String, Class<? extends Package>> inOutPkgMap, String[] nodes) { // Automatically find the classes matching the node names ClassLoader classLoader = getClass().getClassLoader(); String basePackage = Package.class.getPackage().getName(); for (String node : nodes) { // Capitalize the name String name = node.substring(0, 1).toUpperCase() + node.substring(1); // We can have one dash at most in a name. If it's present, we'll try // with the dash or with the next letter capitalized. int dash = name.indexOf('-'); if (dash > 0) { name = name.replaceFirst("-", ""); } for (int alternatives = 0; alternatives < 2; alternatives++) { String fqcn = basePackage + '.' + name + "Package"; //$NON-NLS-1$ try { Class<? extends Package> clazz = (Class<? extends Package>) classLoader.loadClass(fqcn); if (clazz != null) { inOutPkgMap.put(node, clazz); continue; } } catch (Throwable ignore) { } if (alternatives == 0 && dash > 0) { // Try an alternative where the next letter after the dash // is converted to an upper case. name = name.substring(0, dash) + name.substring(dash, dash + 1).toUpperCase() + name.substring(dash + 1); } else { break; } } } } /** * Refresh all sources. This is invoked either internally (reusing an existing monitor) * or as a UI callback on the remote page "Refresh" button (in which case the monitor is * null and a new task should be created.) * * @param forceFetching When true, load sources that haven't been loaded yet. * When false, only refresh sources that have been loaded yet. */ public void refreshSources(final boolean forceFetching) { assert mTaskFactory != null; final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); mTaskFactory.start("Refresh Sources", new ITask() { @Override public void run(ITaskMonitor monitor) { getPackageLoader().loadRemoteAddonsList(monitor); SdkSource[] sources = getSources().getAllSources(); monitor.setDescription("Refresh Sources"); monitor.setProgressMax(monitor.getProgress() + sources.length); for (SdkSource source : sources) { if (forceFetching || source.getPackages() != null || source.getFetchError() != null) { source.load(getDownloadCache(), monitor.createSubMonitor(1), forceHttp); } monitor.incProgress(1); } } }); } /** * Safely invoke all the registered {@link ISdkChangeListener#onSdkLoaded()}. * This can be called from any thread. */ public void broadcastOnSdkLoaded() { if (!mListeners.isEmpty()) { runOnUiThread(new Runnable() { @Override public void run() { for (ISdkChangeListener listener : mListeners) { try { listener.onSdkLoaded(); } catch (Throwable t) { mSdkLog.error(t, null); } } } }); } } /** * Safely invoke all the registered {@link ISdkChangeListener#onSdkReload()}. * This can be called from any thread. */ private void broadcastOnSdkReload() { if (!mListeners.isEmpty()) { runOnUiThread(new Runnable() { @Override public void run() { for (ISdkChangeListener listener : mListeners) { try { listener.onSdkReload(); } catch (Throwable t) { mSdkLog.error(t, null); } } } }); } } /** * Safely invoke all the registered {@link ISdkChangeListener#preInstallHook()}. * This can be called from any thread. */ private void broadcastPreInstallHook() { if (!mListeners.isEmpty()) { runOnUiThread(new Runnable() { @Override public void run() { for (ISdkChangeListener listener : mListeners) { try { listener.preInstallHook(); } catch (Throwable t) { mSdkLog.error(t, null); } } } }); } } /** * Safely invoke all the registered {@link ISdkChangeListener#postInstallHook()}. * This can be called from any thread. */ private void broadcastPostInstallHook() { if (!mListeners.isEmpty()) { runOnUiThread(new Runnable() { @Override public void run() { for (ISdkChangeListener listener : mListeners) { try { listener.postInstallHook(); } catch (Throwable t) { mSdkLog.error(t, null); } } } }); } } /** * Internal helper to return a new {@link ArchiveInstaller}. * This allows us to override the installer for unit-testing. */ @VisibleForTesting(visibility=Visibility.PRIVATE) protected ArchiveInstaller createArchiveInstaler() { return new ArchiveInstaller(); } }