aboutsummaryrefslogtreecommitdiffstats
path: root/sdkmanager
diff options
context:
space:
mode:
authorRaphael Moll <ralf@android.com>2012-04-12 14:41:25 -0700
committerRaphael Moll <ralf@android.com>2012-04-16 15:04:14 -0700
commitaf832e8eeb4eb7e24a292ed912ce8eb7cc2a8233 (patch)
treeb28f218a8a409dbee35786ea68df82ee9eea3068 /sdkmanager
parent72d16d222e7205cab473d01f483c534747c9026b (diff)
downloadsdk-af832e8eeb4eb7e24a292ed912ce8eb7cc2a8233.zip
sdk-af832e8eeb4eb7e24a292ed912ce8eb7cc2a8233.tar.gz
sdk-af832e8eeb4eb7e24a292ed912ce8eb7cc2a8233.tar.bz2
SDK: primitive implementation of download cache.
It supports: - A local binary cache + a few http headers are saved - If ETag is present, generates a GET with If-None-Match - If Last-Modified is present, generates a GET with If-Modified-Since - Ability to configure the cache to be direct (don't cache), or serve without checkout or serve with a server check. - Doesn't check cached files if newer than 10 minutes. - For servers with no ETag/LastModified support, check files every 4 hours (no pref to change this yet.) Change-Id: I515e77291fb6810453e82e73f6508cfc60b2f422
Diffstat (limited to 'sdkmanager')
-rw-r--r--sdkmanager/libs/sdklib/.classpath8
-rw-r--r--sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java3
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java45
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java9
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DownloadCache.java667
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java52
-rw-r--r--sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java67
-rwxr-xr-xsdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/ArchiveInstallerTest.java13
-rwxr-xr-xsdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java30
-rwxr-xr-xsdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IUpdaterData.java3
-rwxr-xr-xsdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterLogic.java2
-rwxr-xr-xsdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java22
-rwxr-xr-xsdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackageLoader.java23
-rwxr-xr-xsdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesPage.java22
-rwxr-xr-xsdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/SdkUpdaterWindowImpl2.java2
-rwxr-xr-xsdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java25
-rwxr-xr-xsdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java6
17 files changed, 841 insertions, 158 deletions
diff --git a/sdkmanager/libs/sdklib/.classpath b/sdkmanager/libs/sdklib/.classpath
index a82525e..3e6a435 100644
--- a/sdkmanager/libs/sdklib/.classpath
+++ b/sdkmanager/libs/sdklib/.classpath
@@ -5,13 +5,13 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
- <classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/commons-compress/commons-compress-1.0.jar"/>
<classpathentry combineaccessrules="false" kind="src" path="/common"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/commons-compress/commons-compress-1.0.jar"/>
<classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/commons-codec-1.4.jar"/>
<classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/commons-logging-1.1.1.jar"/>
- <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpclient-4.1.1.jar"/>
- <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpcore-4.1.jar"/>
- <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpmime-4.1.1.jar"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpclient-4.1.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/http-client/src/httpcomponents-client-4.1.1-src.zip"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpcore-4.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/http-client/src/httpcomponents-core-4.1-src.zip"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpmime-4.1.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/http-client/src/httpcomponents-client-4.1.1-src.zip"/>
<classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/mkidentity/mkidentity-prebuilt.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
index ebdc9c8..6e6c657 100644
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
@@ -260,6 +260,9 @@ public final class SdkConstants {
/** Name of the addon libs folder. */
public final static String FD_ADDON_LIBS = "libs"; //$NON-NLS-1$
+ /** Name of the cache folder in the $HOME/.android. */
+ public final static String FD_CACHE = "cache"; //$NON-NLS-1$
+
/** Namespace for the resource XML, i.e. "http://schemas.android.com/apk/res/android" */
public final static String NS_RESOURCES =
"http://schemas.android.com/apk/res/android"; //$NON-NLS-1$
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java
index 919a30e..216e309 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java
@@ -29,7 +29,6 @@ import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
-import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -82,13 +81,14 @@ public class AddonsListFetcher {
/**
* Fetches the addons list from the given URL.
*
- * @param monitor A monitor to report errors. Cannot be null.
* @param url The URL of an XML file resource that conforms to the latest sdk-addons-list-N.xsd.
* For the default operation, use {@link SdkAddonsListConstants#URL_ADDON_LIST}.
* Cannot be null.
+ * @param cache The {@link DownloadCache} instance to use. Cannot be null.
+ * @param monitor A monitor to report errors. Cannot be null.
* @return An array of {@link Site} on success (possibly empty), or null on error.
*/
- public Site[] fetch(ITaskMonitor monitor, String url) {
+ public Site[] fetch(String url, DownloadCache cache, ITaskMonitor monitor) {
url = url == null ? "" : url.trim();
@@ -102,7 +102,7 @@ public class AddonsListFetcher {
Document validatedDoc = null;
String validatedUri = null;
- ByteArrayInputStream xml = fetchUrl(url, monitor.createSubMonitor(1), exception);
+ InputStream xml = fetchUrl(url, cache, monitor.createSubMonitor(1), exception);
if (xml != null) {
monitor.setDescription("Validate XML");
@@ -187,41 +187,12 @@ public class AddonsListFetcher {
* happens during the fetch.
* @see UrlOpener UrlOpener, which handles all URL logic.
*/
- private ByteArrayInputStream fetchUrl(String urlString, ITaskMonitor monitor,
+ private InputStream fetchUrl(String urlString,
+ DownloadCache cache,
+ ITaskMonitor monitor,
Exception[] outException) {
try {
-
- InputStream is = null;
-
- int inc = 65536;
- int curr = 0;
- byte[] result = new byte[inc];
-
- try {
- is = UrlOpener.openUrl(urlString, monitor);
-
- int n;
- while ((n = is.read(result, curr, result.length - curr)) != -1) {
- curr += n;
- if (curr == result.length) {
- byte[] temp = new byte[curr + inc];
- System.arraycopy(result, 0, temp, 0, curr);
- result = temp;
- }
- }
-
- return new ByteArrayInputStream(result, 0, curr);
-
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- // pass
- }
- }
- }
-
+ return cache.openCachedUrl(urlString, monitor);
} catch (Exception e) {
if (outException != null) {
outException[0] = e;
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java
index 2e2396f..53fc140 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java
@@ -96,6 +96,7 @@ public class ArchiveInstaller {
String osSdkRoot,
boolean forceHttp,
SdkManager sdkManager,
+ DownloadCache cache,
ITaskMonitor monitor) {
Archive newArchive = archiveInfo.getNewArchive();
@@ -122,7 +123,7 @@ public class ArchiveInstaller {
return false;
}
- archiveFile = downloadFile(newArchive, osSdkRoot, monitor, forceHttp);
+ archiveFile = downloadFile(newArchive, osSdkRoot, cache, monitor, forceHttp);
if (archiveFile != null) {
// Unarchive calls the pre/postInstallHook methods.
if (unarchive(archiveInfo, osSdkRoot, archiveFile, sdkManager, monitor)) {
@@ -143,6 +144,7 @@ public class ArchiveInstaller {
@VisibleForTesting(visibility=Visibility.PRIVATE)
protected File downloadFile(Archive archive,
String osSdkRoot,
+ DownloadCache cache,
ITaskMonitor monitor,
boolean forceHttp) {
@@ -219,7 +221,7 @@ public class ArchiveInstaller {
mFileOp.deleteFileOrFolder(tmpFile);
}
- if (fetchUrl(archive, tmpFile, link, pkgName, monitor)) {
+ if (fetchUrl(archive, tmpFile, link, pkgName, cache, monitor)) {
// Fetching was successful, let's use this file.
return tmpFile;
} else {
@@ -303,12 +305,13 @@ public class ArchiveInstaller {
File tmpFile,
String urlString,
String pkgName,
+ DownloadCache cache,
ITaskMonitor monitor) {
FileOutputStream os = null;
InputStream is = null;
try {
- is = UrlOpener.openUrl(urlString, monitor);
+ is = cache.openDirectUrl(urlString, monitor);
os = new FileOutputStream(tmpFile);
MessageDigest digester = archive.getChecksumType().getMessageDigest();
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DownloadCache.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DownloadCache.java
new file mode 100755
index 0000000..039e165
--- /dev/null
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DownloadCache.java
@@ -0,0 +1,667 @@
+/*
+ * Copyright (C) 2012 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;
+
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.annotations.VisibleForTesting.Visibility;
+import com.android.prefs.AndroidLocation;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.SdkConstants;
+import com.android.sdklib.internal.repository.UrlOpener.CanceledByUserException;
+import com.android.util.Pair;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.message.BasicHeader;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * A simple cache for the XML resources handled by the SDK Manager.
+ * <p/>
+ * Callers should use {@link #openDirectUrl(String, ITaskMonitor)} to download "large files"
+ * that should not be cached (like actual installation packages which are several MBs big)
+ * and call {@link #openCachedUrl(String, ITaskMonitor)} to download small XML files.
+ * <p/>
+ * The cache can work in 3 different strategies (direct is a pass-through, fresh-cache is the
+ * default and tries to update resources if they are older than 10 minutes by respecting
+ * either ETag or Last-Modified, and finally server-cache is a strategy to always serve
+ * cached entries if present.)
+ */
+public class DownloadCache {
+
+ /*
+ * HTTP/1.1 references:
+ * - Possible headers:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ * - Rules about conditional requests:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
+ * - Error codes:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
+ */
+
+ private static final boolean DEBUG = System.getenv("SDKMAN_DEBUG_CACHE") != null;
+
+ /** Key for the Status-Code in the info properties. */
+ private static final String KEY_STATUS_CODE = "Status-Code"; //$NON-NLS-1$
+ /** Key for the URL in the info properties. */
+ private static final String KEY_URL = "URL"; //$NON-NLS-1$
+
+ /** Prefix of binary files stored in the {@link SdkConstants#FD_CACHE} directory. */
+ private final static String BIN_FILE_PREFIX = "sdkbin-"; //$NON-NLS-1$
+ /** Prefix of meta info files stored in the {@link SdkConstants#FD_CACHE} directory. */
+ private final static String INFO_FILE_PREFIX = "sdkinf-"; //$NON-NLS-1$
+
+ /**
+ * Minimum time before we consider a cached entry is potentially stale.
+ * Expressed in milliseconds.
+ * <p/>
+ * When using the {@link Strategy#FRESH_CACHE}, the cache will not try to refresh
+ * a cached file if it's has been saved more recently than this time.
+ * When using the direct mode or the serve mode, the cache either doesn't serve
+ * cached files or always serves caches files so this expiration delay is not used.
+ * <p/>
+ * Default is 10 minutes.
+ * <p/>
+ * TODO: change for a dynamic preference later.
+ */
+ private final static long MIN_TIME_EXPIRED_MS = 10*60*1000;
+ /**
+ * Maximum time before we consider a cache entry to be stale.
+ * Expressed in milliseconds.
+ * <p/>
+ * When using the {@link Strategy#FRESH_CACHE}, entries that have no ETag
+ * or Last-Modified will be refreshed if their file timestamp is older than
+ * this value.
+ * <p/>
+ * Default is 4 hours.
+ * <p/>
+ * TODO: change for a dynamic preference later.
+ */
+ private final static long MAX_TIME_EXPIRED_MS = 4*60*60*1000;
+
+ /**
+ * The maximum file size we'll cache for "small" files.
+ * 640KB is more than enough and is already a stretch since these are read in memory.
+ * (The actual typical size of the files handled here is in the 4-64KB range.)
+ */
+ private final static int MAX_SMALL_FILE_SIZE = 640 * 1024;
+
+ /**
+ * HTTP Headers that are saved in an info file.
+ * For HTTP/1.1 header names, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ */
+ private final static String[] INFO_HTTP_HEADERS = {
+ HttpHeaders.LAST_MODIFIED,
+ HttpHeaders.ETAG,
+ HttpHeaders.CONTENT_LENGTH,
+ HttpHeaders.DATE
+ };
+
+ private final Strategy mStrategy;
+ private final File mCacheRoot;
+
+ public enum Strategy {
+ /**
+ * If the files are available in the cache, serve them as-is, otherwise
+ * download them and return the cached version. No expiration or refresh
+ * is attempted if a file is in the cache.
+ */
+ SERVE_CACHE,
+ /**
+ * If the files are available in the cache, check if there's an update
+ * (either using an e-tag check or comparing to the default time expiration).
+ * If files have expired or are not in the cache then download them and return
+ * the cached version.
+ */
+ FRESH_CACHE,
+ /**
+ * Disables caching. URLs are always downloaded and returned directly.
+ * Downloaded streams aren't cached locally.
+ */
+ DIRECT
+ }
+
+ /** Creates a default instance of the URL cache */
+ public DownloadCache(Strategy strategy) {
+ mCacheRoot = initCacheRoot();
+ mStrategy = mCacheRoot == null ? Strategy.DIRECT : strategy;
+ }
+
+ public Strategy getStrategy() {
+ return mStrategy;
+ }
+
+ /**
+ * Returns the directory to be used as a cache.
+ * Creates it if necessary.
+ * Makes it possible to disable or override the cache location in unit tests.
+ *
+ * @return An existing directory to use as a cache root dir,
+ * or null in case of error in which case the cache will be disabled.
+ */
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected File initCacheRoot() {
+ try {
+ File root = new File(AndroidLocation.getFolder());
+ root = new File(root, SdkConstants.FD_CACHE);
+ if (!root.exists()) {
+ root.mkdirs();
+ }
+ return root;
+ } catch (AndroidLocationException e) {
+ // No root? Disable the cache.
+ return null;
+ }
+ }
+
+ /**
+ * Does a direct download of the given URL using {@link UrlOpener}.
+ * This does not check the download cache and does not attempt to cache the file.
+ * Instead the HttpClient library returns a progressive download stream.
+ * <p/>
+ * For details on realm authentication and user/password handling,
+ * check the underlying {@link UrlOpener#openUrl(String, ITaskMonitor, Header[])}
+ * documentation.
+ *
+ * @param urlString the URL string to be opened.
+ * @param monitor {@link ITaskMonitor} which is related to this URL
+ * fetching.
+ * @return Returns an {@link InputStream} holding the URL content.
+ * @throws IOException Exception thrown when there are problems retrieving
+ * the URL or its content.
+ * @throws CanceledByUserException Exception thrown if the user cancels the
+ * authentication dialog.
+ */
+ public InputStream openDirectUrl(String urlString, ITaskMonitor monitor)
+ throws IOException, CanceledByUserException {
+ if (DEBUG) {
+ System.out.println(String.format("%s : Direct download", urlString)); //$NON-NLS-1$
+ }
+ Pair<InputStream, HttpResponse> result =
+ UrlOpener.openUrl(urlString, monitor, null /*headers*/);
+ return result.getFirst();
+ }
+
+ /**
+ * Downloads a small file, typically XML manifests.
+ * The current {@link Strategy} governs whether the file is served as-is
+ * from the cache, potentially updated first or directly downloaded.
+ * <p/>
+ * For large downloads (e.g. installable archives) please do not invoke the
+ * cache and instead use the {@link #openDirectUrl(String, ITaskMonitor)}
+ * method.
+ * <p/>
+ * For details on realm authentication and user/password handling,
+ * check the underlying {@link UrlOpener#openUrl(String, ITaskMonitor, Header[])}
+ * documentation.
+ *
+ * @param urlString the URL string to be opened.
+ * @param monitor {@link ITaskMonitor} which is related to this URL
+ * fetching.
+ * @return Returns an {@link InputStream} holding the URL content.
+ * @throws IOException Exception thrown when there are problems retrieving
+ * the URL or its content.
+ * @throws CanceledByUserException Exception thrown if the user cancels the
+ * authentication dialog.
+ */
+ public InputStream openCachedUrl(String urlString, ITaskMonitor monitor)
+ throws IOException, CanceledByUserException {
+ // Don't cache in direct mode. Don't try to cache non-http URLs.
+ if (mStrategy == Strategy.DIRECT || !urlString.startsWith("http")) { //$NON-NLS-1$
+ return openDirectUrl(urlString, monitor);
+ }
+
+ File cached = new File(mCacheRoot, getCacheFilename(urlString));
+ File info = new File(mCacheRoot, getInfoFilename(cached.getName()));
+
+ boolean useCached = cached.exists();
+
+ if (useCached && mStrategy == Strategy.FRESH_CACHE) {
+ // Check whether the file should be served from the cache or
+ // refreshed first.
+
+ long cacheModifiedMs = cached.lastModified(); /* last mod time in epoch/millis */
+ boolean checkCache = true;
+
+ Properties props = readInfo(info);
+ if (props == null) {
+ // No properties, no chocolate for you.
+ useCached = false;
+ } else {
+ long minExpiration = System.currentTimeMillis() - MIN_TIME_EXPIRED_MS;
+ checkCache = cacheModifiedMs < minExpiration;
+
+ if (!checkCache && DEBUG) {
+ System.out.println(String.format(
+ "%s : Too fresh [%,d ms], not checking yet.", //$NON-NLS-1$
+ urlString, cacheModifiedMs - minExpiration));
+ }
+ }
+
+ if (useCached && checkCache) {
+ assert props != null;
+
+ // Right now we only support 200 codes and will requery all 404s.
+ String code = props.getProperty(KEY_STATUS_CODE, ""); //$NON-NLS-1$
+ useCached = Integer.toString(HttpStatus.SC_OK).equals(code);
+
+ if (!useCached && DEBUG) {
+ System.out.println(String.format(
+ "%s : cache disabled by code %s", //$NON-NLS-1$
+ urlString, code));
+ }
+
+ if (useCached) {
+ // Do we have a valid Content-Length? If so, it should match the file size.
+ try {
+ long length = Long.parseLong(props.getProperty(HttpHeaders.CONTENT_LENGTH,
+ "-1")); //$NON-NLS-1$
+ if (length >= 0) {
+ useCached = length == cached.length();
+
+ if (!useCached && DEBUG) {
+ System.out.println(String.format(
+ "%s : cache disabled by length mismatch %d, expected %d", //$NON-NLS-1$
+ urlString, length, cached.length()));
+ }
+ }
+ } catch (NumberFormatException ignore) {}
+ }
+
+ if (useCached) {
+ // Do we have an ETag and/or a Last-Modified?
+ String etag = props.getProperty(HttpHeaders.ETAG);
+ String lastMod = props.getProperty(HttpHeaders.LAST_MODIFIED);
+
+ if (etag != null || lastMod != null) {
+ // Details on how to use them is defined at
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
+ // Bottom line:
+ // - if there's an ETag, it should be used first with an
+ // If-None-Match header. That's a strong comparison for HTTP/1.1 servers.
+ // - otherwise use a Last-Modified if an If-Modified-Since header exists.
+ // In this case, we place both and the rules indicates a spec-abiding
+ // server should strongly match ETag and weakly the Modified-Since.
+
+ // TODO there are some servers out there which report ETag/Last-Mod
+ // yet don't honor them when presented with a precondition. In this
+ // case we should identify it in the reply and invalidate ETag support
+ // for these servers and instead fallback on the pure-timeout case below.
+
+ AtomicInteger statusCode = new AtomicInteger(0);
+ InputStream is = null;
+ List<Header> headers = new ArrayList<Header>(2);
+
+ if (etag != null) {
+ headers.add(new BasicHeader(HttpHeaders.IF_NONE_MATCH, etag));
+ }
+
+ if (lastMod != null) {
+ headers.add(new BasicHeader(HttpHeaders.IF_MODIFIED_SINCE, lastMod));
+ }
+
+ if (!headers.isEmpty()) {
+ is = downloadAndCache(urlString, monitor, cached, info,
+ headers.toArray(new Header[headers.size()]),
+ statusCode);
+ }
+
+ if (is != null && statusCode.get() == HttpStatus.SC_OK) {
+ // The resource was modified, the server said there was something
+ // new, which has been cached. We can return that to the caller.
+ return is;
+ }
+
+ // If we get here, we should have is == null and code
+ // could be:
+ // - 304 for not-modified -- same resource, still available, in
+ // which case we'll use the cached one.
+ // - 404 -- resource doesn't exist anymore in which case there's
+ // no point in retrying.
+ // - For any other code, just retry a download.
+
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Exception ignore) {}
+ is = null;
+ }
+
+ if (statusCode.get() == HttpStatus.SC_NOT_MODIFIED) {
+ // Cached file was not modified.
+ // Change its timestamp for the next MIN_TIME_EXPIRED_MS check.
+ cached.setLastModified(System.currentTimeMillis());
+
+ // At this point useCached==true so we'll return
+ // the cached file below.
+ } else {
+ // URL fetch returned something other than 200 or 304.
+ // For 404, we're done, no need to check the server again.
+ // For all other codes, we'll retry a download below.
+ useCached = false;
+ if (statusCode.get() == HttpStatus.SC_NOT_FOUND) {
+ return null;
+ }
+ }
+ } else {
+ // If we don't have an Etag nor Last-Modified, let's use a
+ // basic file timestamp and compare to a 1 hour threshold.
+
+ long maxExpiration = System.currentTimeMillis() - MAX_TIME_EXPIRED_MS;
+ useCached = cacheModifiedMs >= maxExpiration;
+
+ if (!useCached && DEBUG) {
+ System.out.println(String.format(
+ "[%1$s] cache disabled by timestamp %2$tD %2$tT < %3$tD %3$tT", //$NON-NLS-1$
+ urlString, cacheModifiedMs, maxExpiration));
+ }
+ }
+ }
+ }
+ }
+
+ if (useCached) {
+ // The caller needs an InputStream that supports the reset() operation.
+ // The default FileInputStream does not, so load the file into a byte
+ // array and return that.
+ try {
+ InputStream is = readCachedFile(cached);
+ if (is != null) {
+ if (DEBUG) {
+ System.out.println(String.format("%s : Use cached file", urlString)); //$NON-NLS-1$
+ }
+
+ return is;
+ }
+ } catch (IOException ignore) {}
+ }
+
+ // If we're not using the cache, try to remove the cache and download again.
+ try {
+ cached.delete();
+ info.delete();
+ } catch (SecurityException ignore) {}
+
+ return downloadAndCache(urlString, monitor, cached, info,
+ null /*headers*/, null /*statusCode*/);
+ }
+
+ // --------------
+
+ private InputStream readCachedFile(File cached) throws IOException {
+ InputStream is = null;
+
+ int inc = 65536;
+ int curr = 0;
+ long len = cached.length();
+ assert len < Integer.MAX_VALUE;
+ if (len >= MAX_SMALL_FILE_SIZE) {
+ // This is supposed to cache small files, not 2+ GB files.
+ return null;
+ }
+ byte[] result = new byte[(int) (len > 0 ? len : inc)];
+
+ try {
+ is = new FileInputStream(cached);
+
+ int n;
+ while ((n = is.read(result, curr, result.length - curr)) != -1) {
+ curr += n;
+ if (curr == result.length) {
+ byte[] temp = new byte[curr + inc];
+ System.arraycopy(result, 0, temp, 0, curr);
+ result = temp;
+ }
+ }
+
+ return new ByteArrayInputStream(result, 0, curr);
+
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException ignore) {}
+ }
+ }
+ }
+
+ /**
+ * Download, cache and return as an in-memory byte stream.
+ * The download is only done if the server returns 200/OK.
+ * On success, store an info file next to the download with
+ * a few headers.
+ * <p/>
+ * This method deletes the cached file and the info file ONLY if it
+ * attempted a download and it failed to complete. It doesn't erase
+ * anything if there's no download because the server returned a 404
+ * or 304 or similar.
+ *
+ * @return An in-memory byte buffer input stream for the downloaded
+ * and locally cached file, or null if nothing was downloaded
+ * (including if it was a 304 Not-Modified status code.)
+ */
+ private InputStream downloadAndCache(
+ String urlString,
+ ITaskMonitor monitor,
+ File cached,
+ File info,
+ @Nullable Header[] headers,
+ @Nullable AtomicInteger outStatusCode)
+ throws FileNotFoundException, IOException, CanceledByUserException {
+ InputStream is = null;
+ OutputStream os = null;
+
+ int inc = 65536;
+ int curr = 0;
+ byte[] result = new byte[inc];
+
+ try {
+ Pair<InputStream, HttpResponse> r = UrlOpener.openUrl(urlString, monitor, headers);
+
+ is = r.getFirst();
+ HttpResponse response = r.getSecond();
+
+ if (DEBUG) {
+ System.out.println(String.format("%s : fetch: %s => %s", //$NON-NLS-1$
+ urlString,
+ headers == null ? "" : Arrays.toString(headers), //$NON-NLS-1$
+ response.getStatusLine()));
+ }
+
+ int code = response.getStatusLine().getStatusCode();
+
+ if (outStatusCode != null) {
+ outStatusCode.set(code);
+ }
+
+ if (code != HttpStatus.SC_OK) {
+ // Only a 200 response code makes sense here.
+ // Even the other 20x codes should not apply, e.g. no content or partial
+ // content are not statuses we want to handle and should never happen.
+ // (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1 for list)
+ return null;
+ }
+
+ os = new FileOutputStream(cached);
+
+ int n;
+ while ((n = is.read(result, curr, result.length - curr)) != -1) {
+ if (os != null && n > 0) {
+ os.write(result, curr, n);
+ }
+
+ curr += n;
+
+ if (os != null && curr > MAX_SMALL_FILE_SIZE) {
+ // If the file size exceeds our "small file size" threshold,
+ // stop caching. We don't want to fill the disk.
+ try {
+ os.close();
+ } catch (IOException ignore) {}
+ try {
+ cached.delete();
+ info.delete();
+ } catch (SecurityException ignore) {}
+ os = null;
+ }
+ if (curr == result.length) {
+ byte[] temp = new byte[curr + inc];
+ System.arraycopy(result, 0, temp, 0, curr);
+ result = temp;
+ }
+ }
+
+ // Close the output stream, signaling it was stored properly.
+ if (os != null) {
+ try {
+ os.close();
+ os = null;
+
+ saveInfo(urlString, response, info);
+ } catch (IOException ignore) {}
+ }
+
+ return new ByteArrayInputStream(result, 0, curr);
+
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException ignore) {}
+ }
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException ignore) {}
+ // If we get here with the output stream not null, it means there
+ // was an issue and we don't want to keep that file. We'll try to
+ // delete it.
+ try {
+ cached.delete();
+ info.delete();
+ } catch (SecurityException ignore) {}
+ }
+ }
+ }
+
+ /**
+ * Saves part of the HTTP Response to the info file.
+ */
+ private void saveInfo(String urlString, HttpResponse response, File info) throws IOException {
+ Properties props = new Properties();
+
+ // we don't need the status code & URL right now.
+ // Save it in case we want to have it later (e.g. to differentiate 200 and 404.)
+ props.setProperty(KEY_URL, urlString);
+ props.setProperty(KEY_STATUS_CODE,
+ Integer.toString(response.getStatusLine().getStatusCode()));
+
+ for (String name : INFO_HTTP_HEADERS) {
+ Header h = response.getFirstHeader(name);
+ if (h != null) {
+ props.setProperty(name, h.getValue());
+ }
+ }
+
+ FileOutputStream os = null;
+ try {
+ os = new FileOutputStream(info);
+ props.store(os, "## Meta data for SDK Manager cache. Do not modify."); //$NON-NLS-1$
+ } finally {
+ if (os != null) {
+ os.close();
+ }
+ }
+ }
+
+ /**
+ * Reads the info properties file.
+ * @return The properties found or null if there's no file or it can't be read.
+ */
+ private Properties readInfo(File info) {
+ if (info.exists()) {
+ Properties props = new Properties();
+
+ InputStream is = null;
+ try {
+ is = new FileInputStream(info);
+ props.load(is);
+ return props;
+ } catch (IOException ignore) {
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException ignore) {}
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Computes the cache filename for the given URL.
+ * The filename uses the {@link #BIN_FILE_PREFIX}, the full URL string's hashcode and
+ * a sanitized portion of the URL filename. The returned filename is never
+ * more than 64 characters to ensure maximum file system compatibility.
+ *
+ * @param urlString The download URL.
+ * @return A leaf filename for the cached download file.
+ */
+ private String getCacheFilename(String urlString) {
+ String hash = String.format("%08x", urlString.hashCode());
+
+ String leaf = urlString.toLowerCase(Locale.US);
+ if (leaf.length() >= 2) {
+ int index = urlString.lastIndexOf('/', leaf.length() - 2);
+ leaf = urlString.substring(index + 1);
+ }
+
+ leaf = leaf.replaceAll("[^a-z0-9_-]+", "_");
+ leaf = leaf.replaceAll("__+", "_");
+
+ leaf = hash + '-' + leaf;
+ int n = 64 - BIN_FILE_PREFIX.length();
+ if (leaf.length() > n) {
+ leaf = leaf.substring(0, n);
+ }
+
+ return BIN_FILE_PREFIX + leaf;
+ }
+
+ private String getInfoFilename(String cacheFilename) {
+ return cacheFilename.replaceFirst(BIN_FILE_PREFIX, INFO_FILE_PREFIX);
+ }
+}
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java
index 38417f7..749bc01 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java
@@ -32,7 +32,6 @@ import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
-import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -192,7 +191,7 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
/**
* Returns the list of known packages found by the last call to load().
* This is null when the source hasn't been loaded yet -- caller should
- * then call {@link #load(ITaskMonitor, boolean)} to load the packages.
+ * then call {@link #load} to load the packages.
*/
public Package[] getPackages() {
return mPackages;
@@ -306,7 +305,7 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
* null in case of error, in which case {@link #getFetchError()} can be used to an
* error message.
*/
- public void load(ITaskMonitor monitor, boolean forceHttp) {
+ public void load(DownloadCache cache, ITaskMonitor monitor, boolean forceHttp) {
setDefaultDescription();
monitor.setProgressMax(7);
@@ -338,7 +337,7 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
String[] defaultNames = getDefaultXmlFileUrls();
String firstDefaultName = defaultNames.length > 0 ? defaultNames[0] : "";
- InputStream xml = fetchUrl(url, monitor.createSubMonitor(1), exception);
+ InputStream xml = fetchUrl(url, cache, monitor.createSubMonitor(1), exception);
if (xml != null) {
int version = getXmlSchemaVersion(xml);
if (version == 0) {
@@ -365,7 +364,7 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
if (newUrl.equals(url)) {
continue;
}
- xml = fetchUrl(newUrl, subMonitor.createSubMonitor(1), exception);
+ xml = fetchUrl(newUrl, cache, subMonitor.createSubMonitor(1), exception);
if (xml != null) {
int version = getXmlSchemaVersion(xml);
if (version == 0) {
@@ -394,7 +393,7 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
}
url += firstDefaultName;
- xml = fetchUrl(url, monitor.createSubMonitor(1), exception);
+ xml = fetchUrl(url, cache, monitor.createSubMonitor(1), exception);
usingAlternateUrl = true;
} else {
monitor.incProgress(1);
@@ -467,7 +466,8 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
}
url += firstDefaultName;
- xml = fetchUrl(url, subMonitor.createSubMonitor(1), null /* outException */);
+ xml = fetchUrl(url, cache, subMonitor.createSubMonitor(1),
+ null /* outException */);
subMonitor.incProgress(1);
// Loop to try the alternative document
if (xml != null) {
@@ -596,40 +596,12 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
* happens during the fetch.
* @see UrlOpener UrlOpener, which handles all URL logic.
*/
- private InputStream fetchUrl(String urlString, ITaskMonitor monitor, Exception[] outException) {
+ private InputStream fetchUrl(String urlString,
+ DownloadCache cache,
+ ITaskMonitor monitor,
+ Exception[] outException) {
try {
-
- InputStream is = null;
-
- int inc = 65536;
- int curr = 0;
- byte[] result = new byte[inc];
-
- try {
- is = UrlOpener.openUrl(urlString, monitor);
-
- int n;
- while ((n = is.read(result, curr, result.length - curr)) != -1) {
- curr += n;
- if (curr == result.length) {
- byte[] temp = new byte[curr + inc];
- System.arraycopy(result, 0, temp, 0, curr);
- result = temp;
- }
- }
-
- return new ByteArrayInputStream(result, 0, curr);
-
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- // pass
- }
- }
- }
-
+ return cache.openCachedUrl(urlString, monitor);
} catch (Exception e) {
if (outException != null) {
outException[0] = e;
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java
index 3114fc0..69d232a 100644
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java
@@ -16,9 +16,15 @@
package com.android.sdklib.internal.repository;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.util.Pair;
+
+import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
+import org.apache.http.ProtocolVersion;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
@@ -30,6 +36,7 @@ import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
+import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
@@ -46,9 +53,9 @@ import java.util.Map;
/**
* This class holds methods for adding URLs management.
- * @see #openUrl(String, ITaskMonitor)
+ * @see #openUrl(String, ITaskMonitor, Header[])
*/
-public class UrlOpener {
+class UrlOpener {
public static class CanceledByUserException extends Exception {
private static final long serialVersionUID = -7669346110926032403L;
@@ -79,35 +86,50 @@ public class UrlOpener {
* - {@code http://hc.apache.org/httpcomponents-client-ga/} <br/>
* - {@code http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html}
* <p/>
- * There's a very simple <b>cache</b> implementation.
+ * There's a very simple realm cache implementation.
* Login/Password for each realm are stored in a static {@link Map}.
- * Before asking the user the method verifies if the information is already available in cache.
+ * Before asking the user the method verifies if the information is already
+ * available in the memory cache.
*
* @param url the URL string to be opened.
* @param monitor {@link ITaskMonitor} which is related to this URL
* fetching.
- * @return Returns an {@link InputStream} holding the URL content.
+ * @param headers An optional array of HTTP headers to use in the GET request.
+ * @return Returns an {@link InputStream} holding the URL content and
+ * the HttpResponse (locale, headers and an status line).
+ * This never returns null; an exception is thrown instead in case of
+ * error or if the user canceled an authentication dialog.
* @throws IOException Exception thrown when there are problems retrieving
* the URL or its content.
* @throws CanceledByUserException Exception thrown if the user cancels the
* authentication dialog.
*/
- static InputStream openUrl(String url, ITaskMonitor monitor)
+ static @NonNull Pair<InputStream, HttpResponse> openUrl(
+ @NonNull String url,
+ @NonNull ITaskMonitor monitor,
+ @Nullable Header[] headers)
throws IOException, CanceledByUserException {
try {
- return openWithHttpClient(url, monitor);
+ return openWithHttpClient(url, monitor, headers);
} catch (ClientProtocolException e) {
// If the protocol is not supported by HttpClient (e.g. file:///),
// revert to the standard java.net.Url.open
URL u = new URL(url);
- return u.openStream();
+ InputStream is = u.openStream();
+ HttpResponse response = new BasicHttpResponse(
+ new ProtocolVersion(u.getProtocol(), 1, 0),
+ 200, "");
+ return Pair.of(is, response);
}
}
- private static InputStream openWithHttpClient(String url, ITaskMonitor monitor)
+ private static @NonNull Pair<InputStream, HttpResponse> openWithHttpClient(
+ @NonNull String url,
+ @NonNull ITaskMonitor monitor,
+ Header[] headers)
throws IOException, ClientProtocolException, CanceledByUserException {
UserCredentials result = null;
String realm = null;
@@ -117,7 +139,12 @@ public class UrlOpener {
// create local execution context
HttpContext localContext = new BasicHttpContext();
- HttpGet httpget = new HttpGet(url);
+ final HttpGet httpGet = new HttpGet(url);
+ if (headers != null) {
+ for (Header header : headers) {
+ httpGet.addHeader(header);
+ }
+ }
// retrieve local java configured network in case there is the need to
// authenticate a proxy
@@ -143,7 +170,7 @@ public class UrlOpener {
// loop while the response is being fetched
while (trying) {
// connect and get status code
- HttpResponse response = httpClient.execute(httpget, localContext);
+ HttpResponse response = httpClient.execute(httpGet, localContext);
int statusCode = response.getStatusLine().getStatusCode();
// check whether any authentication is required
@@ -158,7 +185,7 @@ public class UrlOpener {
authenticationState = (AuthState) localContext
.getAttribute(ClientContext.PROXY_AUTH_STATE);
}
- if (statusCode == HttpStatus.SC_OK) {
+ if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NOT_MODIFIED) {
// in case the status is OK and there is a realm and result,
// cache it
if (realm != null && result != null) {
@@ -226,8 +253,7 @@ public class UrlOpener {
// Note: don't use something like a BufferedHttpEntity since it would consume
// all content and store it in memory, resulting in an OutOfMemory exception
// on a large download.
-
- return new FilterInputStream(entity.getContent()) {
+ InputStream is = new FilterInputStream(entity.getContent()) {
@Override
public void close() throws IOException {
// Since Http Client is no longer needed, close it.
@@ -240,7 +266,20 @@ public class UrlOpener {
super.close();
}
};
+
+ HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine());
+ outResponse.setHeaders(response.getAllHeaders());
+ outResponse.setLocale(response.getLocale());
+
+ return Pair.of(is, outResponse);
}
+ } else if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
+ // It's ok to not have an entity (e.g. nothing to download) for a 304
+ HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine());
+ outResponse.setHeaders(response.getAllHeaders());
+ outResponse.setLocale(response.getLocale());
+
+ return Pair.of(null, outResponse);
}
}
diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/ArchiveInstallerTest.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/ArchiveInstallerTest.java
index 46390b5..8f42f8a 100755
--- a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/ArchiveInstallerTest.java
+++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/ArchiveInstallerTest.java
@@ -58,6 +58,7 @@ public class ArchiveInstallerTest extends TestCase {
protected File downloadFile(
Archive archive,
String osSdkRoot,
+ DownloadCache cache,
ITaskMonitor monitor,
boolean forceHttp) {
File file = mDownloadMap.get(archive);
@@ -102,7 +103,8 @@ public class ArchiveInstallerTest extends TestCase {
MockEmptyPackage p = new MockEmptyPackage("testPkg");
ArchiveReplacement ar = new ArchiveReplacement(p.getArchives()[0], null /*replaced*/);
- assertFalse(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan, mMon));
+ assertFalse(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan,
+ null /*UrlCache*/, mMon));
assertTrue(mMon.getCapturedLog().indexOf("Skipping already installed archive") != -1);
}
@@ -116,7 +118,8 @@ public class ArchiveInstallerTest extends TestCase {
mArchInst.setDownloadResponse(
p.getArchives()[0], createFile("/sdk", "tmp", "download1.zip"));
- assertTrue(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan, mMon));
+ assertTrue(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan,
+ null /*UrlCache*/, mMon));
// check what was created
assertEquals(
@@ -162,7 +165,8 @@ public class ArchiveInstallerTest extends TestCase {
mArchInst.setDownloadResponse(
newPkg.getArchives()[0], createFile("/sdk", "tmp", "download1.zip"));
- assertTrue(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan, mMon));
+ assertTrue(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan,
+ null /*UrlCache*/, mMon));
// check what was created
assertEquals(
@@ -237,7 +241,8 @@ public class ArchiveInstallerTest extends TestCase {
mArchInst.setDownloadResponse(
newPkg.getArchives()[0], createFile("/sdk", "tmp", "download1.zip"));
- assertTrue(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan, mMon));
+ assertTrue(mArchInst.install(ar, mSdkRoot, false /*forceHttp*/, mSdkMan,
+ null /*UrlCache*/, mMon));
// check what was created
assertEquals(
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java
deleted file mode 100755
index 8988bb5..0000000
--- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2011 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;
-
-
-
-/**
- * Interface for lifecycle events of pages.
- */
-public interface IPageListener {
-
- /**
- * The page was just selected and brought to front.
- */
- public void onPageSelected();
-}
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IUpdaterData.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IUpdaterData.java
index 8ec6596..26c52d5 100755
--- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IUpdaterData.java
+++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IUpdaterData.java
@@ -20,6 +20,7 @@ import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.repository.ITaskFactory;
+import com.android.sdklib.internal.repository.DownloadCache;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
import org.eclipse.swt.widgets.Shell;
@@ -35,6 +36,8 @@ interface IUpdaterData {
public abstract ISdkLog getSdkLog();
+ public abstract DownloadCache getDownloadCache();
+
public abstract ImageFactory getImageFactory();
public abstract SdkManager getSdkManager();
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterLogic.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterLogic.java
index 4f35b26..0d9b10a 100755
--- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterLogic.java
+++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterLogic.java
@@ -1276,7 +1276,7 @@ class SdkUpdaterLogic {
Package[] pkgs = remoteSrc.getPackages();
if (pkgs == null) {
- remoteSrc.load(monitor, forceHttp);
+ remoteSrc.load(mUpdaterData.getDownloadCache(), monitor, forceHttp);
pkgs = remoteSrc.getPackages();
}
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
index dd51a59..0def6fa 100755
--- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
+++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
@@ -29,6 +29,7 @@ import com.android.sdklib.internal.repository.AddonsListFetcher;
import com.android.sdklib.internal.repository.AddonsListFetcher.Site;
import com.android.sdklib.internal.repository.Archive;
import com.android.sdklib.internal.repository.ArchiveInstaller;
+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;
@@ -83,18 +84,13 @@ public class UpdaterData implements IUpdaterData {
private SdkManager mSdkManager;
private AvdManager mAvdManager;
-
+ private DownloadCache mDownloadCache; // lazily created in getDownloadCache
private final LocalSdkParser mLocalSdkParser = new LocalSdkParser();
private final SdkSources mSources = new SdkSources();
-
private ImageFactory mImageFactory;
-
private final SettingsController mSettingsController;
-
private final ArrayList<ISdkChangeListener> mListeners = new ArrayList<ISdkChangeListener>();
-
private Shell mWindowShell;
-
private AndroidLocationException mAvdManagerInitError;
/**
@@ -115,6 +111,7 @@ public class UpdaterData implements IUpdaterData {
mOsSdkRoot = osSdkRoot;
mSdkLog = sdkLog;
+ mDownloadCache = getDownloadCache();
mSettingsController = new SettingsController(this);
initSdk();
@@ -126,6 +123,14 @@ public class UpdaterData implements IUpdaterData {
return mOsSdkRoot;
}
+ @Override
+ public DownloadCache getDownloadCache() {
+ if (mDownloadCache == null) {
+ mDownloadCache = new DownloadCache(DownloadCache.Strategy.FRESH_CACHE);
+ }
+ return mDownloadCache;
+ }
+
public void setTaskFactory(ITaskFactory taskFactory) {
mTaskFactory = taskFactory;
}
@@ -441,6 +446,7 @@ public class UpdaterData implements IUpdaterData {
mOsSdkRoot,
forceHttp,
mSdkManager,
+ mDownloadCache,
monitor)) {
// We installed this archive.
newlyInstalledArchives.add(archive);
@@ -1003,7 +1009,7 @@ public class UpdaterData implements IUpdaterData {
if (forceFetching ||
source.getPackages() != null ||
source.getFetchError() != null) {
- source.load(monitor.createSubMonitor(1), forceHttp);
+ source.load(mDownloadCache, monitor.createSubMonitor(1), forceHttp);
}
monitor.incProgress(1);
}
@@ -1053,7 +1059,7 @@ public class UpdaterData implements IUpdaterData {
boolean fetch3rdParties = System.getenv("SDK_SKIP_3RD_PARTIES") == null;
AddonsListFetcher fetcher = new AddonsListFetcher();
- Site[] sites = fetcher.fetch(monitor, url);
+ Site[] sites = fetcher.fetch(url, mDownloadCache, monitor);
if (sites != null) {
mSources.removeAll(SdkSourceCategory.ADDONS_3RD_PARTY);
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackageLoader.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackageLoader.java
index af7ce2c..ad9a2a2 100755
--- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackageLoader.java
+++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackageLoader.java
@@ -17,6 +17,7 @@
package com.android.sdkuilib.internal.repository.sdkman2;
import com.android.sdklib.internal.repository.Archive;
+import com.android.sdklib.internal.repository.DownloadCache;
import com.android.sdklib.internal.repository.ITask;
import com.android.sdklib.internal.repository.ITaskMonitor;
import com.android.sdklib.internal.repository.NullTaskMonitor;
@@ -44,7 +45,7 @@ class PackageLoader {
/**
* Interface for the callback called by
- * {@link PackageLoader#loadPackages(ISourceLoadedCallback)}.
+ * {@link PackageLoader#loadPackages(DownloadCache, ISourceLoadedCallback)}.
* <p/>
* After processing each source, the package loader calls {@link #onUpdateSource}
* with the list of packages found in that source.
@@ -144,12 +145,20 @@ class PackageLoader {
* after each source is finished loaded. In return the callback tells the loader
* whether to continue loading sources.
*/
- public void loadPackages(final ISourceLoadedCallback sourceLoadedCallback) {
+ public void loadPackages(
+ DownloadCache downloadCache,
+ final ISourceLoadedCallback sourceLoadedCallback) {
try {
if (mUpdaterData == null) {
return;
}
+ if (downloadCache == null) {
+ downloadCache = mUpdaterData.getDownloadCache();
+ }
+
+ final DownloadCache downloadCache2 = downloadCache;
+
mUpdaterData.getTaskFactory().start("Loading Sources", new ITask() {
@Override
public void run(ITaskMonitor monitor) {
@@ -177,7 +186,9 @@ class PackageLoader {
for (SdkSource source : sources) {
Package[] pkgs = source.getPackages();
if (pkgs == null) {
- source.load(subMonitor.createSubMonitor(1), forceHttp);
+ source.load(downloadCache2,
+ subMonitor.createSubMonitor(1),
+ forceHttp);
pkgs = source.getPackages();
}
if (pkgs == null) {
@@ -204,7 +215,8 @@ class PackageLoader {
}
/**
- * Load packages, source by source using {@link #loadPackages(ISourceLoadedCallback)},
+ * Load packages, source by source using
+ * {@link #loadPackages(DownloadCache, ISourceLoadedCallback)},
* and executes the given {@link IAutoInstallTask} on the current package list.
* That is for each package known, the install task is queried to find if
* the package is the one to be installed or updated.
@@ -236,7 +248,8 @@ class PackageLoader {
final int installFlags,
final IAutoInstallTask installTask) {
- loadPackages(new ISourceLoadedCallback() {
+ loadPackages(mUpdaterData.getDownloadCache(),
+ new ISourceLoadedCallback() {
List<Archive> mArchivesToInstall = new ArrayList<Archive>();
Map<Package, File> mInstallPaths = new HashMap<Package, File>();
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesPage.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesPage.java
index 99801b0..e0d97f9 100755
--- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesPage.java
+++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesPage.java
@@ -24,7 +24,6 @@ import com.android.sdklib.internal.repository.ITask;
import com.android.sdklib.internal.repository.ITaskMonitor;
import com.android.sdklib.internal.repository.Package;
import com.android.sdklib.internal.repository.SdkSource;
-import com.android.sdkuilib.internal.repository.IPageListener;
import com.android.sdkuilib.internal.repository.UpdaterData;
import com.android.sdkuilib.internal.repository.UpdaterPage;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
@@ -86,8 +85,7 @@ import java.util.Map.Entry;
* remote available packages. This gives an overview of what is installed
* vs what is available and allows the user to update or install packages.
*/
-public class PackagesPage extends UpdaterPage
- implements ISdkChangeListener, IPageListener {
+public class PackagesPage extends UpdaterPage implements ISdkChangeListener {
static final String ICON_CAT_OTHER = "pkgcat_other_16.png"; //$NON-NLS-1$
static final String ICON_CAT_PLATFORM = "pkgcat_16.png"; //$NON-NLS-1$
@@ -169,13 +167,9 @@ public class PackagesPage extends UpdaterPage
postCreate(); //$hide$
}
- @Override
- public void onPageSelected() {
- List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
- if (cats == null || cats.isEmpty()) {
- // Initialize the package list the first time the page is shown.
- loadPackages();
- }
+ public void performFirstLoad() {
+ // Initialize the package list the first time the page is shown.
+ loadPackages(true /*isFirstLoad*/);
}
@SuppressWarnings("unused")
@@ -583,6 +577,10 @@ public class PackagesPage extends UpdaterPage
}
private void loadPackages() {
+ loadPackages(false /*isFirstLoad*/);
+ }
+
+ private void loadPackages(final boolean isFirstLoad) {
if (mUpdaterData == null) {
return;
}
@@ -601,7 +599,9 @@ public class PackagesPage extends UpdaterPage
}
mDiffLogic.updateStart();
- mDiffLogic.getPackageLoader().loadPackages(new ISourceLoadedCallback() {
+ mDiffLogic.getPackageLoader().loadPackages(
+ mUpdaterData.getDownloadCache(), // TODO do a first pass with Cache=SERVE_CACHE
+ new ISourceLoadedCallback() {
@Override
public boolean onUpdateSource(SdkSource source, Package[] newPackages) {
// This runs in a thread and must not access UI directly.
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/SdkUpdaterWindowImpl2.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/SdkUpdaterWindowImpl2.java
index 2f77e45..ab0934a 100755
--- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/SdkUpdaterWindowImpl2.java
+++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/SdkUpdaterWindowImpl2.java
@@ -484,7 +484,7 @@ public class SdkUpdaterWindowImpl2 implements ISdkUpdaterWindow {
mUpdaterData.broadcastOnSdkLoaded();
// Tell the one page its the selected one
- mPkgPage.onPageSelected();
+ mPkgPage.performFirstLoad();
return true;
}
diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java
index fff0814..e691429 100755
--- a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java
+++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java
@@ -20,6 +20,7 @@ import com.android.sdklib.NullSdkLog;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.repository.ArchiveInstaller;
import com.android.sdklib.internal.repository.ArchiveReplacement;
+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;
@@ -30,6 +31,7 @@ import com.android.sdkuilib.internal.repository.icons.ImageFactory;
import org.eclipse.swt.graphics.Image;
+import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -40,6 +42,8 @@ public class MockUpdaterData extends UpdaterData {
private final List<ArchiveReplacement> mInstalled = new ArrayList<ArchiveReplacement>();
+ private DownloadCache mMockDownloadCache;
+
public MockUpdaterData() {
super(SDK_PATH, new MockLog());
@@ -76,6 +80,7 @@ public class MockUpdaterData extends UpdaterData {
String osSdkRoot,
boolean forceHttp,
SdkManager sdkManager,
+ DownloadCache cache,
ITaskMonitor monitor) {
mInstalled.add(archiveInfo);
return true;
@@ -83,6 +88,25 @@ public class MockUpdaterData extends UpdaterData {
};
}
+ /**
+ * Lazily initializes and returns a mock download cache that doesn't use the
+ * local disk and doesn't cache anything.
+ */
+ @Override
+ public DownloadCache getDownloadCache() {
+ if (mMockDownloadCache == null) {
+ mMockDownloadCache = new DownloadCache(DownloadCache.Strategy.DIRECT) {
+ @Override
+ protected File initCacheRoot() {
+ // returns null, preventing the cache from using the default
+ // $HOME/.android folder; this effectively disables the cache.
+ return null;
+ }
+ };
+ }
+ return mMockDownloadCache;
+ }
+
//------------
private class MockTaskFactory implements ITaskFactory {
@@ -91,6 +115,7 @@ public class MockUpdaterData extends UpdaterData {
start(title, null /*parentMonitor*/, task);
}
+ @SuppressWarnings("unused") // works by side-effect of creating a new MockTask.
@Override
public void start(String title, ITaskMonitor parentMonitor, ITask task) {
new MockTask(task);
diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java
index 5d735e3..245ca84 100755
--- a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java
+++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java
@@ -29,6 +29,7 @@ import com.android.sdklib.internal.repository.MockToolPackage;
import com.android.sdklib.internal.repository.Package;
import com.android.sdklib.internal.repository.SdkSource;
import com.android.sdklib.internal.repository.SdkSources;
+import com.android.sdklib.internal.repository.DownloadCache;
import com.android.sdkuilib.internal.repository.icons.ImageFactory;
import org.eclipse.swt.widgets.Shell;
@@ -60,6 +61,11 @@ public class UpdaterLogicTest extends TestCase {
}
@Override
+ public DownloadCache getDownloadCache() {
+ return null;
+ }
+
+ @Override
public SdkManager getSdkManager() {
return null;
}