From af832e8eeb4eb7e24a292ed912ce8eb7cc2a8233 Mon Sep 17 00:00:00 2001 From: Raphael Moll Date: Thu, 12 Apr 2012 14:41:25 -0700 Subject: 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 --- sdkmanager/libs/sdklib/.classpath | 8 +- .../src/com/android/sdklib/SdkConstants.java | 3 + .../internal/repository/AddonsListFetcher.java | 45 +- .../internal/repository/ArchiveInstaller.java | 9 +- .../sdklib/internal/repository/DownloadCache.java | 667 +++++++++++++++++++++ .../sdklib/internal/repository/SdkSource.java | 52 +- .../sdklib/internal/repository/UrlOpener.java | 67 ++- .../internal/repository/ArchiveInstallerTest.java | 13 +- .../internal/repository/IPageListener.java | 30 - .../sdkuilib/internal/repository/IUpdaterData.java | 3 + .../internal/repository/SdkUpdaterLogic.java | 2 +- .../sdkuilib/internal/repository/UpdaterData.java | 22 +- .../internal/repository/sdkman2/PackageLoader.java | 23 +- .../internal/repository/sdkman2/PackagesPage.java | 22 +- .../repository/sdkman2/SdkUpdaterWindowImpl2.java | 2 +- .../internal/repository/MockUpdaterData.java | 25 + .../internal/repository/UpdaterLogicTest.java | 6 + 17 files changed, 841 insertions(+), 158 deletions(-) create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DownloadCache.java delete mode 100755 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java (limited to 'sdkmanager') 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 @@ - + - - - + + + 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * Default is 10 minutes. + *

+ * 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. + *

+ * 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. + *

+ * Default is 4 hours. + *

+ * 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. + *

+ * 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 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. + *

+ * For large downloads (e.g. installable archives) please do not invoke the + * cache and instead use the {@link #openDirectUrl(String, ITaskMonitor)} + * method. + *

+ * 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

headers = new ArrayList
(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. + *

+ * 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 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 { /** * 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 { * 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 { 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 { 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 { } 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 { } 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 { * 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/}
* - {@code http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html} *

- * There's a very simple cache 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 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 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 mListeners = new ArrayList(); - 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)}. *

* 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 mArchivesToInstall = new ArrayList(); Map mInstallPaths = new HashMap(); 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 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 mInstalled = new ArrayList(); + 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; } -- cgit v1.1