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" />
* 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
+ * 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.
+ *
+ *
+ * TODO delete me once InstallerService exists
+ */
+ private static final HashMappackageName != 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();