diff --git a/app/src/androidTest/java/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/app/src/androidTest/java/org/fdroid/fdroid/MultiRepoUpdaterTest.java index ad72da4df..9fc9c9443 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/MultiRepoUpdaterTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/MultiRepoUpdaterTest.java @@ -30,7 +30,7 @@ import java.util.UUID; @SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics public class MultiRepoUpdaterTest extends InstrumentationTestCase { - private static final String TAG = "RepoUpdaterTest"; + private static final String TAG = "MultiRepoUpdaterTest"; private static final String REPO_MAIN = "Test F-Droid repo"; private static final String REPO_ARCHIVE = "Test F-Droid repo (Archive)"; @@ -406,7 +406,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { private RepoUpdater createUpdater(String name, Context context) { Repo repo = new Repo(); repo.signingCertificate = PUB_KEY; - repo.address = UUID.randomUUID().toString(); + repo.address = "https://fake.url/" + UUID.randomUUID().toString() + "/fdroid/repo"; repo.name = name; ContentValues values = new ContentValues(2); diff --git a/app/src/androidTest/java/org/fdroid/fdroid/RepoUpdaterTest.java b/app/src/androidTest/java/org/fdroid/fdroid/RepoUpdaterTest.java index c706265e9..9566bd54d 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/RepoUpdaterTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/RepoUpdaterTest.java @@ -31,6 +31,7 @@ public class RepoUpdaterTest { context = instrumentation.getContext(); testFilesDir = TestUtils.getWriteableDir(instrumentation); Repo repo = new Repo(); + repo.address = "https://fake.url/fdroid/repo"; repo.signingCertificate = this.simpleIndexSigningCert; repoUpdater = new RepoUpdater(context, repo); } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ced498306..7b4b8cdd5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -447,6 +447,9 @@ android:exported="false" /> + diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 88d17a270..5bdb405ef 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -37,7 +37,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.ListFragment; import android.support.v4.app.NavUtils; @@ -86,8 +85,8 @@ import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.InstalledAppProvider; -import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer.InstallFailedException; import org.fdroid.fdroid.installer.Installer.InstallerCallback; @@ -111,6 +110,15 @@ public class AppDetails extends AppCompatActivity { private FDroidApp fdroidApp; private ApkListAdapter adapter; + /** + * Check if {@code packageName} is currently visible to the user. + */ + public static boolean isAppVisible(String packageName) { + return packageName != null && packageName.equals(visiblePackageName); + } + + private static String visiblePackageName; + private static class ViewHolder { TextView version; TextView status; @@ -426,13 +434,19 @@ public class AppDetails extends AppCompatActivity { @Override protected void onResumeFragments() { + // Must be called before super.onResumeFragments(), as the fragments depend on the active + // url being correctly set in order to know whether or not to show the download progress bar. + calcActiveDownloadUrlString(app.packageName); + super.onResumeFragments(); + headerFragment = (AppDetailsHeaderFragment) getSupportFragmentManager().findFragmentById(R.id.header); refreshApkList(); supportInvalidateOptionsMenu(); if (DownloaderService.isQueuedOrActive(activeDownloadUrlString)) { registerDownloaderReceivers(); } + visiblePackageName = app.packageName; } /** @@ -454,6 +468,7 @@ public class AppDetails extends AppCompatActivity { @Override protected void onPause() { super.onPause(); + visiblePackageName = null; // save the active URL for this app in case we come back PreferencesCompat.apply(getPreferences(MODE_PRIVATE) .edit() @@ -582,13 +597,7 @@ public class AppDetails extends AppCompatActivity { Utils.debugLog(TAG, "Getting application details for " + packageName); App newApp = null; - String urlString = getPreferences(MODE_PRIVATE).getString(packageName, null); - if (DownloaderService.isQueuedOrActive(urlString)) { - activeDownloadUrlString = urlString; - } else { - // this URL is no longer active, remove it - PreferencesCompat.apply(getPreferences(MODE_PRIVATE).edit().remove(packageName)); - } + calcActiveDownloadUrlString(packageName); if (!TextUtils.isEmpty(packageName)) { newApp = AppProvider.Helper.findByPackageName(getContentResolver(), packageName); @@ -599,6 +608,16 @@ public class AppDetails extends AppCompatActivity { return this.app != null; } + private void calcActiveDownloadUrlString(String packageName) { + String urlString = getPreferences(MODE_PRIVATE).getString(packageName, null); + if (DownloaderService.isQueuedOrActive(urlString)) { + activeDownloadUrlString = urlString; + } else { + // this URL is no longer active, remove it + PreferencesCompat.apply(getPreferences(MODE_PRIVATE).edit().remove(packageName)); + } + } + /** * If passed null, this will show a message to the user ("Could not find app ..." or something * like that) and then finish the activity. @@ -815,9 +834,6 @@ public class AppDetails extends AppCompatActivity { return; } - final String repoaddress = getRepoAddress(apk); - if (repoaddress == null) return; - if (!apk.compatible) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.installIncompatible); @@ -826,7 +842,7 @@ public class AppDetails extends AppCompatActivity { @Override public void onClick(DialogInterface dialog, int whichButton) { - startDownload(apk, repoaddress); + startDownload(apk); } }); builder.setNegativeButton(R.string.no, @@ -855,24 +871,14 @@ public class AppDetails extends AppCompatActivity { alert.show(); return; } - startDownload(apk, repoaddress); + startDownload(apk); } - @Nullable - private String getRepoAddress(Apk apk) { - final String[] projection = {RepoProvider.DataColumns.ADDRESS}; - Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); - if (repo == null || repo.address == null) { - return null; - } - return repo.address; - } - - private void startDownload(Apk apk, String repoAddress) { - activeDownloadUrlString = Utils.getApkUrl(repoAddress, apk); + private void startDownload(Apk apk) { + activeDownloadUrlString = apk.getUrl(); registerDownloaderReceivers(); headerFragment.startProgress(); - DownloaderService.queue(this, apk.packageName, activeDownloadUrlString); + InstallManagerService.queue(this, app, apk); } private void removeApk(String packageName) { diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index 463af770d..2feb9d73d 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -59,10 +59,13 @@ public class CleanCacheService extends IntentService { * Delete index files which were downloaded, but not removed (e.g. due to F-Droid being * force closed during processing of the file, before getting a chance to delete). This * may include both "index-*-downloaded" and "index-*-extracted.xml" files. - *

+ *

* Note that if the SD card is not ready, then the cache directory will probably not be * available. In this situation no files will be deleted (and thus they may still exist * after the SD card becomes available). + *

+ * This also deletes temp files that are created by + * {@link org.fdroid.fdroid.net.DownloaderFactory#create(Context, String)}, e.g. "dl-*" */ private void deleteStrayIndexFiles() { File cacheDir = getCacheDir(); @@ -79,6 +82,9 @@ public class CleanCacheService extends IntentService { if (f.getName().startsWith("index-")) { FileUtils.deleteQuietly(f); } + if (f.getName().startsWith("dl-")) { + FileUtils.deleteQuietly(f); + } } } } diff --git a/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java b/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java index ee5d5f0aa..31d13395a 100644 --- a/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java +++ b/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java @@ -1,17 +1,14 @@ package org.fdroid.fdroid; -import android.os.Bundle; - -import org.fdroid.fdroid.data.Repo; - import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URL; public class ProgressBufferedInputStream extends BufferedInputStream { private final ProgressListener progressListener; - private final Bundle data; + private final URL sourceUrl; private final int totalBytes; private int currentBytes; @@ -20,12 +17,10 @@ public class ProgressBufferedInputStream extends BufferedInputStream { * Reports progress to the specified {@link ProgressListener}, with the * progress based on the {@code totalBytes}. */ - public ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, Repo repo, int totalBytes) - throws IOException { + public ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, URL sourceUrl, int totalBytes) { super(in); this.progressListener = progressListener; - this.data = new Bundle(1); - this.data.putString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS, repo.address); + this.sourceUrl = sourceUrl; this.totalBytes = totalBytes; } @@ -37,10 +32,7 @@ public class ProgressBufferedInputStream extends BufferedInputStream { * the digits changing because it looks pretty, < 9000 since the reads won't * line up exactly */ if (currentBytes % 333333 < 9000) { - progressListener.onProgress( - new ProgressListener.Event( - RepoUpdater.PROGRESS_TYPE_PROCESS_XML, - currentBytes, totalBytes, data)); + progressListener.onProgress(sourceUrl, currentBytes, totalBytes); } } return super.read(buffer, byteOffset, byteCount); diff --git a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java index b2f4f252b..915b64ee6 100644 --- a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java +++ b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java @@ -1,76 +1,15 @@ package org.fdroid.fdroid; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; +import java.net.URL; +/** + * This is meant only to send download progress for any URL (e.g. index + * updates, APKs, etc). This also keeps this class pure Java so that classes + * that use {@code ProgressListener} can be tested on the JVM, without requiring + * an Android device or emulator. + */ public interface ProgressListener { - void onProgress(Event event); - - // I went a bit overboard with the overloaded constructors, but they all - // seemed potentially useful and unambiguous, so I just put them in there - // while I'm here. - class Event implements Parcelable { - - public static final int NO_VALUE = Integer.MIN_VALUE; - - public final String type; - public final Bundle data; - - // These two are not final, so that you can create a template Event, - // pass it into a function which performs something over time, and - // that function can initialize "total" and progressively - // update "progress" - public int progress; - public final int total; - - public Event(String type) { - this(type, NO_VALUE, NO_VALUE, null); - } - - public Event(String type, int progress, int total, Bundle data) { - this.type = type; - this.progress = progress; - this.total = total; - this.data = (data == null) ? new Bundle() : data; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(type); - dest.writeInt(progress); - dest.writeInt(total); - dest.writeBundle(data); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public Event createFromParcel(Parcel in) { - return new Event(in.readString(), in.readInt(), in.readInt(), in.readBundle()); - } - - @Override - public Event[] newArray(int size) { - return new Event[size]; - } - }; - - /** - * Can help to provide context to the listener about what process is causing the event. - * For example, the repo updater uses one listener to listen to multiple downloaders. - * When it receives an event, it doesn't know which repo download is causing the event, - * so we pass that through to the downloader when we set the progress listener. This way, - * we can ask the event for the name of the repo. - */ - public Bundle getData() { - return data; - } - } + void onProgress(URL sourceUrl, int bytesRead, int totalBytes); } diff --git a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java index 4dc986e05..e720a6031 100644 --- a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java +++ b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java @@ -21,6 +21,8 @@ import org.xml.sax.XMLReader; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.security.CodeSigner; import java.security.cert.Certificate; import java.security.cert.X509Certificate; @@ -48,10 +50,6 @@ public class RepoUpdater { private static final String TAG = "RepoUpdater"; - public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml"; - public static final String PROGRESS_COMMITTING = "committing"; - public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress"; - public final String indexUrl; @NonNull @@ -60,7 +58,8 @@ public class RepoUpdater { private final Repo repo; private boolean hasChanged; @Nullable - private ProgressListener progressListener; + private ProgressListener committingProgressListener; + private ProgressListener processXmlProgressListener; private String cacheTag; private X509Certificate signingCertFromJar; @@ -84,8 +83,12 @@ public class RepoUpdater { this.indexUrl = url; } - public void setProgressListener(@Nullable ProgressListener progressListener) { - this.progressListener = progressListener; + public void setProcessXmlProgressListener(ProgressListener progressListener) { + this.processXmlProgressListener = progressListener; + } + + public void setCommittingProgressListener(ProgressListener progressListener) { + this.committingProgressListener = progressListener; } public boolean hasChanged() { @@ -177,7 +180,7 @@ public class RepoUpdater { JarFile jarFile = new JarFile(downloadedFile, true); JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml"); indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), - progressListener, repo, (int) indexEntry.getSize()); + processXmlProgressListener, new URL(repo.address), (int) indexEntry.getSize()); // Process the index... final SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); @@ -207,8 +210,13 @@ public class RepoUpdater { private void commitToDb() throws UpdateException { Log.i(TAG, "Repo signature verified, saving app metadata to database."); - if (progressListener != null) { - progressListener.onProgress(new ProgressListener.Event(PROGRESS_COMMITTING)); + if (committingProgressListener != null) { + try { + //TODO this should be an event, not a progress listener + committingProgressListener.onProgress(new URL(indexUrl), 0, -1); + } catch (MalformedURLException e) { + e.printStackTrace(); + } } persister.commit(repoDetailsToSave); } diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index c3cadf50b..8dc291305 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -48,13 +48,15 @@ import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; +import java.net.URL; import java.util.ArrayList; import java.util.List; -public class UpdateService extends IntentService implements ProgressListener { +public class UpdateService extends IntentService { private static final String TAG = "UpdateService"; @@ -375,7 +377,8 @@ public class UpdateService extends IntentService implements ProgressListener { RepoUpdater updater = new RepoUpdater(getBaseContext(), repo); localBroadcastManager.registerReceiver(downloadProgressReceiver, DownloaderService.getIntentFilter(updater.indexUrl, Downloader.ACTION_PROGRESS)); - updater.setProgressListener(this); + updater.setProcessXmlProgressListener(processXmlProgressListener); + updater.setCommittingProgressListener(committingProgressListener); try { updater.update(); if (updater.hasChanged()) { @@ -393,7 +396,7 @@ public class UpdateService extends IntentService implements ProgressListener { // now that downloading the index is done, start downloading updates if (changes && fdroidPrefs.isAutoDownloadEnabled()) { - autoDownloadUpdates(repo.address); + autoDownloadUpdates(); } } @@ -485,7 +488,7 @@ public class UpdateService extends IntentService implements ProgressListener { return inboxStyle; } - private void autoDownloadUpdates(String repoAddress) { + private void autoDownloadUpdates() { Cursor cursor = getContentResolver().query( AppProvider.getCanUpdateUri(), new String[]{ @@ -496,11 +499,8 @@ public class UpdateService extends IntentService implements ProgressListener { cursor.moveToFirst(); for (int i = 0; i < cursor.getCount(); i++) { App app = new App(cursor); - Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode, new String[]{ - ApkProvider.DataColumns.NAME, - }); - String urlString = Utils.getApkUrl(repoAddress, apk); - DownloaderService.queue(this, app.packageName, urlString); + Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode); + InstallManagerService.queue(this, app, apk); cursor.moveToNext(); } cursor.close(); @@ -528,25 +528,25 @@ public class UpdateService extends IntentService implements ProgressListener { notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build()); } - /** - * Received progress event from the RepoXMLHandler. It could be progress - * downloading from the repo, or perhaps processing the info from the repo. - */ - @Override - public void onProgress(ProgressListener.Event event) { - String message = ""; - String repoAddress = event.getData().getString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS); - String downloadedSize = Utils.getFriendlySize(event.progress); - String totalSize = Utils.getFriendlySize(event.total); - int percent = event.total > 0 ? (int) ((double) event.progress / event.total * 100) : -1; - switch (event.type) { - case RepoUpdater.PROGRESS_TYPE_PROCESS_XML: - message = getString(R.string.status_processing_xml_percent, repoAddress, downloadedSize, totalSize, percent); - break; - case RepoUpdater.PROGRESS_COMMITTING: - message = getString(R.string.status_inserting_apps); - break; + private final ProgressListener processXmlProgressListener = new ProgressListener() { + @Override + public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { + String downloadedSize = Utils.getFriendlySize(bytesRead); + String totalSize = Utils.getFriendlySize(totalBytes); + int percent = -1; + if (totalBytes > 0) { + percent = (int) ((double) bytesRead / totalBytes * 100); + } + String message = getString(R.string.status_processing_xml_percent, sourceUrl, downloadedSize, totalSize, percent); + sendStatus(getApplicationContext(), STATUS_INFO, message, percent); } - sendStatus(this, STATUS_INFO, message, percent); - } + }; + + private final ProgressListener committingProgressListener = new ProgressListener() { + @Override + public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { + String message = getString(R.string.status_inserting_apps); + sendStatus(getApplicationContext(), STATUS_INFO, message); + } + }; } diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index d0dfa9b1a..733280f52 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -39,7 +39,6 @@ import com.nostra13.universalimageloader.utils.StorageUtils; import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.compat.FileCompat; -import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.SanitizedFile; import org.xml.sax.XMLReader; @@ -116,27 +115,13 @@ public final class Utils { return "/icons-120/"; } - public static void copy(InputStream input, OutputStream output) - throws IOException { - copy(input, output, null, null); - } - - public static void copy(InputStream input, OutputStream output, - ProgressListener progressListener, - ProgressListener.Event templateProgressEvent) - throws IOException { + public static void copy(InputStream input, OutputStream output) throws IOException { byte[] buffer = new byte[BUFFER_SIZE]; - int bytesRead = 0; while (true) { int count = input.read(buffer); if (count == -1) { break; } - if (progressListener != null) { - bytesRead += count; - templateProgressEvent.progress = bytesRead; - progressListener.onProgress(templateProgressEvent); - } output.write(buffer, 0, count); } output.flush(); @@ -335,6 +320,17 @@ public final class Utils { return apkCacheDir; } + /** + * Get the full path for where an APK URL will be downloaded into. + */ + public static SanitizedFile getApkDownloadPath(Context context, Uri uri) { + File dir = new File(Utils.getApkCacheDir(context), uri.getHost() + "-" + uri.getPort()); + if (!dir.exists()) { + dir.mkdirs(); + } + return new SanitizedFile(dir, uri.getLastPathSegment()); + } + /** * Recursively delete files in {@code dir} that were last modified * {@code secondsAgo} seconds ago, e.g. when it was downloaded. @@ -434,10 +430,6 @@ public final class Utils { return new Locale(languageTag); } - public static String getApkUrl(String repoAddress, Apk apk) { - return repoAddress + "/" + apk.apkName.replace(" ", "%20"); - } - public static final class CommaSeparatedList implements Iterable { private final String value; diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index 387312940..ee68ede09 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -121,6 +121,13 @@ public class Apk extends ValueObject implements Comparable { } } + public String getUrl() { + if (repoAddress == null || apkName == null) { + throw new IllegalStateException("Apk needs to have both ApkProvider.DataColumns.REPO_ADDRESS and ApkProvider.DataColumns.NAME set in order to calculate URL."); + } + return repoAddress + "/" + apkName.replace(" ", "%20"); + } + @Override public String toString() { return packageName + " (version " + versionCode + ")"; diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java new file mode 100644 index 000000000..12a20b692 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -0,0 +1,344 @@ +package org.fdroid.fdroid.installer; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; + +import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.net.Downloader; +import org.fdroid.fdroid.net.DownloaderService; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Manages the whole process when a background update triggers an install or the user + * requests an APK to be installed. It handles checking whether the APK is cached, + * downloading it, putting up and maintaining a {@link Notification}, and more. + *

+ * Data is sent via {@link Intent}s so that Android handles the message queuing + * and {@link Service} lifecycle for us, although it adds one layer of redirection + * between the static method to send the {@code Intent} and the method to + * actually process it. + *

+ * The full URL for the APK file to download is also used as the unique ID to + * represent the download itself throughout F-Droid. This follows the model + * of {@link Intent#setData(Uri)}, where the core data of an {@code Intent} is + * a {@code Uri}. The full download URL is guaranteed to be unique since it + * points to files on a filesystem, where there cannot be multiple files with + * the same name. This provides a unique ID beyond just {@code packageName} + * and {@code versionCode} since there could be different copies of the same + * APK on different servers, signed by different keys, or even different builds. + *

    + *
  • for a {@link Uri} ID, use {@code Uri}, {@link Intent#getData()} + *
  • for a {@code String} ID, use {@code urlString}, {@link Uri#toString()}, or + * {@link Intent#getDataString()} + *
  • for an {@code int} ID, use {@link String#hashCode()} + *

+ */ +public class InstallManagerService extends Service { + public static final String TAG = "InstallManagerService"; + + private static final String ACTION_INSTALL = "org.fdroid.fdroid.InstallManagerService.action.INSTALL"; + + /** + * The collection of {@link Apk}s that are actively going through this whole process, + * matching the {@link App}s in {@code ACTIVE_APPS}. The key is the download URL, as + * in {@link Apk#getUrl()} or {@code urlString}. + */ + private static final HashMap ACTIVE_APKS = new HashMap(3); + + /** + * The collection of {@link App}s that are actively going through this whole process, + * matching the {@link Apk}s in {@code ACTIVE_APKS}. The key is the + * {@code packageName} of the app. + */ + private static final HashMap ACTIVE_APPS = new HashMap(3); + + /** + * The array of active {@link BroadcastReceiver}s for each active APK. The key is the + * download URL, as in {@link Apk#getUrl()} or {@code urlString}. + */ + private final HashMap receivers = new HashMap(3); + + /** + * Get the app name based on a {@code urlString} key. The app name needs + * to be kept around for the final notification update, but {@link App} + * and {@link Apk} instances have already removed by the time that final + * notification update comes around. Once there is a proper + * {@code InstallerService} and its integrated here, this must go away, + * since the {@link App} and {@link Apk} instances will be available. + *

+ * TODO delete me once InstallerService exists + */ + private static final HashMap TEMP_HACK_APP_NAMES = new HashMap(3); + + private LocalBroadcastManager localBroadcastManager; + private NotificationManager notificationManager; + + /** + * This service does not use binding, so no need to implement this method + */ + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + Utils.debugLog(TAG, "creating Service"); + localBroadcastManager = LocalBroadcastManager.getInstance(this); + notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + BroadcastReceiver br = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String packageName = intent.getData().getSchemeSpecificPart(); + for (Map.Entry entry : ACTIVE_APKS.entrySet()) { + if (TextUtils.equals(packageName, entry.getValue().packageName)) { + String urlString = entry.getKey(); + cancelNotification(urlString); + break; + } + } + } + }; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addDataScheme("package"); + registerReceiver(br, intentFilter); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Utils.debugLog(TAG, "onStartCommand " + intent); + String urlString = intent.getDataString(); + Apk apk = ACTIVE_APKS.get(urlString); + + Notification notification = createNotification(intent.getDataString(), apk).build(); + notificationManager.notify(urlString.hashCode(), notification); + + registerDownloaderReceivers(urlString); + + File apkFilePath = Utils.getApkDownloadPath(this, intent.getData()); + long apkFileSize = apkFilePath.length(); + if (!apkFilePath.exists() || apkFileSize < apk.size) { + Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath); + DownloaderService.queue(this, urlString); + } else if (apkFileSize == apk.size) { + Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath); + sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); + sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath); + } else { + Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath); + apkFilePath.delete(); + DownloaderService.queue(this, urlString); + } + return START_REDELIVER_INTENT; // if killed before completion, retry Intent + } + + private void sendBroadcast(Uri uri, String action, File file) { + Intent intent = new Intent(action); + intent.setData(uri); + intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath()); + localBroadcastManager.sendBroadcast(intent); + } + + private void unregisterDownloaderReceivers(String urlString) { + for (BroadcastReceiver receiver : receivers.get(urlString)) { + localBroadcastManager.unregisterReceiver(receiver); + } + } + + private void registerDownloaderReceivers(String urlString) { + BroadcastReceiver startedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + } + }; + BroadcastReceiver progressReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String urlString = intent.getDataString(); + Apk apk = ACTIVE_APKS.get(urlString); + int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0); + int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0); + Notification notification = createNotification(urlString, apk) + .setProgress(totalBytes, bytesRead, false) + .build(); + notificationManager.notify(urlString.hashCode(), notification); + } + }; + BroadcastReceiver completeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String urlString = intent.getDataString(); + // TODO these need to be removed based on whether they are fed to InstallerService or not + Apk apk = ACTIVE_APKS.remove(urlString); + ACTIVE_APPS.remove(apk.packageName); + if (AppDetails.isAppVisible(apk.packageName)) { + cancelNotification(urlString); + } else { + notifyDownloadComplete(apk, urlString); + } + unregisterDownloaderReceivers(urlString); + } + }; + BroadcastReceiver interruptedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String urlString = intent.getDataString(); + Apk apk = ACTIVE_APKS.remove(urlString); + ACTIVE_APPS.remove(apk.packageName); + unregisterDownloaderReceivers(urlString); + if (AppDetails.isAppVisible(apk.packageName)) { + cancelNotification(urlString); + } + } + }; + localBroadcastManager.registerReceiver(startedReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_STARTED)); + localBroadcastManager.registerReceiver(progressReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_PROGRESS)); + localBroadcastManager.registerReceiver(completeReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE)); + localBroadcastManager.registerReceiver(interruptedReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_INTERRUPTED)); + receivers.put(urlString, new BroadcastReceiver[]{ + startedReceiver, progressReceiver, completeReceiver, interruptedReceiver, + }); + } + + private NotificationCompat.Builder createNotification(String urlString, Apk apk) { + int downloadUrlId = urlString.hashCode(); + return new NotificationCompat.Builder(this) + .setAutoCancel(true) + .setContentIntent(getAppDetailsIntent(downloadUrlId, apk)) + .setContentTitle(getString(R.string.downloading_apk, getAppName(urlString, apk))) + .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel), + DownloaderService.getCancelPendingIntent(this, urlString)) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentText(urlString) + .setProgress(100, 0, true); + } + + private String getAppName(String urlString, Apk apk) { + App app = ACTIVE_APPS.get(apk.packageName); + if (app == null || TextUtils.isEmpty(app.name)) { + if (TEMP_HACK_APP_NAMES.containsKey(urlString)) { + return getString(R.string.downloading_apk, TEMP_HACK_APP_NAMES.get(urlString)); + } else { + // this is ugly, but its better than nothing as a failsafe + return getString(R.string.downloading_apk, urlString); + } + } else { + return getString(R.string.downloading_apk, app.name); + } + } + + /** + * Get a {@link PendingIntent} for a {@link Notification} to send when it + * is clicked. {@link AppDetails} handles {@code Intent}s that are missing + * or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked + * here. + */ + private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) { + Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class) + .putExtra(AppDetails.EXTRA_APPID, apk.packageName); + return TaskStackBuilder.create(getApplicationContext()) + .addParentStack(AppDetails.class) + .addNextIntent(notifyIntent) + .getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Post a notification about a completed download. {@code packageName} must be a valid + * and currently in the app index database. + */ + private void notifyDownloadComplete(Apk apk, String urlString) { + String title; + try { + PackageManager pm = getPackageManager(); + title = String.format(getString(R.string.tap_to_update_format), + pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0))); + } catch (PackageManager.NameNotFoundException e) { + title = String.format(getString(R.string.tap_to_install_format), getAppName(urlString, apk)); + } + + int downloadUrlId = urlString.hashCode(); + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this) + .setAutoCancel(true) + .setContentTitle(title) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentIntent(getAppDetailsIntent(downloadUrlId, apk)) + .setContentText(getString(R.string.tap_to_install)); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.notify(downloadUrlId, builder.build()); + } + + /** + * Cancel the {@link Notification} tied to {@code urlString}, which is the + * unique ID used to represent a given APK file. {@link String#hashCode()} + * converts {@code urlString} to the required {@code int}. + */ + private void cancelNotification(String urlString) { + notificationManager.cancel(urlString.hashCode()); + } + + /** + * Install an APK, checking the cache and downloading if necessary before starting the process. + * All notifications are sent as an {@link Intent} via local broadcasts to be received by + * + * @param context this app's {@link Context} + */ + public static void queue(Context context, App app, Apk apk) { + String urlString = apk.getUrl(); + Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString); + ACTIVE_APKS.put(urlString, apk); + ACTIVE_APPS.put(app.packageName, app); + TEMP_HACK_APP_NAMES.put(urlString, app.name); // TODO delete me once InstallerService exists + Intent intent = new Intent(context, InstallManagerService.class); + intent.setAction(ACTION_INSTALL); + intent.setData(Uri.parse(urlString)); + context.startService(intent); + } + + /** + * Returns a {@link Set} of the {@code urlString}s that are currently active. + * {@code urlString}s are used as unique IDs throughout the + * {@code InstallManagerService} process, either as a {@code String} or as an + * {@code int} from {@link String#hashCode()}. + */ + public static Set getActiveDownloadUrls() { + return ACTIVE_APKS.keySet(); + } + + /** + * Returns a {@link Set} of the {@code packageName}s that are currently active. + * {@code packageName}s are used as unique IDs for apps throughout all of + * Android, F-Droid, and other apps stores. + */ + public static Set getActivePackageNames() { + return ACTIVE_APPS.keySet(); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java index 7981c2571..cb68aa575 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.net; +import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.Utils; import java.io.File; @@ -36,19 +37,10 @@ public abstract class Downloader { protected final URL sourceUrl; protected String cacheTag; - /** - * This is meant only to send progress to {@link DownloaderService}. This - * also keeps this class pure Java so that it can be tested on the JVM, - * without requiring an Android device or emulator. - */ - interface DownloaderProgressListener { - void sendProgress(URL sourceUrl, int bytesRead, int totalBytes); - } - /** * For sending download progress, should only be called in {@link #progressTask} */ - private volatile DownloaderProgressListener downloaderProgressListener; + private volatile ProgressListener downloaderProgressListener; protected abstract InputStream getDownloadersInputStream() throws IOException; @@ -64,7 +56,7 @@ public abstract class Downloader { return new WrappedInputStream(getDownloadersInputStream()); } - public void setListener(DownloaderProgressListener listener) { + public void setListener(ProgressListener listener) { this.downloaderProgressListener = listener; } @@ -194,7 +186,7 @@ public abstract class Downloader { @Override public void run() { if (downloaderProgressListener != null) { - downloaderProgressListener.sendProgress(sourceUrl, bytesRead, totalBytes); + downloaderProgressListener.onProgress(sourceUrl, bytesRead, totalBytes); } } }; diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index 0f53a4693..7e5961d95 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -17,14 +17,11 @@ package org.fdroid.fdroid.net; -import android.app.Notification; -import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; @@ -33,21 +30,12 @@ import android.os.Looper; import android.os.Message; import android.os.PatternMatcher; import android.os.Process; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; -import org.fdroid.fdroid.AppDetails; -import org.fdroid.fdroid.FDroid; -import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.R; +import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.SanitizedFile; import java.io.File; @@ -86,13 +74,9 @@ import java.net.URL; public class DownloaderService extends Service { private static final String TAG = "DownloaderService"; - private static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.net.DownloaderService.extra.PACKAGE_NAME"; - private static final String ACTION_QUEUE = "org.fdroid.fdroid.net.DownloaderService.action.QUEUE"; private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL"; - private static final int NOTIFY_DOWNLOADING = 0x2344; - private volatile Looper serviceLooper; private static volatile ServiceHandler serviceHandler; private static volatile Downloader downloader; @@ -137,6 +121,7 @@ public class DownloaderService extends Service { Integer whatToRemove = uriString.hashCode(); if (serviceHandler.hasMessages(whatToRemove)) { serviceHandler.removeMessages(whatToRemove); + sendBroadcast(intent.getData(), Downloader.ACTION_INTERRUPTED); } else if (isActive(uriString)) { downloader.cancelDownload(); } else { @@ -154,77 +139,27 @@ public class DownloaderService extends Service { } } - private NotificationCompat.Builder createNotification(String urlString, @Nullable String packageName) { - return new NotificationCompat.Builder(this) - .setAutoCancel(true) - .setContentIntent(createAppDetailsIntent(0, packageName)) - .setContentTitle(getNotificationTitle(packageName)) - .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel), - createCancelDownloadIntent(this, 0, urlString)) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setContentText(urlString) - .setProgress(100, 0, true); - } - - /** - * If downloading an apk (i.e. packageName != null) then the title will indicate - * the name of the app which the apk belongs to. Otherwise, it will be a generic "Downloading..." - * message. - */ - private String getNotificationTitle(@Nullable String packageName) { - if (packageName != null) { - final App app = AppProvider.Helper.findByPackageName( - getContentResolver(), packageName, new String[]{AppProvider.DataColumns.NAME}); - if (app != null) { - return getString(R.string.downloading_apk, app.name); - } - } - return getString(R.string.downloading); - } - - private PendingIntent createAppDetailsIntent(int requestCode, String packageName) { - TaskStackBuilder stackBuilder; - if (packageName != null) { - Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class) - .putExtra(AppDetails.EXTRA_APPID, packageName); - - stackBuilder = TaskStackBuilder - .create(getApplicationContext()) - .addParentStack(AppDetails.class) - .addNextIntent(notifyIntent); - } else { - Intent notifyIntent = new Intent(getApplicationContext(), FDroid.class); - stackBuilder = TaskStackBuilder - .create(getApplicationContext()) - .addParentStack(FDroid.class) - .addNextIntent(notifyIntent); - } - - return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public static PendingIntent createCancelDownloadIntent(@NonNull Context context, int - requestCode, @NonNull String urlString) { + public static PendingIntent getCancelPendingIntent(Context context, String urlString) { Intent cancelIntent = new Intent(context.getApplicationContext(), DownloaderService.class) .setData(Uri.parse(urlString)) - .setAction(ACTION_CANCEL); + .setAction(ACTION_CANCEL) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); return PendingIntent.getService(context.getApplicationContext(), - requestCode, + urlString.hashCode(), cancelIntent, - PendingIntent.FLAG_CANCEL_CURRENT); + PendingIntent.FLAG_UPDATE_CURRENT); } @Override public int onStartCommand(Intent intent, int flags, int startId) { - onStart(intent, startId); Utils.debugLog(TAG, "onStartCommand " + intent); + onStart(intent, startId); return START_REDELIVER_INTENT; // if killed before completion, retry Intent } @Override public void onDestroy() { Utils.debugLog(TAG, "Destroying downloader service. Will move to background and stop our Looper."); - stopForeground(true); serviceLooper.quit(); //NOPMD - this is copied from IntentService, no super call needed } @@ -254,40 +189,23 @@ public class DownloaderService extends Service { */ protected void handleIntent(Intent intent) { final Uri uri = intent.getData(); - File downloadDir = new File(Utils.getApkCacheDir(this), uri.getHost() + "-" + uri.getPort()); - downloadDir.mkdirs(); - final SanitizedFile localFile = new SanitizedFile(downloadDir, uri.getLastPathSegment()); - final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + final SanitizedFile localFile = Utils.getApkDownloadPath(this, uri); sendBroadcast(uri, Downloader.ACTION_STARTED, localFile); - if (Preferences.get().isUpdateNotificationEnabled()) { - Notification notification = createNotification(intent.getDataString(), intent.getStringExtra(EXTRA_PACKAGE_NAME)).build(); - startForeground(NOTIFY_DOWNLOADING, notification); - } - try { downloader = DownloaderFactory.create(this, uri, localFile); - downloader.setListener(new Downloader.DownloaderProgressListener() { + downloader.setListener(new ProgressListener() { @Override - public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) { + public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { Intent intent = new Intent(Downloader.ACTION_PROGRESS); intent.setData(uri); intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead); intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes); localBroadcastManager.sendBroadcast(intent); - - if (Preferences.get().isUpdateNotificationEnabled()) { - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - Notification notification = createNotification(uri.toString(), packageName) - .setProgress(totalBytes, bytesRead, false) - .build(); - nm.notify(NOTIFY_DOWNLOADING, notification); - } } }); downloader.download(); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); - notifyDownloadComplete(packageName, intent.getDataString()); } catch (InterruptedException e) { sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile); } catch (IOException e) { @@ -302,34 +220,8 @@ public class DownloaderService extends Service { downloader = null; } - /** - * Post a notification about a completed download. {@code packageName} must be a valid - * and currently in the app index database. - */ - private void notifyDownloadComplete(String packageName, String urlString) { - String title; - try { - PackageManager pm = getPackageManager(); - title = String.format(getString(R.string.tap_to_update_format), - pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0))); - } catch (PackageManager.NameNotFoundException e) { - App app = AppProvider.Helper.findByPackageName(getContentResolver(), packageName, - new String[]{ - AppProvider.DataColumns.NAME, - }); - title = String.format(getString(R.string.tap_to_install_format), app.name); - } - - int downloadUrlId = urlString.hashCode(); - NotificationCompat.Builder builder = - new NotificationCompat.Builder(this) - .setAutoCancel(true) - .setContentTitle(title) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setContentIntent(createAppDetailsIntent(downloadUrlId, packageName)) - .setContentText(getString(R.string.tap_to_install)); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.notify(downloadUrlId, builder.build()); + private void sendBroadcast(Uri uri, String action) { + sendBroadcast(uri, action, null, null); } private void sendBroadcast(Uri uri, String action, File file) { @@ -339,7 +231,9 @@ public class DownloaderService extends Service { private void sendBroadcast(Uri uri, String action, File file, String errorMessage) { Intent intent = new Intent(action); intent.setData(uri); - intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath()); + if (file != null) { + intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath()); + } if (!TextUtils.isEmpty(errorMessage)) { intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, errorMessage); } @@ -351,19 +245,15 @@ public class DownloaderService extends Service { *

* All notifications are sent as an {@link Intent} via local broadcasts to be received by * - * @param context this app's {@link Context} - * @param packageName The packageName of the app being downloaded - * @param urlString The URL to add to the download queue + * @param context this app's {@link Context} + * @param urlString The URL to add to the download queue * @see #cancel(Context, String) */ - public static void queue(Context context, String packageName, String urlString) { + public static void queue(Context context, String urlString) { Utils.debugLog(TAG, "Preparing " + urlString + " to go into the download queue"); Intent intent = new Intent(context, DownloaderService.class); intent.setAction(ACTION_QUEUE); intent.setData(Uri.parse(urlString)); - if (!TextUtils.isEmpty(packageName)) { - intent.putExtra(EXTRA_PACKAGE_NAME, packageName); - } context.startService(intent); } @@ -374,7 +264,7 @@ public class DownloaderService extends Service { * * @param context this app's {@link Context} * @param urlString The URL to remove from the download queue - * @see #queue(Context, String, String) + * @see #queue(Context, String) */ public static void cancel(Context context, String urlString) { Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download"); @@ -393,6 +283,9 @@ public class DownloaderService extends Service { if (TextUtils.isEmpty(urlString)) { //NOPMD - suggests unreadable format return false; } + if (serviceHandler == null) { + return false; // this service is not even running + } return serviceHandler.hasMessages(urlString.hashCode()) || isActive(urlString); } diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index 5cc5f75ea..5b71d6d7a 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -157,7 +157,7 @@ public class HttpDownloader extends Downloader { if (isCached()) { Utils.debugLog(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")"); } else { - Utils.debugLog(TAG, "doDownload for " + sourceUrl + " " + resumable); + Utils.debugLog(TAG, "Need to download " + sourceUrl + " (is resumable: " + resumable + ")"); downloadFromStream(8192, resumable); updateCacheCheck(); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java index 009a0399b..d7b42505d 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java @@ -297,7 +297,7 @@ public class SwapAppsView extends ListView implements // TODO: Unregister receivers correctly... Apk apk = getApkToInstall(); - String url = Utils.getApkUrl(apk.repoAddress, apk); + String url = apk.getUrl(); localBroadcastManager = LocalBroadcastManager.getInstance(getActivity()); localBroadcastManager.registerReceiver(appListViewResetReceiver, diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index a1585a512..6b0b46aaf 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -41,6 +41,7 @@ import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.NewRepoConfig; +import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.localrepo.LocalRepoManager; import org.fdroid.fdroid.localrepo.SwapService; @@ -778,7 +779,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { public void install(@NonNull final App app) { final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode); - String urlString = Utils.getApkUrl(apk.repoAddress, apk); + String urlString = apk.getUrl(); downloadCompleteReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -788,7 +789,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { }; localBroadcastManager.registerReceiver(downloadCompleteReceiver, DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE)); - DownloaderService.queue(this, app.packageName, urlString); + InstallManagerService.queue(this, app, apk); } private void handleDownloadComplete(File apkFile, String packageName, String urlString) { diff --git a/app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java b/app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java index 7b56bee6b..3beedff97 100644 --- a/app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid.net; +import org.fdroid.fdroid.ProgressListener; import org.junit.Test; import java.io.File; @@ -48,9 +49,9 @@ public class HttpDownloaderTest { URL url = new URL(urlString); File destFile = File.createTempFile("dl-", ""); final HttpDownloader httpDownloader = new HttpDownloader(url, destFile); - httpDownloader.setListener(new Downloader.DownloaderProgressListener() { + httpDownloader.setListener(new ProgressListener() { @Override - public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) { + public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { System.out.println("DownloaderProgressListener.sendProgress " + sourceUrl + " " + bytesRead + " / " + totalBytes); receivedProgress = true; } @@ -111,9 +112,9 @@ public class HttpDownloaderTest { URL url = new URL("https://f-droid.org/repo/index.jar"); File destFile = File.createTempFile("dl-", ""); final HttpDownloader httpDownloader = new HttpDownloader(url, destFile); - httpDownloader.setListener(new Downloader.DownloaderProgressListener() { + httpDownloader.setListener(new ProgressListener() { @Override - public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) { + public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { System.out.println("DownloaderProgressListener.sendProgress " + bytesRead + " / " + totalBytes); receivedProgress = true; latch.countDown();