/*
* 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.sdkuilib.internal.repository;
import com.android.annotations.VisibleForTesting;
import com.android.annotations.VisibleForTesting.Visibility;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkConstants;
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.AddonPackage;
import com.android.sdklib.internal.repository.AddonsListFetcher;
import com.android.sdklib.internal.repository.Archive;
import com.android.sdklib.internal.repository.ArchiveInstaller;
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.Package;
import com.android.sdklib.internal.repository.PlatformToolPackage;
import com.android.sdklib.internal.repository.SdkAddonSource;
import com.android.sdklib.internal.repository.SdkRepoSource;
import com.android.sdklib.internal.repository.SdkSource;
import com.android.sdklib.internal.repository.SdkSourceCategory;
import com.android.sdklib.internal.repository.SdkSources;
import com.android.sdklib.internal.repository.ToolPackage;
import com.android.sdklib.internal.repository.AddonsListFetcher.Site;
import com.android.sdklib.repository.SdkAddonConstants;
import com.android.sdklib.repository.SdkAddonsListConstants;
import com.android.sdklib.repository.SdkRepoConstants;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
import com.android.sdkuilib.repository.ISdkChangeListener;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
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.Map;
/**
* Data shared between {@link UpdaterWindowImpl} and its pages.
*/
class UpdaterData implements IUpdaterData {
private String mOsSdkRoot;
private final ISdkLog mSdkLog;
private ITaskFactory mTaskFactory;
private SdkManager mSdkManager;
private AvdManager mAvdManager;
private final LocalSdkParser mLocalSdkParser = new LocalSdkParser();
private final SdkSources mSources = new SdkSources();
private final LocalSdkAdapter mLocalSdkAdapter = new LocalSdkAdapter(this);
private final RepoSourcesAdapter mSourcesAdapter = new RepoSourcesAdapter(this);
private ImageFactory mImageFactory;
private final SettingsController mSettingsController;
private final ArrayList<ISdkChangeListener> mListeners = new ArrayList<ISdkChangeListener>();
private Shell mWindowShell;
private AndroidLocationException mAvdManagerInitError;
/**
* 0 = need to fetch remote addons list once..
* 1 = fetch succeeded, don't need to do it any more.
* -1= fetch failed, do it again only if the user requests a refresh
* or changes the force-http setting.
*/
private int mStateFetchRemoteAddonsList;
/**
* Creates a new updater data.
*
* @param sdkLog Logger. Cannot be null.
* @param osSdkRoot The OS path to the SDK root.
*/
public UpdaterData(String osSdkRoot, ISdkLog sdkLog) {
mOsSdkRoot = osSdkRoot;
mSdkLog = sdkLog;
mSettingsController = new SettingsController(this);
initSdk();
}
// ----- getters, setters ----
public String getOsSdkRoot() {
return mOsSdkRoot;
}
public void setTaskFactory(ITaskFactory taskFactory) {
mTaskFactory = taskFactory;
}
public ITaskFactory getTaskFactory() {
return mTaskFactory;
}
public SdkSources getSources() {
return mSources;
}
public RepoSourcesAdapter getSourcesAdapter() {
return mSourcesAdapter;
}
public LocalSdkParser getLocalSdkParser() {
return mLocalSdkParser;
}
public LocalSdkAdapter getLocalSdkAdapter() {
return mLocalSdkAdapter;
}
public ISdkLog getSdkLog() {
return mSdkLog;
}
public void setImageFactory(ImageFactory imageFactory) {
mImageFactory = imageFactory;
}
public ImageFactory getImageFactory() {
return mImageFactory;
}
public SdkManager getSdkManager() {
return mSdkManager;
}
public AvdManager getAvdManager() {
return mAvdManager;
}
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 void setWindowShell(Shell windowShell) {
mWindowShell = windowShell;
}
public Shell getWindowShell() {
return mWindowShell;
}
/**
* 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);
// We may not have any UI. Only display a dialog if there's a window shell available.
if (mWindowShell != null) {
MessageDialog.openError(mWindowShell,
"Android Virtual Devices Manager",
error);
} else {
mSdkLog.error(null /* Throwable */, "%s", error); //$NON-NLS-1$
}
return true;
}
return false;
}
// -----
/**
* Initializes the {@link SdkManager} and the {@link AvdManager}.
*/
@VisibleForTesting(visibility=Visibility.PRIVATE)
protected void initSdk() {
setSdkManager(SdkManager.createManager(mOsSdkRoot, mSdkLog));
try {
mAvdManager = null; // remove the old one if needed.
mAvdManager = new AvdManager(mSdkManager, 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();
}
@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.
* <p/>
* Note that the "remote add-ons" list is not loaded from here. Instead
* it is fetched the first time the {@link RemotePackagesPage} is displayed.
*/
public void setupDefaultSources() {
SdkSources sources = getSources();
sources.add(SdkSourceCategory.ANDROID_REPO,
new SdkRepoSource(SdkRepoConstants.URL_GOOGLE_SDK_SITE,
SdkSourceCategory.ANDROID_REPO.getUiName()));
// Load user sources
sources.loadUserAddons(getSdkLog());
// SDK_TEST_URLS is a semicolon-separated list of URLs that can be used to
// seed the SDK Updater list for full repos and addon repositories. This is
// only meant as a debugging and QA testing tool and not for user usage.
//
// To be used, the URLs must either end with the / or end with the canonical
// filename expected for either a full repo or an add-on repo. This lets QA
// use URLs ending with / to cover all cases.
String str = System.getenv("SDK_TEST_URLS");
if (str != null) {
String[] urls = str.split(";");
for (String url : urls) {
if (url != null) {
url = url.trim();
if (url.endsWith("/") || url.endsWith(SdkRepoConstants.URL_DEFAULT_FILENAME)) {
String fullUrl = url;
if (fullUrl.endsWith("/")) {
fullUrl += SdkRepoConstants.URL_DEFAULT_FILENAME;
}
SdkSource s = new SdkRepoSource(fullUrl, null/*uiName*/);
if (!sources.hasSourceUrl(s)) {
sources.add(SdkSourceCategory.GETENV_REPOS, s);
}
}
if (url.endsWith("/") || url.endsWith(SdkAddonConstants.URL_DEFAULT_FILENAME)) {
String fullUrl = url;
if (fullUrl.endsWith("/")) {
fullUrl += SdkAddonConstants.URL_DEFAULT_FILENAME;
}
SdkSource s = new SdkAddonSource(fullUrl, null/*uiName*/);
if (!sources.hasSourceUrl(s)) {
sources.add(SdkSourceCategory.GETENV_ADDONS, s);
}
}
}
}
}
}
/**
* 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() {
LocalSdkParser parser = getLocalSdkParser();
Package[] packages = parser.getPackages();
if (packages == null) {
// load on demand the first time
packages = parser.parseSdk(getOsSdkRoot(), getSdkManager(), getSdkLog());
}
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.
*/
@VisibleForTesting(visibility=Visibility.PRIVATE)
protected void installArchives(final List<ArchiveInfo> archives) {
if (mTaskFactory == null) {
throw new IllegalArgumentException("Task Factory is null");
}
final boolean forceHttp = getSettingsController().getForceHttp();
// sort all archives based on their dependency level.
Collections.sort(archives, new InstallOrderComparator());
mTaskFactory.start("Installing Archives", new ITask() {
public void run(ITaskMonitor monitor) {
final int progressPerArchive = 2 * ArchiveInstaller.NUM_MONITOR_INC;
monitor.setProgressMax(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()) {
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.setResult("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.setResult("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(archive,
mOsSdkRoot,
forceHttp,
mSdkManager,
monitor)) {
// We installed this 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.setResult(msg);
mSdkLog.error(t, msg);
} 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.setResult("Updated ADB to support the USB devices declared in the SDK add-ons.");
} catch (Exception e) {
mSdkLog.error(e, "Update ADB failed");
monitor.setResult("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();
}
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();
}
}
});
}
/**
* 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>();
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.
*
* @param monitor
*/
private void askForAdbRestart(ITaskMonitor monitor) {
final boolean[] canRestart = new boolean[] { true };
if (getWindowShell() != null && getSettingsController().getAskBeforeAdbRestart()) {
// need to ask for permission first
Display display = getWindowShell().getDisplay();
display.syncExec(new Runnable() {
public void run() {
canRestart[0] = MessageDialog.openQuestion(getWindowShell(),
"ADB Restart",
"A package that depends on ADB has been updated. \n" +
"Do you want to restart ADB now?");
}
});
}
if (canRestart[0]) {
AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor);
adb.stopAdb();
adb.startAdb();
}
}
private void notifyToolsNeedsToBeRestarted() {
if (getWindowShell() == null) {
// We don't need to print anything if this is a standalone console update.
return;
}
Display display = getWindowShell().getDisplay();
display.syncExec(new Runnable() {
public void run() {
MessageDialog.openInformation(getWindowShell(),
"Android Tools Updated",
"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 started this window from Eclipse, please check if the Android " +
"plug-in needs to be updated.");
}
});
}
/**
* 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.
*/
public void updateOrInstallAll_WithGUI(
Collection<Archive> selectedArchives,
boolean includeObsoletes) {
// Note: we no longer call refreshSources(true) here. This will be done
// automatically by computeUpdates() iif it needs to access sources to
// resolve missing dependencies.
UpdaterLogic ul = new UpdaterLogic(this);
List<ArchiveInfo> archives = ul.computeUpdates(
selectedArchives,
getSources(),
getLocalSdkParser().getPackages(),
includeObsoletes);
if (selectedArchives == null) {
loadRemoteAddonsList();
ul.addNewPlatforms(
archives,
getSources(),
getLocalSdkParser().getPackages(),
includeObsoletes);
}
// TODO if selectedArchives is null and archives.len==0, find if there are
// any new platform we can suggest to install instead.
Collections.sort(archives);
UpdateChooserDialog dialog = new UpdateChooserDialog(getWindowShell(), this, archives);
dialog.open();
ArrayList<ArchiveInfo> result = dialog.getResult();
if (result != null && result.size() > 0) {
installArchives(result);
}
}
/**
* Tries to update all the *existing* local packages.
* This version is intended to run without a GUI and
* only outputs to the current {@link ISdkLog}.
*
* @param pkgFilter A list of {@link SdkRepoConstants#NODES} to limit the type of packages
* we can update. A null or empty list means to update everything possible.
* @param includeObsoletes True to also list and install obsolete packages.
* @param dryMode True to check what would be updated/installed but do not actually
* download or install anything.
*/
@SuppressWarnings("unchecked")
public void updateOrInstallAll_NoGUI(
Collection<String> pkgFilter,
boolean includeObsoletes,
boolean dryMode) {
refreshSources(true);
UpdaterLogic ul = new UpdaterLogic(this);
List<ArchiveInfo> archives = ul.computeUpdates(
null /*selectedArchives*/,
getSources(),
getLocalSdkParser().getPackages(),
includeObsoletes);
loadRemoteAddonsList();
ul.addNewPlatforms(
archives,
getSources(),
getLocalSdkParser().getPackages(),
includeObsoletes);
Collections.sort(archives);
// Filter the selected archives to only keep the ones matching the filter
if (pkgFilter != null && pkgFilter.size() > 0 && archives != null && archives.size() > 0) {
// Map filter types to an SdkRepository Package type.
HashMap<String, Class<? extends Package>> pkgMap =
new HashMap<String, Class<? extends Package>>();
// Automatically find the classes matching the node names
ClassLoader classLoader = getClass().getClassLoader();
String basePackage = Package.class.getPackage().getName();
for (String node : SdkRepoConstants.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$ //$NON-NLS-2$
try {
Class<? extends Package> clazz =
(Class<? extends Package>) classLoader.loadClass(fqcn);
if (clazz != null) {
pkgMap.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;
}
}
}
if (SdkRepoConstants.NODES.length != pkgMap.size()) {
// Sanity check in case we forget to update this node array.
// We don't cancel the operation though.
mSdkLog.printf(
"*** Filter Mismatch! ***\n" +
"*** The package filter list has changed. Please report this.");
}
// Now make a set of the types that are allowed by the filter.
HashSet<Class<? extends Package>> allowedPkgSet =
new HashSet<Class<? extends Package>>();
for (String type : pkgFilter) {
if (pkgMap.containsKey(type)) {
allowedPkgSet.add(pkgMap.get(type));
} else {
// This should not happen unless there's a mismatch in the package map.
mSdkLog.error(null, "Ignoring unknown package filter '%1$s'", type);
}
}
// we don't need the map anymore
pkgMap = null;
Iterator<ArchiveInfo> it = archives.iterator();
while (it.hasNext()) {
boolean keep = false;
ArchiveInfo ai = it.next();
Archive a = ai.getNewArchive();
if (a != null) {
Package p = a.getParentPackage();
if (p != null && allowedPkgSet.contains(p.getClass())) {
keep = true;
}
}
if (!keep) {
it.remove();
}
}
if (archives.size() == 0) {
mSdkLog.printf("The package filter removed all packages. There is nothing to install.\n" +
"Please consider trying updating again without a package filter.\n");
return;
}
}
if (archives != null && archives.size() > 0) {
if (dryMode) {
mSdkLog.printf("Packages selected for install:\n");
for (ArchiveInfo ai : archives) {
Archive a = ai.getNewArchive();
if (a != null) {
Package p = a.getParentPackage();
if (p != null) {
mSdkLog.printf("- %1$s\n", p.getShortDescription());
}
}
}
mSdkLog.printf("\nDry mode is on so nothing will actually be installed.\n");
} else {
installArchives(archives);
}
} else {
mSdkLog.printf("There is nothing to install or update.\n");
}
}
/**
* 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().getForceHttp();
mTaskFactory.start("Refresh Sources", new ITask() {
public void run(ITaskMonitor monitor) {
if (mStateFetchRemoteAddonsList <= 0) {
loadRemoteAddonsListInTask(monitor);
}
SdkSource[] sources = mSources.getAllSources();
monitor.setProgressMax(monitor.getProgress() + sources.length);
for (SdkSource source : sources) {
if (forceFetching ||
source.getPackages() != null ||
source.getFetchError() != null) {
source.load(monitor.createSubMonitor(1), forceHttp);
}
monitor.incProgress(1);
}
}
});
}
/**
* Loads the remote add-ons list.
*/
public void loadRemoteAddonsList() {
if (mStateFetchRemoteAddonsList != 0) {
return;
}
mTaskFactory.start("Load Add-ons List", new ITask() {
public void run(ITaskMonitor monitor) {
loadRemoteAddonsListInTask(monitor);
}
});
}
private void loadRemoteAddonsListInTask(ITaskMonitor monitor) {
mStateFetchRemoteAddonsList = -1;
// SDK_TEST_URLS is a semicolon-separated list of URLs that can be used to
// seed the SDK Updater list. This is only meant as a debugging and QA testing
// tool and not for user usage.
//
// To be used, the URLs must either end with the / or end with the canonical
// filename expected for an addon list. This lets QA use URLs ending with /
// to cover all cases.
//
// Since SDK_TEST_URLS can contain many such URLs, we take the first one that
// matches our criteria.
String url = System.getenv("SDK_TEST_URLS");
if (url == null) {
// No override, use the canonical URL.
url = SdkAddonsListConstants.URL_ADDON_LIST;
} else {
String[] urls = url.split(";");
url = null;
for (String u : urls) {
u = u.trim();
// This is an URL that comes from the env var. We expect it to either
// end with a / or the canonical name, otherwise we don't use it.
if (u.endsWith("/")) {
url = u + SdkAddonsListConstants.URL_DEFAULT_FILENAME;
break;
} else if (u.endsWith(SdkAddonsListConstants.URL_DEFAULT_FILENAME)) {
url = u;
break;
}
}
}
if (url != null) {
if (getSettingsController().getForceHttp()) {
url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$
}
AddonsListFetcher fetcher = new AddonsListFetcher();
Site[] sites = fetcher.fetch(monitor, url);
if (sites != null) {
mSources.removeAll(SdkSourceCategory.ADDONS_3RD_PARTY);
for (Site s : sites) {
mSources.add(SdkSourceCategory.ADDONS_3RD_PARTY,
new SdkAddonSource(s.getUrl(), s.getUiName()));
}
mStateFetchRemoteAddonsList = 1;
}
}
}
/**
* Safely invoke all the registered {@link ISdkChangeListener#onSdkLoaded()}.
* This can be called from any thread.
*/
/*package*/ void broadcastOnSdkLoaded() {
if (mWindowShell != null && mListeners.size() > 0) {
mWindowShell.getDisplay().syncExec(new Runnable() {
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 (mWindowShell != null && mListeners.size() > 0) {
mWindowShell.getDisplay().syncExec(new Runnable() {
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 (mWindowShell != null && mListeners.size() > 0) {
mWindowShell.getDisplay().syncExec(new Runnable() {
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 (mWindowShell != null && mListeners.size() > 0) {
mWindowShell.getDisplay().syncExec(new Runnable() {
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();
}
}