package org.fdroid.fdroid.updater; import android.content.ContentValues; import android.content.Context; import android.os.Bundle; import android.util.Log; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.RepoXMLHandler; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; abstract public class RepoUpdater { public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml"; public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress"; public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) { if (repo.fingerprint == null && repo.pubkey == null) { return new UnsignedRepoUpdater(ctx, repo); } else { return new SignedRepoUpdater(ctx, repo); } } protected final Context context; protected final Repo repo; private List<App> apps = new ArrayList<App>(); private List<Apk> apks = new ArrayList<Apk>(); private RepoUpdateRememberer rememberer = null; protected boolean usePubkeyInJar = false; protected boolean hasChanged = false; protected ProgressListener progressListener; public RepoUpdater(Context ctx, Repo repo) { this.context = ctx; this.repo = repo; } public void setProgressListener(ProgressListener progressListener) { this.progressListener = progressListener; } public boolean hasChanged() { return hasChanged; } public List<App> getApps() { return apps; } public List<Apk> getApks() { return apks; } /** * For example, you may want to unzip a jar file to get the index inside, * or if the file is not compressed, you can just return a reference to * the downloaded file. * * @throws UpdateException All error states will come from here. */ protected abstract File getIndexFromFile(File downloadedFile) throws UpdateException; protected abstract String getIndexAddress(); protected Downloader downloadIndex() throws UpdateException { Downloader downloader = null; try { downloader = DownloaderFactory.create(getIndexAddress(), context); downloader.setCacheTag(repo.lastetag); if (progressListener != null) { // interactive session, show progress Bundle data = new Bundle(1); data.putString(PROGRESS_DATA_REPO_ADDRESS, repo.address); downloader.setProgressListener(progressListener, data); } downloader.downloadUninterrupted(); if (downloader.isCached()) { // The index is unchanged since we last read it. We just mark // everything that came from this repo as being updated. Log.d("FDroid", "Repo index for " + getIndexAddress() + " is up to date (by etag)"); } } catch (IOException e) { if (downloader != null && downloader.getFile() != null) { downloader.getFile().delete(); } throw new UpdateException( repo, "Error getting index file from " + repo.address, e); } return downloader; } private int estimateAppCount(File indexFile) { int count = -1; try { // A bit of a hack, this might return false positives if an apps description // or some other part of the XML file contains this, but it is a pretty good // estimate and makes the progress counter more informative. // As with asking the server about the size of the index before downloading, // this also has a time tradeoff. It takes about three seconds to iterate // through the file and count 600 apps on a slow emulator (v17), but if it is // taking two minutes to update, the three second wait may be worth it. final String APPLICATION = "<application"; count = Utils.countSubstringOccurrence(indexFile, APPLICATION); } catch (IOException e) { // Do nothing. Leave count at default -1 value. } return count; } public void update() throws UpdateException { File downloadedFile = null; File indexFile = null; try { Downloader downloader = downloadIndex(); hasChanged = downloader.hasChanged(); if (hasChanged) { downloadedFile = downloader.getFile(); indexFile = getIndexFromFile(downloadedFile); // Process the index... SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); XMLReader reader = parser.getXMLReader(); RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener); if (progressListener != null) { // Only bother spending the time to count the expected apps // if we can show that to the user... handler.setTotalAppCount(estimateAppCount(indexFile)); } reader.setContentHandler(handler); InputSource is = new InputSource( new BufferedReader(new FileReader(indexFile))); reader.parse(is); apps = handler.getApps(); apks = handler.getApks(); rememberer = new RepoUpdateRememberer(); rememberer.context = context; rememberer.repo = repo; rememberer.values = prepareRepoDetailsForSaving(handler, downloader.getCacheTag()); } } catch (SAXException e) { throw new UpdateException( repo, "Error parsing index for repo " + repo.address, e); } catch (FileNotFoundException e) { throw new UpdateException( repo, "Error parsing index for repo " + repo.address, e); } catch (ParserConfigurationException e) { throw new UpdateException( repo, "Error parsing index for repo " + repo.address, e); } catch (IOException e) { throw new UpdateException( repo, "Error parsing index for repo " + repo.address, e); } finally { if (downloadedFile != null && downloadedFile != indexFile && downloadedFile.exists()) { downloadedFile.delete(); } if (indexFile != null && indexFile.exists()) { indexFile.delete(); } } } private ContentValues prepareRepoDetailsForSaving (RepoXMLHandler handler, String etag) { ContentValues values = new ContentValues(); values.put(RepoProvider.DataColumns.LAST_UPDATED, Utils.DATE_FORMAT.format(new Date())); if (repo.lastetag == null || !repo.lastetag.equals(etag)) { values.put(RepoProvider.DataColumns.LAST_ETAG, etag); } /* * We read an unsigned index that indicates that a signed version * is available. Or we received a repo config that included the * fingerprint, so we need to save the pubkey now. */ if (handler.getPubKey() != null && (repo.pubkey == null || usePubkeyInJar)) { // TODO: Spend the time *now* going to get the etag of the signed // repo, so that we can prevent downloading it next time. Otherwise // next time we update, we have to download the signed index // in its entirety, regardless of if it contains the same // information as the unsigned one does not... Log.d("FDroid", "Public key found - switching to signed repo for future updates"); values.put(RepoProvider.DataColumns.PUBLIC_KEY, handler.getPubKey()); usePubkeyInJar = false; } if (handler.getVersion() != -1 && handler.getVersion() != repo.version) { Log.d("FDroid", "Repo specified a new version: from " + repo.version + " to " + handler.getVersion()); values.put(RepoProvider.DataColumns.VERSION, handler.getVersion()); } if (handler.getMaxAge() != -1 && handler.getMaxAge() != repo.maxage) { Log.d("FDroid", "Repo specified a new maximum age - updated"); values.put(RepoProvider.DataColumns.MAX_AGE, handler.getMaxAge()); } if (handler.getDescription() != null && !handler.getDescription().equals(repo.description)) { values.put(RepoProvider.DataColumns.DESCRIPTION, handler.getDescription()); } if (handler.getName() != null && !handler.getName().equals(repo.name)) { values.put(RepoProvider.DataColumns.NAME, handler.getName()); } return values; } public RepoUpdateRememberer getRememberer() { return rememberer; } public static class RepoUpdateRememberer { private Context context; private Repo repo; private ContentValues values; public void rememberUpdate() { RepoProvider.Helper.update(context, repo, values); } } public static class UpdateException extends Exception { private static final long serialVersionUID = -4492452418826132803L; public final Repo repo; public UpdateException(Repo repo, String message) { super(message); this.repo = repo; } public UpdateException(Repo repo, String message, Exception cause) { super(message, cause); this.repo = repo; } } }