Merge branch 'canonical-url-overhaul' into 'master'

Canonical URL overhaul

Closes #1742, #1736, and #1727

See merge request fdroid/fdroidclient!809
This commit is contained in:
Hans-Christoph Steiner 2019-03-28 11:32:46 +00:00
commit 727e9ed5dd
28 changed files with 386 additions and 316 deletions

View File

@ -70,7 +70,7 @@ public class HttpDownloaderTest {
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile); final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
httpDownloader.setListener(new ProgressListener() { httpDownloader.setListener(new ProgressListener() {
@Override @Override
public void onProgress(String urlString, long bytesRead, long totalBytes) { public void onProgress(long bytesRead, long totalBytes) {
receivedProgress = true; receivedProgress = true;
} }
}); });
@ -132,7 +132,7 @@ public class HttpDownloaderTest {
final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile); final HttpDownloader httpDownloader = new HttpDownloader(uri, destFile);
httpDownloader.setListener(new ProgressListener() { httpDownloader.setListener(new ProgressListener() {
@Override @Override
public void onProgress(String urlString, long bytesRead, long totalBytes) { public void onProgress(long bytesRead, long totalBytes) {
receivedProgress = true; receivedProgress = true;
latch.countDown(); latch.countDown();
} }

View File

@ -314,10 +314,8 @@ public class SwapAppsView extends ListView implements
} }
if (apk != null) { if (apk != null) {
String urlString = apk.getUrl();
// TODO unregister receivers? or will they just die with this instance // TODO unregister receivers? or will they just die with this instance
IntentFilter downloadFilter = DownloaderService.getIntentFilter(urlString); IntentFilter downloadFilter = DownloaderService.getIntentFilter(apk.getCanonicalUrl());
localBroadcastManager.registerReceiver(downloadReceiver, downloadFilter); localBroadcastManager.registerReceiver(downloadReceiver, downloadFilter);
} }

View File

@ -836,9 +836,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
} }
public void install(@NonNull final App app, @NonNull final Apk apk) { public void install(@NonNull final App app, @NonNull final Apk apk) {
Uri downloadUri = Uri.parse(apk.getUrl());
localBroadcastManager.registerReceiver(installReceiver, localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(downloadUri)); Installer.getInstallIntentFilter(apk.getCanonicalUrl()));
InstallManagerService.queue(this, app, apk); InstallManagerService.queue(this, app, apk);
} }

View File

@ -72,9 +72,7 @@ public final class AppUpdateStatusManager {
*/ */
public static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove"; public static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove";
public static final String EXTRA_APK_URL = "urlstring";
public static final String EXTRA_STATUS = "status"; public static final String EXTRA_STATUS = "status";
public static final String EXTRA_REASON_FOR_CHANGE = "reason"; public static final String EXTRA_REASON_FOR_CHANGE = "reason";
public static final String REASON_READY_TO_INSTALL = "readytoinstall"; public static final String REASON_READY_TO_INSTALL = "readytoinstall";
@ -129,11 +127,11 @@ public final class AppUpdateStatusManager {
/** /**
* @return the unique ID used to represent this specific package's install process * @return the unique ID used to represent this specific package's install process
* also known as {@code urlString}. * also known as {@code canonicalUrl}.
* @see org.fdroid.fdroid.installer.InstallManagerService * @see org.fdroid.fdroid.installer.InstallManagerService
*/ */
public String getUniqueKey() { public String getCanonicalUrl() {
return apk.getUrl(); return apk.getCanonicalUrl();
} }
/** /**
@ -225,9 +223,9 @@ public final class AppUpdateStatusManager {
} }
@Nullable @Nullable
public AppUpdateStatus get(String key) { public AppUpdateStatus get(String canonicalUrl) {
synchronized (appMapping) { synchronized (appMapping) {
return appMapping.get(key); return appMapping.get(canonicalUrl);
} }
} }
@ -264,7 +262,7 @@ public final class AppUpdateStatusManager {
notifyChange(entry, isStatusUpdate); notifyChange(entry, isStatusUpdate);
if (status == Status.Installed) { if (status == Status.Installed) {
InstallManagerService.removePendingInstall(context, entry.getUniqueKey()); InstallManagerService.removePendingInstall(context, entry.getCanonicalUrl());
} }
} }
@ -272,11 +270,11 @@ public final class AppUpdateStatusManager {
Utils.debugLog(LOGTAG, "Add APK " + apk.apkName + " with state " + status.name()); Utils.debugLog(LOGTAG, "Add APK " + apk.apkName + " with state " + status.name());
AppUpdateStatus entry = createAppEntry(apk, status, intent); AppUpdateStatus entry = createAppEntry(apk, status, intent);
setEntryContentIntentIfEmpty(entry); setEntryContentIntentIfEmpty(entry);
appMapping.put(entry.getUniqueKey(), entry); appMapping.put(entry.getCanonicalUrl(), entry);
notifyAdd(entry); notifyAdd(entry);
if (status == Status.Installed) { if (status == Status.Installed) {
InstallManagerService.removePendingInstall(context, entry.getUniqueKey()); InstallManagerService.removePendingInstall(context, entry.getCanonicalUrl());
} }
} }
@ -291,7 +289,7 @@ public final class AppUpdateStatusManager {
private void notifyAdd(AppUpdateStatus entry) { private void notifyAdd(AppUpdateStatus entry) {
if (!isBatchUpdating) { if (!isBatchUpdating) {
Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_ADDED); Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_ADDED);
broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey()); broadcastIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, entry.getCanonicalUrl());
broadcastIntent.putExtra(EXTRA_STATUS, entry.copy()); broadcastIntent.putExtra(EXTRA_STATUS, entry.copy());
localBroadcastManager.sendBroadcast(broadcastIntent); localBroadcastManager.sendBroadcast(broadcastIntent);
} }
@ -300,7 +298,7 @@ public final class AppUpdateStatusManager {
private void notifyChange(AppUpdateStatus entry, boolean isStatusUpdate) { private void notifyChange(AppUpdateStatus entry, boolean isStatusUpdate) {
if (!isBatchUpdating) { if (!isBatchUpdating) {
Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_CHANGED); Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_CHANGED);
broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey()); broadcastIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, entry.getCanonicalUrl());
broadcastIntent.putExtra(EXTRA_STATUS, entry.copy()); broadcastIntent.putExtra(EXTRA_STATUS, entry.copy());
broadcastIntent.putExtra(EXTRA_IS_STATUS_UPDATE, isStatusUpdate); broadcastIntent.putExtra(EXTRA_IS_STATUS_UPDATE, isStatusUpdate);
localBroadcastManager.sendBroadcast(broadcastIntent); localBroadcastManager.sendBroadcast(broadcastIntent);
@ -310,7 +308,7 @@ public final class AppUpdateStatusManager {
private void notifyRemove(AppUpdateStatus entry) { private void notifyRemove(AppUpdateStatus entry) {
if (!isBatchUpdating) { if (!isBatchUpdating) {
Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_REMOVED); Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_REMOVED);
broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey()); broadcastIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, entry.getCanonicalUrl());
broadcastIntent.putExtra(EXTRA_STATUS, entry.copy()); broadcastIntent.putExtra(EXTRA_STATUS, entry.copy());
localBroadcastManager.sendBroadcast(broadcastIntent); localBroadcastManager.sendBroadcast(broadcastIntent);
} }
@ -321,7 +319,7 @@ public final class AppUpdateStatusManager {
ContentResolver resolver = context.getContentResolver(); ContentResolver resolver = context.getContentResolver();
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repoId); App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repoId);
AppUpdateStatus ret = new AppUpdateStatus(app, apk, status, intent); AppUpdateStatus ret = new AppUpdateStatus(app, apk, status, intent);
appMapping.put(apk.getUrl(), ret); appMapping.put(apk.getCanonicalUrl(), ret);
return ret; return ret;
} }
} }
@ -347,7 +345,7 @@ public final class AppUpdateStatusManager {
} }
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(apk.getUrl()); AppUpdateStatus entry = appMapping.get(apk.getCanonicalUrl());
if (entry != null) { if (entry != null) {
updateApkInternal(entry, status, pendingIntent); updateApkInternal(entry, status, pendingIntent);
} else { } else {
@ -359,9 +357,9 @@ public final class AppUpdateStatusManager {
/** /**
* @param pendingIntent Action when notification is clicked. Can be null for default action(s) * @param pendingIntent Action when notification is clicked. Can be null for default action(s)
*/ */
public void updateApk(String key, @NonNull Status status, @Nullable PendingIntent pendingIntent) { public void updateApk(String canonicalUrl, @NonNull Status status, @Nullable PendingIntent pendingIntent) {
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(key); AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) { if (entry != null) {
updateApkInternal(entry, status, pendingIntent); updateApkInternal(entry, status, pendingIntent);
} }
@ -369,9 +367,9 @@ public final class AppUpdateStatusManager {
} }
@Nullable @Nullable
public Apk getApk(String key) { public Apk getApk(String canonicalUrl) {
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(key); AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) { if (entry != null) {
return entry.apk; return entry.apk;
} }
@ -382,13 +380,13 @@ public final class AppUpdateStatusManager {
/** /**
* Remove an APK from being tracked, since it is now considered {@link Status#Installed} * Remove an APK from being tracked, since it is now considered {@link Status#Installed}
* *
* @param key the unique ID for the install process, also called {@code urlString} * @param canonicalUrl the unique ID for the install process
* @see org.fdroid.fdroid.installer.InstallManagerService * @see org.fdroid.fdroid.installer.InstallManagerService
*/ */
public void removeApk(String key) { public void removeApk(String canonicalUrl) {
synchronized (appMapping) { synchronized (appMapping) {
InstallManagerService.removePendingInstall(context, key); InstallManagerService.removePendingInstall(context, canonicalUrl);
AppUpdateStatus entry = appMapping.remove(key); AppUpdateStatus entry = appMapping.remove(canonicalUrl);
if (entry != null) { if (entry != null) {
Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.apkName); Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.apkName);
notifyRemove(entry); notifyRemove(entry);
@ -396,9 +394,9 @@ public final class AppUpdateStatusManager {
} }
} }
public void refreshApk(String key) { public void refreshApk(String canonicalUrl) {
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(key); AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) { if (entry != null) {
Utils.debugLog(LOGTAG, "Refresh APK " + entry.apk.apkName); Utils.debugLog(LOGTAG, "Refresh APK " + entry.apk.apkName);
notifyChange(entry, true); notifyChange(entry, true);
@ -406,9 +404,9 @@ public final class AppUpdateStatusManager {
} }
} }
public void updateApkProgress(String key, long max, long current) { public void updateApkProgress(String canonicalUrl, long max, long current) {
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(key); AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) { if (entry != null) {
entry.progressMax = max; entry.progressMax = max;
entry.progressCurrent = current; entry.progressCurrent = current;
@ -420,22 +418,22 @@ public final class AppUpdateStatusManager {
/** /**
* @param errorText If null, then it is likely because the user cancelled the download. * @param errorText If null, then it is likely because the user cancelled the download.
*/ */
public void setDownloadError(String url, @Nullable String errorText) { public void setDownloadError(String canonicalUrl, @Nullable String errorText) {
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(url); AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) { if (entry != null) {
entry.status = Status.DownloadInterrupted; entry.status = Status.DownloadInterrupted;
entry.errorText = errorText; entry.errorText = errorText;
entry.intent = null; entry.intent = null;
notifyChange(entry, true); notifyChange(entry, true);
removeApk(url); removeApk(canonicalUrl);
} }
} }
} }
public void setApkError(Apk apk, String errorText) { public void setApkError(Apk apk, String errorText) {
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(apk.getUrl()); AppUpdateStatus entry = appMapping.get(apk.getCanonicalUrl());
if (entry == null) { if (entry == null) {
entry = createAppEntry(apk, Status.InstallError, null); entry = createAppEntry(apk, Status.InstallError, null);
} }
@ -444,7 +442,7 @@ public final class AppUpdateStatusManager {
entry.intent = getAppErrorIntent(entry); entry.intent = getAppErrorIntent(entry);
notifyChange(entry, false); notifyChange(entry, false);
InstallManagerService.removePendingInstall(context, entry.getUniqueKey()); InstallManagerService.removePendingInstall(context, entry.getCanonicalUrl());
} }
} }

View File

@ -216,7 +216,7 @@ public class IndexUpdater {
JarFile jarFile = new JarFile(downloadedFile, true); JarFile jarFile = new JarFile(downloadedFile, true);
JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexUpdater.DATA_FILE_NAME); JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexUpdater.DATA_FILE_NAME);
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
processIndexListener, repo.address, (int) indexEntry.getSize()); processIndexListener, (int) indexEntry.getSize());
// Process the index... // Process the index...
SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParserFactory factory = SAXParserFactory.newInstance();
@ -254,14 +254,14 @@ public class IndexUpdater {
protected final ProgressListener downloadListener = new ProgressListener() { protected final ProgressListener downloadListener = new ProgressListener() {
@Override @Override
public void onProgress(String urlString, long bytesRead, long totalBytes) { public void onProgress(long bytesRead, long totalBytes) {
UpdateService.reportDownloadProgress(context, IndexUpdater.this, bytesRead, totalBytes); UpdateService.reportDownloadProgress(context, IndexUpdater.this, bytesRead, totalBytes);
} }
}; };
protected final ProgressListener processIndexListener = new ProgressListener() { protected final ProgressListener processIndexListener = new ProgressListener() {
@Override @Override
public void onProgress(String urlString, long bytesRead, long totalBytes) { public void onProgress(long bytesRead, long totalBytes) {
UpdateService.reportProcessIndexProgress(context, IndexUpdater.this, bytesRead, totalBytes); UpdateService.reportProcessIndexProgress(context, IndexUpdater.this, bytesRead, totalBytes);
} }
}; };

View File

@ -198,7 +198,7 @@ public class IndexV1Updater extends IndexUpdater {
JarFile jarFile = new JarFile(outputFile, true); JarFile jarFile = new JarFile(outputFile, true);
JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME); JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME);
InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
processIndexListener, repo.address, (int) indexEntry.getSize()); processIndexListener, (int) indexEntry.getSize());
processIndexV1(indexInputStream, indexEntry, cacheTag); processIndexV1(indexInputStream, indexEntry, cacheTag);
} }

View File

@ -14,7 +14,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(context); AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(context);
String notificationKey = intent.getStringExtra(NotificationHelper.EXTRA_NOTIFICATION_KEY); String canonicalUrl = intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
switch (intent.getAction()) { switch (intent.getAction()) {
case NotificationHelper.BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED: case NotificationHelper.BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED:
manager.clearAllUpdates(); manager.clearAllUpdates();
@ -25,13 +25,13 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
case NotificationHelper.BROADCAST_NOTIFICATIONS_UPDATE_CLEARED: case NotificationHelper.BROADCAST_NOTIFICATIONS_UPDATE_CLEARED:
// If clearing apps in state "InstallError" (like when auto-cancelling) we // If clearing apps in state "InstallError" (like when auto-cancelling) we
// remove them from the status manager entirely. // remove them from the status manager entirely.
AppUpdateStatusManager.AppUpdateStatus appUpdateStatus = manager.get(notificationKey); AppUpdateStatusManager.AppUpdateStatus appUpdateStatus = manager.get(canonicalUrl);
if (appUpdateStatus != null && appUpdateStatus.status == AppUpdateStatusManager.Status.InstallError) { if (appUpdateStatus != null && appUpdateStatus.status == AppUpdateStatusManager.Status.InstallError) {
manager.removeApk(notificationKey); manager.removeApk(canonicalUrl);
} }
break; break;
case NotificationHelper.BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED: case NotificationHelper.BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED:
manager.removeApk(notificationKey); manager.removeApk(canonicalUrl);
break; break;
} }
} }

View File

@ -46,13 +46,6 @@ class NotificationHelper {
private static final int MAX_UPDATES_TO_SHOW = 5; private static final int MAX_UPDATES_TO_SHOW = 5;
private static final int MAX_INSTALLED_TO_SHOW = 10; private static final int MAX_INSTALLED_TO_SHOW = 10;
/**
* Unique ID used to represent this specific package's install process,
* including {@link Notification}s, also known as {@code urlString}.
*
* @see org.fdroid.fdroid.installer.InstallManagerService
*/
static final String EXTRA_NOTIFICATION_KEY = "key";
private static final String GROUP_UPDATES = "updates"; private static final String GROUP_UPDATES = "updates";
private static final String GROUP_INSTALLED = "installed"; private static final String GROUP_INSTALLED = "installed";
@ -93,14 +86,14 @@ class NotificationHelper {
case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED: case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED:
updateStatusLists(); updateStatusLists();
createSummaryNotifications(); createSummaryNotifications();
url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); url = intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
entry = appUpdateStatusManager.get(url); entry = appUpdateStatusManager.get(url);
if (entry != null) { if (entry != null) {
createNotification(entry); createNotification(entry);
} }
break; break;
case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED: case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED:
url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); url = intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
entry = appUpdateStatusManager.get(url); entry = appUpdateStatusManager.get(url);
updateStatusLists(); updateStatusLists();
if (entry != null) { if (entry != null) {
@ -111,7 +104,7 @@ class NotificationHelper {
} }
break; break;
case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED:
url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); url = intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
notificationManager.cancel(url, NOTIFY_ID_INSTALLED); notificationManager.cancel(url, NOTIFY_ID_INSTALLED);
notificationManager.cancel(url, NOTIFY_ID_UPDATES); notificationManager.cancel(url, NOTIFY_ID_UPDATES);
updateStatusLists(); updateStatusLists();
@ -164,8 +157,8 @@ class NotificationHelper {
private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) { private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
if (shouldIgnoreEntry(entry)) { if (shouldIgnoreEntry(entry)) {
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
return; return;
} }
@ -177,23 +170,23 @@ class NotificationHelper {
if (entry.status == AppUpdateStatusManager.Status.Installed) { if (entry.status == AppUpdateStatusManager.Status.Installed) {
if (useStackedNotifications()) { if (useStackedNotifications()) {
notification = createInstalledNotification(entry); notification = createInstalledNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_INSTALLED, notification); notificationManager.notify(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED, notification);
} else if (installed.size() == 1) { } else if (installed.size() == 1) {
notification = createInstalledNotification(entry); notification = createInstalledNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification); notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification);
} }
} else { } else {
if (useStackedNotifications()) { if (useStackedNotifications()) {
notification = createUpdateNotification(entry); notification = createUpdateNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_UPDATES, notification); notificationManager.notify(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES, notification);
} else if (updates.size() == 1) { } else if (updates.size() == 1) {
notification = createUpdateNotification(entry); notification = createUpdateNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification); notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification);
} }
} }
@ -346,7 +339,7 @@ class NotificationHelper {
} }
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATE_CLEARED); Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATE_CLEARED);
intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); intentDeleted.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, entry.getCanonicalUrl());
intentDeleted.setClass(context, NotificationBroadcastReceiver.class); intentDeleted.setClass(context, NotificationBroadcastReceiver.class);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted); builder.setDeleteIntent(piDeleted);
@ -435,7 +428,7 @@ class NotificationHelper {
} }
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); intentDeleted.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, entry.getCanonicalUrl());
intentDeleted.setClass(context, NotificationBroadcastReceiver.class); intentDeleted.setClass(context, NotificationBroadcastReceiver.class);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted); builder.setDeleteIntent(piDeleted);
@ -540,7 +533,7 @@ class NotificationHelper {
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
// Need to check that the notification is still valid, and also that the image // Need to check that the notification is still valid, and also that the image
// is indeed cached now, so we won't get stuck in an endless loop. // is indeed cached now, so we won't get stuck in an endless loop.
AppUpdateStatusManager.AppUpdateStatus oldEntry = appUpdateStatusManager.get(entry.getUniqueKey()); AppUpdateStatusManager.AppUpdateStatus oldEntry = appUpdateStatusManager.get(entry.getCanonicalUrl());
if (oldEntry != null if (oldEntry != null
&& oldEntry.app != null && oldEntry.app != null
&& oldEntry.app.iconUrl != null && oldEntry.app.iconUrl != null

View File

@ -9,7 +9,6 @@ import java.io.InputStream;
class ProgressBufferedInputStream extends BufferedInputStream { class ProgressBufferedInputStream extends BufferedInputStream {
private final ProgressListener progressListener; private final ProgressListener progressListener;
private final String urlString;
private final int totalBytes; private final int totalBytes;
private int currentBytes; private int currentBytes;
@ -18,10 +17,9 @@ class ProgressBufferedInputStream extends BufferedInputStream {
* Reports progress to the specified {@link ProgressListener}, with the * Reports progress to the specified {@link ProgressListener}, with the
* progress based on the {@code totalBytes}. * progress based on the {@code totalBytes}.
*/ */
ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, String urlString, int totalBytes) { ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, int totalBytes) {
super(in); super(in);
this.progressListener = progressListener; this.progressListener = progressListener;
this.urlString = urlString;
this.totalBytes = totalBytes; this.totalBytes = totalBytes;
} }
@ -33,7 +31,7 @@ class ProgressBufferedInputStream extends BufferedInputStream {
* the digits changing because it looks pretty, < 9000 since the reads won't * the digits changing because it looks pretty, < 9000 since the reads won't
* line up exactly */ * line up exactly */
if (currentBytes % 333333 < 9000) { if (currentBytes % 333333 < 9000) {
progressListener.onProgress(urlString, currentBytes, totalBytes); progressListener.onProgress(currentBytes, totalBytes);
} }
} }
return super.read(buffer, byteOffset, byteCount); return super.read(buffer, byteOffset, byteCount);

View File

@ -19,6 +19,6 @@ import java.net.URL;
*/ */
public interface ProgressListener { public interface ProgressListener {
void onProgress(String urlString, long bytesRead, long totalBytes); void onProgress(long bytesRead, long totalBytes);
} }

View File

@ -263,8 +263,17 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
} }
} }
/**
* Get the URL that points to the canonical download source for this
* package. This is also used as the unique ID for tracking downloading,
* progress, and notifications throughout the whole install process. It
* is guaranteed to uniquely represent this file since it points to a file
* on the file system of the canonical webserver.
*
* @see org.fdroid.fdroid.installer.InstallManagerService
*/
@JsonIgnore // prevent tests from failing due to nulls in checkRepoAddress() @JsonIgnore // prevent tests from failing due to nulls in checkRepoAddress()
public String getUrl() { public String getCanonicalUrl() {
checkRepoAddress(); checkRepoAddress();
return repoAddress + "/" + apkName.replace(" ", "%20"); return repoAddress + "/" + apkName.replace(" ", "%20");
} }
@ -527,7 +536,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public File getMediaInstallPath(Context context) { public File getMediaInstallPath(Context context) {
File path = Environment.getExternalStoragePublicDirectory( File path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS); // Default for all other non-apk/media files Environment.DIRECTORY_DOWNLOADS); // Default for all other non-apk/media files
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(this.getUrl()); String fileExtension = MimeTypeMap.getFileExtensionFromUrl(this.getCanonicalUrl());
if (TextUtils.isEmpty(fileExtension)) return path; if (TextUtils.isEmpty(fileExtension)) return path;
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
String[] mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension).split("/"); String[] mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension).split("/");

View File

@ -237,7 +237,7 @@ public class InstalledAppProviderService extends JobIntentService {
PackageInfo packageInfo = getPackageInfo(intent, packageName); PackageInfo packageInfo = getPackageInfo(intent, packageName);
if (packageInfo != null) { if (packageInfo != null) {
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) { for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
ausm.updateApk(status.getUniqueKey(), AppUpdateStatusManager.Status.Installed, null); ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.Installed, null);
} }
File apk = getPathToInstalledApk(packageInfo); File apk = getPathToInstalledApk(packageInfo);
if (apk == null) { if (apk == null) {
@ -258,7 +258,7 @@ public class InstalledAppProviderService extends JobIntentService {
} else if (ACTION_DELETE.equals(action)) { } else if (ACTION_DELETE.equals(action)) {
deleteAppFromDb(this, packageName); deleteAppFromDb(this, packageName);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) { for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
ausm.updateApk(status.getUniqueKey(), AppUpdateStatusManager.Status.InstallError, null); ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.InstallError, null);
} }
} }
packageChangeNotifier.onNext(packageName); packageChangeNotifier.onNext(packageName);

View File

@ -113,10 +113,16 @@ public class ApkCache {
} }
/** /**
* Get the full path for where an APK URL will be downloaded into. * Get the full path for where an package URL will be downloaded into.
*/ */
public static SanitizedFile getApkDownloadPath(Context context, String urlString) { public static SanitizedFile getApkDownloadPath(Context context, String urlString) {
Uri uri = Uri.parse(urlString); return getApkDownloadPath(context, Uri.parse(urlString));
}
/**
* Get the full path for where an package URL will be downloaded into.
*/
public static SanitizedFile getApkDownloadPath(Context context, Uri uri) {
File dir = new File(getApkCacheDir(context), uri.getHost() + "-" + uri.getPort()); File dir = new File(getApkCacheDir(context), uri.getHost() + "-" + uri.getPort());
if (!dir.exists()) { if (!dir.exists()) {
dir.mkdirs(); dir.mkdirs();

View File

@ -43,11 +43,11 @@ public class DefaultInstaller extends Installer {
} }
@Override @Override
protected void installPackageInternal(Uri localApkUri, Uri downloadUri) { protected void installPackageInternal(Uri localApkUri, Uri canonicalUri) {
Intent installIntent = new Intent(context, DefaultInstallerActivity.class); Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE); installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
installIntent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri); installIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri);
installIntent.putExtra(Installer.EXTRA_APK, apk); installIntent.putExtra(Installer.EXTRA_APK, apk);
installIntent.setData(localApkUri); installIntent.setData(localApkUri);
@ -57,7 +57,7 @@ public class DefaultInstaller extends Installer {
installIntent, installIntent,
PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_USER_INTERACTION, sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_USER_INTERACTION,
installPendingIntent); installPendingIntent);
} }

View File

@ -48,7 +48,10 @@ public class DefaultInstallerActivity extends FragmentActivity {
private static final int REQUEST_CODE_INSTALL = 0; private static final int REQUEST_CODE_INSTALL = 0;
private static final int REQUEST_CODE_UNINSTALL = 1; private static final int REQUEST_CODE_UNINSTALL = 1;
private Uri downloadUri; /**
* @see InstallManagerService
*/
private Uri canonicalUri;
// for the broadcasts // for the broadcasts
private DefaultInstaller installer; private DefaultInstaller installer;
@ -63,7 +66,7 @@ public class DefaultInstallerActivity extends FragmentActivity {
installer = new DefaultInstaller(this, apk); installer = new DefaultInstaller(this, apk);
if (ACTION_INSTALL_PACKAGE.equals(action)) { if (ACTION_INSTALL_PACKAGE.equals(action)) {
Uri localApkUri = intent.getData(); Uri localApkUri = intent.getData();
downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI); canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
installPackage(localApkUri); installPackage(localApkUri);
} else if (ACTION_UNINSTALL_PACKAGE.equals(action)) { } else if (ACTION_UNINSTALL_PACKAGE.equals(action)) {
uninstallPackage(apk.packageName); uninstallPackage(apk.packageName);
@ -120,7 +123,7 @@ public class DefaultInstallerActivity extends FragmentActivity {
startActivityForResult(intent, REQUEST_CODE_INSTALL); startActivityForResult(intent, REQUEST_CODE_INSTALL);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
Log.e(TAG, "ActivityNotFoundException", e); Log.e(TAG, "ActivityNotFoundException", e);
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED,
"This Android rom does not support ACTION_INSTALL_PACKAGE!"); "This Android rom does not support ACTION_INSTALL_PACKAGE!");
finish(); finish();
} }
@ -169,23 +172,23 @@ public class DefaultInstallerActivity extends FragmentActivity {
* never executed on Androids < 4.0 * never executed on Androids < 4.0
*/ */
if (Build.VERSION.SDK_INT < 14) { if (Build.VERSION.SDK_INT < 14) {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_COMPLETE); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_COMPLETE);
break; break;
} }
switch (resultCode) { switch (resultCode) {
case Activity.RESULT_OK: case Activity.RESULT_OK:
installer.sendBroadcastInstall(downloadUri, installer.sendBroadcastInstall(canonicalUri,
Installer.ACTION_INSTALL_COMPLETE); Installer.ACTION_INSTALL_COMPLETE);
break; break;
case Activity.RESULT_CANCELED: case Activity.RESULT_CANCELED:
installer.sendBroadcastInstall(downloadUri, installer.sendBroadcastInstall(canonicalUri,
Installer.ACTION_INSTALL_INTERRUPTED); Installer.ACTION_INSTALL_INTERRUPTED);
break; break;
case Activity.RESULT_FIRST_USER: case Activity.RESULT_FIRST_USER:
default: default:
// AOSP returns Activity.RESULT_FIRST_USER on error // AOSP returns Activity.RESULT_FIRST_USER on error
installer.sendBroadcastInstall(downloadUri, installer.sendBroadcastInstall(canonicalUri,
Installer.ACTION_INSTALL_INTERRUPTED, Installer.ACTION_INSTALL_INTERRUPTED,
getString(R.string.install_error_unknown)); getString(R.string.install_error_unknown));
break; break;

View File

@ -23,7 +23,6 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
@ -44,15 +43,15 @@ public class FileInstaller extends Installer {
} }
@Override @Override
public void installPackage(Uri localApkUri, Uri downloadUri) { public void installPackage(Uri localApkUri, Uri canonicalUri) {
installPackageInternal(localApkUri, downloadUri); installPackageInternal(localApkUri, canonicalUri);
} }
@Override @Override
protected void installPackageInternal(Uri localApkUri, Uri downloadUri) { protected void installPackageInternal(Uri localApkUri, Uri canonicalUri) {
Intent installIntent = new Intent(context, FileInstallerActivity.class); Intent installIntent = new Intent(context, FileInstallerActivity.class);
installIntent.setAction(FileInstallerActivity.ACTION_INSTALL_FILE); installIntent.setAction(FileInstallerActivity.ACTION_INSTALL_FILE);
installIntent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri); installIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri);
installIntent.putExtra(Installer.EXTRA_APK, apk); installIntent.putExtra(Installer.EXTRA_APK, apk);
installIntent.setData(localApkUri); installIntent.setData(localApkUri);
@ -62,7 +61,7 @@ public class FileInstaller extends Installer {
installIntent, installIntent,
PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_USER_INTERACTION, sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_USER_INTERACTION,
installPendingIntent); installPendingIntent);
} }

View File

@ -13,7 +13,6 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.widget.Toast; import android.widget.Toast;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
@ -40,7 +39,10 @@ public class FileInstallerActivity extends FragmentActivity {
private Apk apk; private Apk apk;
private Uri localApkUri; private Uri localApkUri;
private Uri downloadUri; /**
* @see InstallManagerService
*/
private Uri canonicalUri;
private int act = 0; private int act = 0;
@ -51,12 +53,12 @@ public class FileInstallerActivity extends FragmentActivity {
Intent intent = getIntent(); Intent intent = getIntent();
String action = intent.getAction(); String action = intent.getAction();
localApkUri = intent.getData(); localApkUri = intent.getData();
downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI); canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
apk = intent.getParcelableExtra(Installer.EXTRA_APK); apk = intent.getParcelableExtra(Installer.EXTRA_APK);
installer = new FileInstaller(this, apk); installer = new FileInstaller(this, apk);
if (ACTION_INSTALL_FILE.equals(action)) { if (ACTION_INSTALL_FILE.equals(action)) {
if (hasStoragePermission()) { if (hasStoragePermission()) {
installPackage(localApkUri, downloadUri, apk); installPackage(localApkUri, canonicalUri, apk);
} else { } else {
requestPermission(); requestPermission();
act = 1; act = 1;
@ -110,7 +112,7 @@ public class FileInstallerActivity extends FragmentActivity {
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) { public void onClick(DialogInterface dialog, int id) {
if (act == 1) { if (act == 1) {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED);
} else if (act == 2) { } else if (act == 2) {
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED); installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED);
} }
@ -129,13 +131,13 @@ public class FileInstallerActivity extends FragmentActivity {
if (grantResults.length > 0 if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (act == 1) { if (act == 1) {
installPackage(localApkUri, downloadUri, apk); installPackage(localApkUri, canonicalUri, apk);
} else if (act == 2) { } else if (act == 2) {
uninstallPackage(apk); uninstallPackage(apk);
} }
} else { } else {
if (act == 1) { if (act == 1) {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED);
} else if (act == 2) { } else if (act == 2) {
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED); installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED);
} }
@ -144,7 +146,7 @@ public class FileInstallerActivity extends FragmentActivity {
} }
} }
private void installPackage(Uri localApkUri, Uri downloadUri, Apk apk) { private void installPackage(Uri localApkUri, Uri canonicalUri, Apk apk) {
Utils.debugLog(TAG, "Installing: " + localApkUri.getPath()); Utils.debugLog(TAG, "Installing: " + localApkUri.getPath());
File path = apk.getMediaInstallPath(activity.getApplicationContext()); File path = apk.getMediaInstallPath(activity.getApplicationContext());
path.mkdirs(); path.mkdirs();
@ -152,15 +154,15 @@ public class FileInstallerActivity extends FragmentActivity {
FileUtils.copyFileToDirectory(new File(localApkUri.getPath()), path); FileUtils.copyFileToDirectory(new File(localApkUri.getPath()), path);
} catch (IOException e) { } catch (IOException e) {
Utils.debugLog(TAG, "Failed to copy: " + e.getMessage()); Utils.debugLog(TAG, "Failed to copy: " + e.getMessage());
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED);
} }
if (apk.isMediaInstalled(activity.getApplicationContext())) { // Copying worked if (apk.isMediaInstalled(activity.getApplicationContext())) { // Copying worked
Utils.debugLog(TAG, "Copying worked: " + localApkUri.getPath()); Utils.debugLog(TAG, "Copying worked: " + localApkUri.getPath());
Toast.makeText(this, String.format(this.getString(R.string.app_installed_media), path.toString()), Toast.makeText(this, String.format(this.getString(R.string.app_installed_media), path.toString()),
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_COMPLETE); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_COMPLETE);
} else { } else {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED);
} }
finish(); finish();
} }

View File

@ -11,7 +11,6 @@ import android.content.pm.PackageInfo;
import android.net.Uri; import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@ -66,9 +65,10 @@ import java.io.IOException;
* APK on different servers, signed by different keys, or even different builds. * APK on different servers, signed by different keys, or even different builds.
* <p><ul> * <p><ul>
* <li>for a {@link Uri} ID, use {@code Uri}, {@link Intent#getData()} * <li>for a {@link Uri} ID, use {@code Uri}, {@link Intent#getData()}
* <li>for a {@code String} ID, use {@code urlString}, {@link Uri#toString()}, or * <li>for a {@code String} ID, use {@code canonicalUrl}, {@link Uri#toString()}, or
* {@link Intent#getDataString()} * {@link Intent#getDataString()}
* <li>for an {@code int} ID, use {@link String#hashCode()} or {@link Uri#hashCode()} * <li>for an {@code int} ID, use {@link String#hashCode()} or {@link Uri#hashCode()}
* <li>for an {@link Intent} extra, use {@link org.fdroid.fdroid.net.Downloader#EXTRA_CANONICAL_URL}
* </ul></p> * </ul></p>
* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both * The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both
* include caching of the generated {@code String}, so it should be plenty fast. * include caching of the generated {@code String}, so it should be plenty fast.
@ -147,24 +147,25 @@ public class InstallManagerService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
Utils.debugLog(TAG, "onStartCommand " + intent); Utils.debugLog(TAG, "onStartCommand " + intent);
String urlString = intent.getDataString(); String canonicalUrl = intent.getDataString();
if (TextUtils.isEmpty(urlString)) { if (TextUtils.isEmpty(canonicalUrl)) {
Utils.debugLog(TAG, "empty urlString, nothing to do"); Utils.debugLog(TAG, "empty canonicalUrl, nothing to do");
return START_NOT_STICKY; return START_NOT_STICKY;
} }
String action = intent.getAction(); String action = intent.getAction();
if (ACTION_CANCEL.equals(action)) { if (ACTION_CANCEL.equals(action)) {
DownloaderService.cancel(this, urlString); DownloaderService.cancel(this, canonicalUrl);
Apk apk = appUpdateStatusManager.getApk(urlString); Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
if (apk != null) { if (apk != null) {
Utils.debugLog(TAG, "also canceling OBB downloads");
DownloaderService.cancel(this, apk.getPatchObbUrl()); DownloaderService.cancel(this, apk.getPatchObbUrl());
DownloaderService.cancel(this, apk.getMainObbUrl()); DownloaderService.cancel(this, apk.getMainObbUrl());
} }
return START_NOT_STICKY; return START_NOT_STICKY;
} else if (ACTION_INSTALL.equals(action)) { } else if (ACTION_INSTALL.equals(action)) {
if (!isPendingInstall(urlString)) { if (!isPendingInstall(canonicalUrl)) {
Log.i(TAG, "Ignoring INSTALL that is not Pending Install: " + intent); Log.i(TAG, "Ignoring INSTALL that is not Pending Install: " + intent);
return START_NOT_STICKY; return START_NOT_STICKY;
} }
@ -174,14 +175,14 @@ public class InstallManagerService extends Service {
} }
if (!intent.hasExtra(EXTRA_APP) || !intent.hasExtra(EXTRA_APK)) { if (!intent.hasExtra(EXTRA_APP) || !intent.hasExtra(EXTRA_APK)) {
Utils.debugLog(TAG, urlString + " did not include both an App and Apk instance, ignoring"); Utils.debugLog(TAG, canonicalUrl + " did not include both an App and Apk instance, ignoring");
return START_NOT_STICKY; return START_NOT_STICKY;
} }
if ((flags & START_FLAG_REDELIVERY) == START_FLAG_REDELIVERY if ((flags & START_FLAG_REDELIVERY) == START_FLAG_REDELIVERY
&& !DownloaderService.isQueuedOrActive(urlString)) { && !DownloaderService.isQueuedOrActive(canonicalUrl)) {
Utils.debugLog(TAG, urlString + " finished downloading while InstallManagerService was killed."); Utils.debugLog(TAG, canonicalUrl + " finished downloading while InstallManagerService was killed.");
appUpdateStatusManager.removeApk(urlString); appUpdateStatusManager.removeApk(canonicalUrl);
return START_NOT_STICKY; return START_NOT_STICKY;
} }
@ -205,23 +206,23 @@ public class InstallManagerService extends Service {
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null); appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null);
registerPackageDownloaderReceivers(urlString); registerPackageDownloaderReceivers(canonicalUrl);
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256); getMainObb(canonicalUrl, apk);
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256); getPatchObb(canonicalUrl, apk);
File apkFilePath = ApkCache.getApkDownloadPath(this, apk.getUrl()); File apkFilePath = ApkCache.getApkDownloadPath(this, apk.getCanonicalUrl());
long apkFileSize = apkFilePath.length(); long apkFileSize = apkFilePath.length();
if (!apkFilePath.exists() || apkFileSize < apk.size) { if (!apkFilePath.exists() || apkFileSize < apk.size) {
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "download " + canonicalUrl + " " + apkFilePath);
DownloaderService.queue(this, switchUrlToNewMirror(urlString, apk.repoId), apk.repoId, urlString); DownloaderService.queueUsingRandomMirror(this, apk.repoId, canonicalUrl);
} else if (ApkCache.apkIsCached(apkFilePath, apk)) { } else if (ApkCache.apkIsCached(apkFilePath, apk)) {
Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "skip download, we have it, straight to install " + canonicalUrl + " " + apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath);
} else { } else {
Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "delete and download again " + canonicalUrl + " " + apkFilePath);
apkFilePath.delete(); apkFilePath.delete();
DownloaderService.queue(this, switchUrlToNewMirror(urlString, apk.repoId), apk.repoId, urlString); DownloaderService.queueUsingRandomMirror(this, apk.repoId, canonicalUrl);
} }
return START_REDELIVER_INTENT; // if killed before completion, retry Intent return START_REDELIVER_INTENT; // if killed before completion, retry Intent
@ -234,22 +235,12 @@ public class InstallManagerService extends Service {
localBroadcastManager.sendBroadcast(intent); localBroadcastManager.sendBroadcast(intent);
} }
/** private void getMainObb(final String canonicalUrl, Apk apk) {
* Tries to return a version of {@code urlString} from a mirror, if there getObb(canonicalUrl, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256, apk.repoId);
* is an error, it just returns {@code urlString}.
*
* @see FDroidApp#getNewMirrorOnError(String, org.fdroid.fdroid.data.Repo)
*/
public String getNewMirrorOnError(@Nullable String urlString, long repoId) {
try {
return FDroidApp.getNewMirrorOnError(urlString, RepoProvider.Helper.findById(this, repoId));
} catch (IOException e) {
return urlString;
}
} }
public String switchUrlToNewMirror(@Nullable String urlString, long repoId) { private void getPatchObb(final String canonicalUrl, Apk apk) {
return FDroidApp.switchUrlToNewMirror(urlString, RepoProvider.Helper.findById(this, repoId)); getObb(canonicalUrl, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256, apk.repoId);
} }
/** /**
@ -259,8 +250,8 @@ public class InstallManagerService extends Service {
* *
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a> * @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
*/ */
private void getObb(final String urlString, String obbUrlString, private void getObb(final String canonicalUrl, String obbUrlString,
final File obbDestFile, final String hash) { final File obbDestFile, final String hash, final long repoId) {
if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) { if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) {
return; return;
} }
@ -278,7 +269,7 @@ public class InstallManagerService extends Service {
long bytesRead = intent.getLongExtra(Downloader.EXTRA_BYTES_READ, 0); long bytesRead = intent.getLongExtra(Downloader.EXTRA_BYTES_READ, 0);
long totalBytes = intent.getLongExtra(Downloader.EXTRA_TOTAL_BYTES, 0); long totalBytes = intent.getLongExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
appUpdateStatusManager.updateApkProgress(urlString, totalBytes, bytesRead); appUpdateStatusManager.updateApkProgress(canonicalUrl, totalBytes, bytesRead);
} else if (Downloader.ACTION_COMPLETE.equals(action)) { } else if (Downloader.ACTION_COMPLETE.equals(action)) {
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
@ -310,22 +301,22 @@ public class InstallManagerService extends Service {
} else if (Downloader.ACTION_INTERRUPTED.equals(action)) { } else if (Downloader.ACTION_INTERRUPTED.equals(action)) {
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
} else if (Downloader.ACTION_CONNECTION_FAILED.equals(action)) { } else if (Downloader.ACTION_CONNECTION_FAILED.equals(action)) {
DownloaderService.queue(context, getNewMirrorOnError(urlString, 0), 0, urlString); DownloaderService.queueUsingDifferentMirror(context, repoId, canonicalUrl);
} else { } else {
throw new RuntimeException("intent action not handled!"); throw new RuntimeException("intent action not handled!");
} }
} }
}; };
DownloaderService.queue(this, switchUrlToNewMirror(obbUrlString, 0), 0, obbUrlString); DownloaderService.queueUsingRandomMirror(this, repoId, obbUrlString);
localBroadcastManager.registerReceiver(downloadReceiver, localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(obbUrlString)); DownloaderService.getIntentFilter(obbUrlString));
} }
/** /**
* Register a {@link BroadcastReceiver} for tracking download progress for a * Register a {@link BroadcastReceiver} for tracking download progress for a
* give {@code urlString}. There can be multiple of these registered at a time. * give {@code canonicalUrl}. There can be multiple of these registered at a time.
*/ */
private void registerPackageDownloaderReceivers(String urlString) { private void registerPackageDownloaderReceivers(String canonicalUrl) {
BroadcastReceiver downloadReceiver = new BroadcastReceiver() { BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override @Override
@ -334,52 +325,58 @@ public class InstallManagerService extends Service {
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
return; return;
} }
Uri downloadUri = intent.getData(); Uri canonicalUri = intent.getData();
String urlString = downloadUri.toString(); String canonicalUrl = intent.getDataString();
long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0); long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0);
String mirrorUrlString = intent.getStringExtra(Downloader.EXTRA_MIRROR_URL);
switch (intent.getAction()) { switch (intent.getAction()) {
case Downloader.ACTION_STARTED: case Downloader.ACTION_STARTED:
// App should currently be in the "PendingDownload" state, so this changes it to "Downloading". // App should currently be in the "PendingDownload" state, so this changes it to "Downloading".
Intent intentObject = new Intent(context, InstallManagerService.class); Intent intentObject = new Intent(context, InstallManagerService.class);
intentObject.setAction(ACTION_CANCEL); intentObject.setAction(ACTION_CANCEL);
intentObject.setData(downloadUri); intentObject.setData(canonicalUri);
PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0); PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0);
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Downloading, action); appUpdateStatusManager.updateApk(canonicalUrl,
AppUpdateStatusManager.Status.Downloading, action);
break; break;
case Downloader.ACTION_PROGRESS: case Downloader.ACTION_PROGRESS:
long bytesRead = intent.getLongExtra(Downloader.EXTRA_BYTES_READ, 0); long bytesRead = intent.getLongExtra(Downloader.EXTRA_BYTES_READ, 0);
long totalBytes = intent.getLongExtra(Downloader.EXTRA_TOTAL_BYTES, 0); long totalBytes = intent.getLongExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
appUpdateStatusManager.updateApkProgress(urlString, totalBytes, bytesRead); appUpdateStatusManager.updateApkProgress(canonicalUrl, totalBytes, bytesRead);
break; break;
case Downloader.ACTION_COMPLETE: case Downloader.ACTION_COMPLETE:
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
Uri localApkUri = Uri.fromFile(localFile); Uri localApkUri = Uri.fromFile(localFile);
Utils.debugLog(TAG, "download completed of " + mirrorUrlString + " to " + localApkUri); Utils.debugLog(TAG, "download completed of "
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null); + intent.getStringExtra(Downloader.EXTRA_MIRROR_URL) + " to " + localApkUri);
appUpdateStatusManager.updateApk(canonicalUrl,
AppUpdateStatusManager.Status.ReadyToInstall, null);
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
registerInstallReceiver(downloadUri); registerInstallReceiver(canonicalUrl);
Apk apk = appUpdateStatusManager.getApk(urlString); Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
if (apk != null) { if (apk != null) {
InstallerService.install(context, localApkUri, downloadUri, apk); InstallerService.install(context, localApkUri, canonicalUri, apk);
} }
break; break;
case Downloader.ACTION_INTERRUPTED: case Downloader.ACTION_INTERRUPTED:
appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)); appUpdateStatusManager.setDownloadError(canonicalUrl,
intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
break; break;
case Downloader.ACTION_CONNECTION_FAILED: case Downloader.ACTION_CONNECTION_FAILED:
// TODO move this logic into DownloaderService to hide the mirror URL stuff from this class
try { try {
String currentUrlString = FDroidApp.getNewMirrorOnError(mirrorUrlString, String currentUrlString = FDroidApp.getNewMirrorOnError(
intent.getStringExtra(Downloader.EXTRA_MIRROR_URL),
RepoProvider.Helper.findById(InstallManagerService.this, repoId)); RepoProvider.Helper.findById(InstallManagerService.this, repoId));
DownloaderService.queue(context, currentUrlString, repoId, urlString); DownloaderService.queue(context, currentUrlString, repoId, canonicalUrl);
DownloaderService.setTimeout(FDroidApp.getTimeout()); DownloaderService.setTimeout(FDroidApp.getTimeout());
} catch (IOException e) { } catch (IOException e) {
appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)); appUpdateStatusManager.setDownloadError(canonicalUrl,
intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
} }
break; break;
@ -390,14 +387,14 @@ public class InstallManagerService extends Service {
}; };
localBroadcastManager.registerReceiver(downloadReceiver, localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(urlString)); DownloaderService.getIntentFilter(canonicalUrl));
} }
/** /**
* Register a {@link BroadcastReceiver} for tracking install progress for a * Register a {@link BroadcastReceiver} for tracking install progress for a
* give {@link Uri}. There can be multiple of these registered at a time. * give {@link Uri}. There can be multiple of these registered at a time.
*/ */
private void registerInstallReceiver(Uri downloadUri) { private void registerInstallReceiver(String canonicalUrl) {
BroadcastReceiver installReceiver = new BroadcastReceiver() { BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override @Override
@ -406,15 +403,17 @@ public class InstallManagerService extends Service {
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
return; return;
} }
String downloadUrl = intent.getDataString(); String canonicalUrl = intent.getDataString();
Apk apk; Apk apk;
switch (intent.getAction()) { switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED: case Installer.ACTION_INSTALL_STARTED:
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installing, null); appUpdateStatusManager.updateApk(canonicalUrl,
AppUpdateStatusManager.Status.Installing, null);
break; break;
case Installer.ACTION_INSTALL_COMPLETE: case Installer.ACTION_INSTALL_COMPLETE:
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null); appUpdateStatusManager.updateApk(canonicalUrl,
Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl); AppUpdateStatusManager.Status.Installed, null);
Apk apkComplete = appUpdateStatusManager.getApk(canonicalUrl);
if (apkComplete != null && apkComplete.isApk()) { if (apkComplete != null && apkComplete.isApk()) {
try { try {
@ -432,7 +431,7 @@ public class InstallManagerService extends Service {
if (!TextUtils.isEmpty(errorMessage)) { if (!TextUtils.isEmpty(errorMessage)) {
appUpdateStatusManager.setApkError(apk, errorMessage); appUpdateStatusManager.setApkError(apk, errorMessage);
} else { } else {
appUpdateStatusManager.removeApk(downloadUrl); appUpdateStatusManager.removeApk(canonicalUrl);
} }
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
break; break;
@ -448,7 +447,7 @@ public class InstallManagerService extends Service {
}; };
localBroadcastManager.registerReceiver(installReceiver, localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(downloadUri)); Installer.getInstallIntentFilter(canonicalUrl));
} }
/** /**
@ -464,23 +463,23 @@ public class InstallManagerService extends Service {
* @param context this app's {@link Context} * @param context this app's {@link Context}
*/ */
public static void queue(Context context, App app, @NonNull Apk apk) { public static void queue(Context context, App app, @NonNull Apk apk) {
String urlString = apk.getUrl(); String canonicalUrl = apk.getCanonicalUrl();
AppUpdateStatusManager.getInstance(context).addApk(apk, AppUpdateStatusManager.Status.PendingInstall, null); AppUpdateStatusManager.getInstance(context).addApk(apk, AppUpdateStatusManager.Status.PendingInstall, null);
putPendingInstall(context, urlString, apk.packageName); putPendingInstall(context, canonicalUrl, apk.packageName);
Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString); Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + canonicalUrl);
Intent intent = new Intent(context, InstallManagerService.class); Intent intent = new Intent(context, InstallManagerService.class);
intent.setAction(ACTION_INSTALL); intent.setAction(ACTION_INSTALL);
intent.setData(Uri.parse(urlString)); intent.setData(Uri.parse(canonicalUrl));
intent.putExtra(EXTRA_APP, app); intent.putExtra(EXTRA_APP, app);
intent.putExtra(EXTRA_APK, apk); intent.putExtra(EXTRA_APK, apk);
context.startService(intent); context.startService(intent);
} }
public static void cancel(Context context, String urlString) { public static void cancel(Context context, String canonicalUrl) {
removePendingInstall(context, urlString); removePendingInstall(context, canonicalUrl);
Intent intent = new Intent(context, InstallManagerService.class); Intent intent = new Intent(context, InstallManagerService.class);
intent.setAction(ACTION_CANCEL); intent.setAction(ACTION_CANCEL);
intent.setData(Uri.parse(urlString)); intent.setData(Uri.parse(canonicalUrl));
context.startService(intent); context.startService(intent);
} }
@ -491,29 +490,29 @@ public class InstallManagerService extends Service {
* completed, or the device lost power in the middle of the install * completed, or the device lost power in the middle of the install
* process. * process.
*/ */
public boolean isPendingInstall(String urlString) { public boolean isPendingInstall(String canonicalUrl) {
return pendingInstalls.contains(urlString); return pendingInstalls.contains(canonicalUrl);
} }
/** /**
* Mark a given APK as in the process of being installed, with * Mark a given APK as in the process of being installed, with
* the {@code urlString} of the download used as the unique ID, * the {@code canonicalUrl} of the download used as the unique ID,
* and the file hash used to verify that things are the same. * and the file hash used to verify that things are the same.
* *
* @see #isPendingInstall(String) * @see #isPendingInstall(String)
*/ */
public static void putPendingInstall(Context context, String urlString, String packageName) { public static void putPendingInstall(Context context, String canonicalUrl, String packageName) {
if (pendingInstalls == null) { if (pendingInstalls == null) {
pendingInstalls = getPendingInstalls(context); pendingInstalls = getPendingInstalls(context);
} }
pendingInstalls.edit().putString(urlString, packageName).apply(); pendingInstalls.edit().putString(canonicalUrl, packageName).apply();
} }
public static void removePendingInstall(Context context, String urlString) { public static void removePendingInstall(Context context, String canonicalUrl) {
if (pendingInstalls == null) { if (pendingInstalls == null) {
pendingInstalls = getPendingInstalls(context); pendingInstalls = getPendingInstalls(context);
} }
pendingInstalls.edit().remove(urlString).apply(); pendingInstalls.edit().remove(canonicalUrl).apply();
} }
private static SharedPreferences getPendingInstalls(Context context) { private static SharedPreferences getPendingInstalls(Context context) {

View File

@ -64,15 +64,6 @@ public abstract class Installer {
public static final String ACTION_UNINSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_INTERRUPTED"; public static final String ACTION_UNINSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_INTERRUPTED";
public static final String ACTION_UNINSTALL_USER_INTERACTION = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_USER_INTERACTION"; public static final String ACTION_UNINSTALL_USER_INTERACTION = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_USER_INTERACTION";
/**
* The URI where the APK was originally downloaded from. This is also used
* as the unique ID representing this in the whole install process in
* {@link InstallManagerService}, there is is generally known as the
* "download URL" since it is the URL used to download the APK.
*
* @see Intent#EXTRA_ORIGINATING_URI
*/
static final String EXTRA_DOWNLOAD_URI = "org.fdroid.fdroid.installer.Installer.extra.DOWNLOAD_URI";
public static final String EXTRA_APK = "org.fdroid.fdroid.installer.Installer.extra.APK"; public static final String EXTRA_APK = "org.fdroid.fdroid.installer.Installer.extra.APK";
public static final String EXTRA_USER_INTERACTION_PI = "org.fdroid.fdroid.installer.Installer.extra.USER_INTERACTION_PI"; public static final String EXTRA_USER_INTERACTION_PI = "org.fdroid.fdroid.installer.Installer.extra.USER_INTERACTION_PI";
public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.installer.Installer.extra.ERROR_MESSAGE"; public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.installer.Installer.extra.ERROR_MESSAGE";
@ -165,23 +156,23 @@ public abstract class Installer {
return intent; return intent;
} }
void sendBroadcastInstall(Uri downloadUri, String action, PendingIntent pendingIntent) { void sendBroadcastInstall(Uri canonicalUri, String action, PendingIntent pendingIntent) {
sendBroadcastInstall(context, downloadUri, action, apk, pendingIntent, null); sendBroadcastInstall(context, canonicalUri, action, apk, pendingIntent, null);
} }
void sendBroadcastInstall(Uri downloadUri, String action) { void sendBroadcastInstall(Uri canonicalUri, String action) {
sendBroadcastInstall(context, downloadUri, action, apk, null, null); sendBroadcastInstall(context, canonicalUri, action, apk, null, null);
} }
void sendBroadcastInstall(Uri downloadUri, String action, String errorMessage) { void sendBroadcastInstall(Uri canonicalUri, String action, String errorMessage) {
sendBroadcastInstall(context, downloadUri, action, apk, null, errorMessage); sendBroadcastInstall(context, canonicalUri, action, apk, null, errorMessage);
} }
static void sendBroadcastInstall(Context context, static void sendBroadcastInstall(Context context,
Uri downloadUri, String action, Apk apk, Uri canonicalUri, String action, Apk apk,
PendingIntent pendingIntent, String errorMessage) { PendingIntent pendingIntent, String errorMessage) {
Intent intent = new Intent(action); Intent intent = new Intent(action);
intent.setData(downloadUri); intent.setData(canonicalUri);
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent); intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
intent.putExtra(Installer.EXTRA_APK, apk); intent.putExtra(Installer.EXTRA_APK, apk);
if (!TextUtils.isEmpty(errorMessage)) { if (!TextUtils.isEmpty(errorMessage)) {
@ -226,20 +217,34 @@ public abstract class Installer {
/** /**
* Gets an {@link IntentFilter} for matching events from the install * Gets an {@link IntentFilter} for matching events from the install
* process based on the original download URL as a {@link Uri}. * process based on {@code canonicalUri}, which is the global unique
* ID for a package going through the install process.
*
* @see InstallManagerService for more about {@code canonicalUri}
*/ */
public static IntentFilter getInstallIntentFilter(Uri uri) { public static IntentFilter getInstallIntentFilter(Uri canonicalUri) {
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Installer.ACTION_INSTALL_STARTED); intentFilter.addAction(Installer.ACTION_INSTALL_STARTED);
intentFilter.addAction(Installer.ACTION_INSTALL_COMPLETE); intentFilter.addAction(Installer.ACTION_INSTALL_COMPLETE);
intentFilter.addAction(Installer.ACTION_INSTALL_INTERRUPTED); intentFilter.addAction(Installer.ACTION_INSTALL_INTERRUPTED);
intentFilter.addAction(Installer.ACTION_INSTALL_USER_INTERACTION); intentFilter.addAction(Installer.ACTION_INSTALL_USER_INTERACTION);
intentFilter.addDataScheme(uri.getScheme()); intentFilter.addDataScheme(canonicalUri.getScheme());
intentFilter.addDataAuthority(uri.getHost(), String.valueOf(uri.getPort())); intentFilter.addDataAuthority(canonicalUri.getHost(), String.valueOf(canonicalUri.getPort()));
intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL); intentFilter.addDataPath(canonicalUri.getPath(), PatternMatcher.PATTERN_LITERAL);
return intentFilter; return intentFilter;
} }
/**
* Gets an {@link IntentFilter} for matching events from the install
* process based on {@code canonicalUrl}, which is the global unique
* ID for a package going through the install process.
*
* @see InstallManagerService for more about {@code canonicalUrl}
*/
public static IntentFilter getInstallIntentFilter(String canonicalUrl) {
return getInstallIntentFilter(Uri.parse(canonicalUrl));
}
public static IntentFilter getUninstallIntentFilter(String packageName) { public static IntentFilter getUninstallIntentFilter(String packageName) {
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Installer.ACTION_UNINSTALL_STARTED); intentFilter.addAction(Installer.ACTION_UNINSTALL_STARTED);
@ -262,20 +267,20 @@ public abstract class Installer {
* is prompted with the system installer dialog, which shows all the * is prompted with the system installer dialog, which shows all the
* permissions that the APK is requesting. * permissions that the APK is requesting.
* *
* @param localApkUri points to the local copy of the APK to be installed * @param localApkUri points to the local copy of the APK to be installed
* @param downloadUri serves as the unique ID for all actions related to the * @param canonicalUri serves as the unique ID for all actions related to the
* installation of that specific APK * installation of that specific APK
* @see InstallManagerService * @see InstallManagerService
* @see <a href="https://issuetracker.google.com/issues/37091886">ACTION_INSTALL_PACKAGE Fails For Any Possible Uri</a> * @see <a href="https://issuetracker.google.com/issues/37091886">ACTION_INSTALL_PACKAGE Fails For Any Possible Uri</a>
*/ */
public void installPackage(Uri localApkUri, Uri downloadUri) { public void installPackage(Uri localApkUri, Uri canonicalUri) {
Uri sanitizedUri; Uri sanitizedUri;
try { try {
sanitizedUri = ApkFileProvider.getSafeUri(context, localApkUri, apk); sanitizedUri = ApkFileProvider.getSafeUri(context, localApkUri, apk);
} catch (IOException e) { } catch (IOException e) {
Utils.debugLog(TAG, e.getMessage(), e); Utils.debugLog(TAG, e.getMessage(), e);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage()); sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage());
return; return;
} }
@ -285,7 +290,7 @@ public abstract class Installer {
apkVerifier.verifyApk(); apkVerifier.verifyApk();
} catch (ApkVerifier.ApkVerificationException e) { } catch (ApkVerifier.ApkVerificationException e) {
Utils.debugLog(TAG, e.getMessage(), e); Utils.debugLog(TAG, e.getMessage(), e);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage()); sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage());
return; return;
} catch (ApkVerifier.ApkPermissionUnequalException e) { } catch (ApkVerifier.ApkPermissionUnequalException e) {
// if permissions of apk are not the ones listed in the repo // if permissions of apk are not the ones listed in the repo
@ -295,15 +300,15 @@ public abstract class Installer {
Utils.debugLog(TAG, e.getMessage(), e); Utils.debugLog(TAG, e.getMessage(), e);
Utils.debugLog(TAG, "Falling back to AOSP DefaultInstaller!"); Utils.debugLog(TAG, "Falling back to AOSP DefaultInstaller!");
DefaultInstaller defaultInstaller = new DefaultInstaller(context, apk); DefaultInstaller defaultInstaller = new DefaultInstaller(context, apk);
defaultInstaller.installPackageInternal(sanitizedUri, downloadUri); defaultInstaller.installPackageInternal(sanitizedUri, canonicalUri);
return; return;
} }
} }
installPackageInternal(sanitizedUri, downloadUri); installPackageInternal(sanitizedUri, canonicalUri);
} }
protected abstract void installPackageInternal(Uri localApkUri, Uri downloadUri); protected abstract void installPackageInternal(Uri localApkUri, Uri canonicalUri);
/** /**
* Uninstall app as defined by {@link Installer#apk} in * Uninstall app as defined by {@link Installer#apk} in

View File

@ -28,10 +28,10 @@ import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService; import android.support.v4.app.JobIntentService;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.views.AppDetailsActivity;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.AppDetailsActivity;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
@ -74,8 +74,8 @@ public class InstallerService extends JobIntentService {
if (ACTION_INSTALL.equals(intent.getAction())) { if (ACTION_INSTALL.equals(intent.getAction())) {
Uri uri = intent.getData(); Uri uri = intent.getData();
Uri downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI); Uri canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
installer.installPackage(uri, downloadUri); installer.installPackage(uri, canonicalUri);
} else if (ACTION_UNINSTALL.equals(intent.getAction())) { } else if (ACTION_UNINSTALL.equals(intent.getAction())) {
installer.uninstallPackage(); installer.uninstallPackage();
new Thread() { new Thread() {
@ -111,19 +111,20 @@ public class InstallerService extends JobIntentService {
* {@link #uninstall(Context, Apk)} since this is called in one place where * {@link #uninstall(Context, Apk)} since this is called in one place where
* the input has already been validated. * the input has already been validated.
* *
* @param context this app's {@link Context} * @param context this app's {@link Context}
* @param localApkUri {@link Uri} pointing to (downloaded) local apk file * @param localApkUri {@link Uri} pointing to (downloaded) local apk file
* @param downloadUri {@link Uri} where the apk has been downloaded from * @param canonicalUri {@link Uri} used as the global unique ID for the package
* @param apk apk object of app that should be installed * @param apk apk object of app that should be installed
* @see #uninstall(Context, Apk) * @see #uninstall(Context, Apk)
* @see InstallManagerService
*/ */
public static void install(Context context, Uri localApkUri, Uri downloadUri, Apk apk) { public static void install(Context context, Uri localApkUri, Uri canonicalUri, Apk apk) {
Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk, Installer.sendBroadcastInstall(context, canonicalUri, Installer.ACTION_INSTALL_STARTED, apk,
null, null); null, null);
Intent intent = new Intent(context, InstallerService.class); Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_INSTALL); intent.setAction(ACTION_INSTALL);
intent.setData(localApkUri); intent.setData(localApkUri);
intent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri); intent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri);
intent.putExtra(Installer.EXTRA_APK, apk); intent.putExtra(Installer.EXTRA_APK, apk);
enqueueWork(context, intent); enqueueWork(context, intent);
} }

View File

@ -308,7 +308,7 @@ public class PrivilegedInstaller extends Installer {
} }
@Override @Override
protected void installPackageInternal(final Uri localApkUri, final Uri downloadUri) { protected void installPackageInternal(final Uri localApkUri, final Uri canonicalUri) {
ServiceConnection mServiceConnection = new ServiceConnection() { ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceConnected(ComponentName name, IBinder service) {
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service); IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
@ -317,9 +317,9 @@ public class PrivilegedInstaller extends Installer {
@Override @Override
public void handleResult(String packageName, int returnCode) throws RemoteException { public void handleResult(String packageName, int returnCode) throws RemoteException {
if (returnCode == INSTALL_SUCCEEDED) { if (returnCode == INSTALL_SUCCEEDED) {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_COMPLETE); sendBroadcastInstall(canonicalUri, ACTION_INSTALL_COMPLETE);
} else { } else {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED, sendBroadcastInstall(canonicalUri, ACTION_INSTALL_INTERRUPTED,
"Error " + returnCode + ": " "Error " + returnCode + ": "
+ INSTALL_RETURN_CODES.get(returnCode)); + INSTALL_RETURN_CODES.get(returnCode));
} }
@ -329,7 +329,7 @@ public class PrivilegedInstaller extends Installer {
try { try {
boolean hasPermissions = privService.hasPrivilegedPermissions(); boolean hasPermissions = privService.hasPrivilegedPermissions();
if (!hasPermissions) { if (!hasPermissions) {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED, sendBroadcastInstall(canonicalUri, ACTION_INSTALL_INTERRUPTED,
context.getString(R.string.system_install_denied_permissions)); context.getString(R.string.system_install_denied_permissions));
return; return;
} }
@ -338,7 +338,7 @@ public class PrivilegedInstaller extends Installer {
null, callback); null, callback);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.e(TAG, "RemoteException", e); Log.e(TAG, "RemoteException", e);
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED, sendBroadcastInstall(canonicalUri, ACTION_INSTALL_INTERRUPTED,
"connecting to privileged service failed"); "connecting to privileged service failed");
} }
} }

View File

@ -29,9 +29,16 @@ public abstract class Downloader {
public static final String EXTRA_BYTES_READ = "org.fdroid.fdroid.net.Downloader.extra.BYTES_READ"; public static final String EXTRA_BYTES_READ = "org.fdroid.fdroid.net.Downloader.extra.BYTES_READ";
public static final String EXTRA_TOTAL_BYTES = "org.fdroid.fdroid.net.Downloader.extra.TOTAL_BYTES"; public static final String EXTRA_TOTAL_BYTES = "org.fdroid.fdroid.net.Downloader.extra.TOTAL_BYTES";
public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MESSAGE"; public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MESSAGE";
public static final String EXTRA_REPO_ID = "org.fdroid.fdroid.net.Downloader.extra.ERROR_REPO_ID"; public static final String EXTRA_REPO_ID = "org.fdroid.fdroid.net.Downloader.extra.REPO_ID";
public static final String EXTRA_CANONICAL_URL = "org.fdroid.fdroid.net.Downloader.extra.ERROR_CANONICAL_URL"; public static final String EXTRA_MIRROR_URL = "org.fdroid.fdroid.net.Downloader.extra.MIRROR_URL";
public static final String EXTRA_MIRROR_URL = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MIRROR_URL"; /**
* Unique ID used to represent this specific package's install process,
* including {@link android.app.Notification}s, also known as {@code canonicalUrl}.
*
* @see org.fdroid.fdroid.installer.InstallManagerService
* @see android.content.Intent#EXTRA_ORIGINATING_URI
*/
public static final String EXTRA_CANONICAL_URL = "org.fdroid.fdroid.net.Downloader.extra.CANONICAL_URL";
public static final int DEFAULT_TIMEOUT = 10000; public static final int DEFAULT_TIMEOUT = 10000;
public static final int SECOND_TIMEOUT = (int) DateUtils.MINUTE_IN_MILLIS; public static final int SECOND_TIMEOUT = (int) DateUtils.MINUTE_IN_MILLIS;
@ -204,10 +211,16 @@ public abstract class Downloader {
* Send progress updates on a timer to avoid flooding receivers with pointless events. * Send progress updates on a timer to avoid flooding receivers with pointless events.
*/ */
private final TimerTask progressTask = new TimerTask() { private final TimerTask progressTask = new TimerTask() {
private long lastBytesRead = Long.MIN_VALUE;
private long lastTotalBytes = Long.MIN_VALUE;
@Override @Override
public void run() { public void run() {
if (downloaderProgressListener != null) { if (downloaderProgressListener != null
downloaderProgressListener.onProgress(urlString, bytesRead, totalBytes); && (bytesRead != lastBytesRead || totalBytes != lastTotalBytes)) {
downloaderProgressListener.onProgress(bytesRead, totalBytes);
lastBytesRead = bytesRead;
lastTotalBytes = totalBytes;
} }
} }
}; };

View File

@ -32,9 +32,13 @@ import android.os.Process;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.LogPrinter;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.installer.ApkCache; import org.fdroid.fdroid.installer.ApkCache;
@ -73,17 +77,18 @@ import java.net.UnknownHostException;
* long as necessary (and will not block the application's main loop), but * long as necessary (and will not block the application's main loop), but
* only one request will be processed at a time. * only one request will be processed at a time.
* <p> * <p>
* The full URL for the file to download is also used as the unique ID to * The Canonical URL for the file to download is also used as the unique ID to
* represent the download itself throughout F-Droid. This follows the model * 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 * of {@link Intent#setData(Uri)}, where the core data of an {@code Intent} is
* a {@code Uri}. For places that need an {@code int} ID, * a {@code Uri}. For places that need an {@code int} ID,
* {@link String#hashCode()} should be used to get a reproducible, unique {@code int} * {@link String#hashCode()} should be used to get a reproducible, unique {@code int}
* from any {@code urlString}. The full URL is guaranteed to be unique since * from any {@code canonicalUrl}. That full URL is guaranteed to be unique since
* it points to a file on a filesystem. This is more important with media files * it points to a file on a filesystem. This is more important with media files
* than with APKs since there is not reliable standard for a unique ID for * than with APKs since there is not reliable standard for a unique ID for
* media files, unlike APKs with {@code packageName} and {@code versionCode}. * media files, unlike APKs with {@code packageName} and {@code versionCode}.
* *
* @see android.app.IntentService * @see android.app.IntentService
* @see org.fdroid.fdroid.installer.InstallManagerService
*/ */
public class DownloaderService extends Service { public class DownloaderService extends Service {
private static final String TAG = "DownloaderService"; private static final String TAG = "DownloaderService";
@ -94,10 +99,13 @@ public class DownloaderService extends Service {
private volatile Looper serviceLooper; private volatile Looper serviceLooper;
private static volatile ServiceHandler serviceHandler; private static volatile ServiceHandler serviceHandler;
private static volatile Downloader downloader; private static volatile Downloader downloader;
private static volatile String activeCanonicalUrl;
private LocalBroadcastManager localBroadcastManager; private LocalBroadcastManager localBroadcastManager;
private static volatile int timeout; private static volatile int timeout;
private final class ServiceHandler extends Handler { private final class ServiceHandler extends Handler {
static final String TAG = "ServiceHandler";
ServiceHandler(Looper looper) { ServiceHandler(Looper looper) {
super(looper); super(looper);
} }
@ -119,6 +127,9 @@ public class DownloaderService extends Service {
thread.start(); thread.start();
serviceLooper = thread.getLooper(); serviceLooper = thread.getLooper();
if (BuildConfig.DEBUG) {
serviceLooper.setMessageLogging(new LogPrinter(Log.DEBUG, ServiceHandler.TAG));
}
serviceHandler = new ServiceHandler(serviceLooper); serviceHandler = new ServiceHandler(serviceLooper);
localBroadcastManager = LocalBroadcastManager.getInstance(this); localBroadcastManager = LocalBroadcastManager.getInstance(this);
} }
@ -131,21 +142,27 @@ public class DownloaderService extends Service {
return START_NOT_STICKY; return START_NOT_STICKY;
} }
String uriString = intent.getDataString(); String downloadUrl = intent.getDataString();
if (uriString == null) { if (downloadUrl == null) {
Utils.debugLog(TAG, "Received Intent with no URI: " + intent); Utils.debugLog(TAG, "Received Intent with no URI: " + intent);
return START_NOT_STICKY; return START_NOT_STICKY;
} }
String canonicalUrl = intent.getStringExtra(Downloader.EXTRA_CANONICAL_URL);
if (canonicalUrl == null) {
Utils.debugLog(TAG, "Received Intent with no EXTRA_CANONICAL_URL: " + intent);
return START_NOT_STICKY;
}
if (ACTION_CANCEL.equals(intent.getAction())) { if (ACTION_CANCEL.equals(intent.getAction())) {
Utils.debugLog(TAG, "Cancelling download of " + uriString); Utils.debugLog(TAG, "Cancelling download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
Integer whatToRemove = uriString.hashCode(); + " downloading from " + downloadUrl);
Integer whatToRemove = canonicalUrl.hashCode();
if (serviceHandler.hasMessages(whatToRemove)) { if (serviceHandler.hasMessages(whatToRemove)) {
Utils.debugLog(TAG, "Removing download with ID of " + whatToRemove Utils.debugLog(TAG, "Removing download with ID of " + whatToRemove
+ " from service handler, then sending interrupted event."); + " from service handler, then sending interrupted event.");
serviceHandler.removeMessages(whatToRemove); serviceHandler.removeMessages(whatToRemove);
sendBroadcast(intent.getData(), Downloader.ACTION_INTERRUPTED); sendCancelledBroadcast(intent.getData(), canonicalUrl);
} else if (isActive(uriString)) { } else if (isActive(canonicalUrl)) {
downloader.cancelDownload(); downloader.cancelDownload();
} else { } else {
Utils.debugLog(TAG, "ACTION_CANCEL called on something not queued or running" Utils.debugLog(TAG, "ACTION_CANCEL called on something not queued or running"
@ -155,9 +172,10 @@ public class DownloaderService extends Service {
Message msg = serviceHandler.obtainMessage(); Message msg = serviceHandler.obtainMessage();
msg.arg1 = startId; msg.arg1 = startId;
msg.obj = intent; msg.obj = intent;
msg.what = uriString.hashCode(); msg.what = canonicalUrl.hashCode();
serviceHandler.sendMessage(msg); serviceHandler.sendMessage(msg);
Utils.debugLog(TAG, "Queued download of " + uriString); Utils.debugLog(TAG, "Queued download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
+ " using " + downloadUrl);
} else { } else {
Utils.debugLog(TAG, "Received Intent with unknown action: " + intent); Utils.debugLog(TAG, "Received Intent with unknown action: " + intent);
} }
@ -198,18 +216,19 @@ public class DownloaderService extends Service {
*/ */
private void handleIntent(Intent intent) { private void handleIntent(Intent intent) {
final Uri uri = intent.getData(); final Uri uri = intent.getData();
long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0); final long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0);
String canonicalUrlString = intent.getStringExtra(Downloader.EXTRA_CANONICAL_URL); final Uri canonicalUrl = Uri.parse(intent.getStringExtra(Downloader.EXTRA_CANONICAL_URL));
final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, canonicalUrlString); final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, canonicalUrl);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile, repoId, canonicalUrlString); sendBroadcast(uri, Downloader.ACTION_STARTED, localFile, repoId, canonicalUrl);
try { try {
activeCanonicalUrl = canonicalUrl.toString();
downloader = DownloaderFactory.create(this, uri, localFile); downloader = DownloaderFactory.create(this, uri, localFile);
downloader.setListener(new ProgressListener() { downloader.setListener(new ProgressListener() {
@Override @Override
public void onProgress(String urlString, long bytesRead, long totalBytes) { public void onProgress(long bytesRead, long totalBytes) {
Intent intent = new Intent(Downloader.ACTION_PROGRESS); Intent intent = new Intent(Downloader.ACTION_PROGRESS);
intent.setData(uri); intent.setData(canonicalUrl);
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead); intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes); intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes);
localBroadcastManager.sendBroadcast(intent); localBroadcastManager.sendBroadcast(intent);
@ -219,47 +238,44 @@ public class DownloaderService extends Service {
downloader.download(); downloader.download();
if (downloader.isNotFound()) { if (downloader.isNotFound()) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, getString(R.string.download_404), sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, getString(R.string.download_404),
repoId, canonicalUrlString); repoId, canonicalUrl);
} else { } else {
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile, repoId, canonicalUrlString); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile, repoId, canonicalUrl);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, repoId, canonicalUrlString); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, repoId, canonicalUrl);
} catch (ConnectException | HttpRetryException | NoRouteToHostException | SocketTimeoutException } catch (ConnectException | HttpRetryException | NoRouteToHostException | SocketTimeoutException
| SSLHandshakeException | SSLKeyException | SSLPeerUnverifiedException | SSLProtocolException | SSLHandshakeException | SSLKeyException | SSLPeerUnverifiedException | SSLProtocolException
| ProtocolException | UnknownHostException e) { | ProtocolException | UnknownHostException e) {
// if the above list of exceptions changes, also change it in IndexV1Updater.update() // if the above list of exceptions changes, also change it in IndexV1Updater.update()
Log.e(TAG, e.getLocalizedMessage()); Log.e(TAG, e.getLocalizedMessage());
sendBroadcast(uri, Downloader.ACTION_CONNECTION_FAILED, localFile, repoId, canonicalUrlString); sendBroadcast(uri, Downloader.ACTION_CONNECTION_FAILED, localFile, repoId, canonicalUrl);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile,
e.getLocalizedMessage(), repoId, canonicalUrlString); e.getLocalizedMessage(), repoId, canonicalUrl);
} finally { } finally {
if (downloader != null) { if (downloader != null) {
downloader.close(); downloader.close();
} }
} }
downloader = null; downloader = null;
activeCanonicalUrl = null;
} }
private void sendBroadcast(Uri uri, String action) { private void sendCancelledBroadcast(Uri uri, String canonicalUrl) {
sendBroadcast(uri, action, null, null); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, null, 0, Uri.parse(canonicalUrl));
} }
private void sendBroadcast(Uri uri, String action, File file, long repoId, String originalUrlString) { private void sendBroadcast(Uri uri, String action, File file, long repoId, Uri canonicalUrl) {
sendBroadcast(uri, action, file, null, repoId, originalUrlString); sendBroadcast(uri, action, file, null, repoId, canonicalUrl);
}
private void sendBroadcast(Uri uri, String action, File file, String errorMessage) {
sendBroadcast(uri, action, file, errorMessage, 0, null);
} }
private void sendBroadcast(Uri uri, String action, File file, String errorMessage, long repoId, private void sendBroadcast(Uri uri, String action, File file, String errorMessage, long repoId,
String originalUrlString) { Uri canonicalUrl) {
Intent intent = new Intent(action); Intent intent = new Intent(action);
if (originalUrlString != null) { if (canonicalUrl != null) {
intent.setData(Uri.parse(originalUrlString)); intent.setData(canonicalUrl);
} }
if (file != null) { if (file != null) {
intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath()); intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
@ -277,42 +293,73 @@ public class DownloaderService extends Service {
* <p> * <p>
* All notifications are sent as an {@link Intent} via local broadcasts to be received by * All notifications are sent as an {@link Intent} via local broadcasts to be received by
* *
* @param context this app's {@link Context} * @param context this app's {@link Context}
* @param mirrorUrlString The URL to add to the download queue * @param mirrorUrl The URL to add to the download queue
* @param repoId the database ID number representing one repo * @param repoId the database ID number representing one repo
* @param urlString the URL used as the unique ID throughout F-Droid * @param canonicalUrl the URL used as the unique ID throughout F-Droid
* @see #cancel(Context, String) * @see #cancel(Context, String)
*/ */
public static void queue(Context context, String mirrorUrlString, long repoId, String urlString) { public static void queue(Context context, String mirrorUrl, long repoId, String canonicalUrl) {
if (TextUtils.isEmpty(mirrorUrlString)) { if (TextUtils.isEmpty(mirrorUrl)) {
return; return;
} }
Utils.debugLog(TAG, "Preparing " + mirrorUrlString + " to go into the download queue"); Utils.debugLog(TAG, "Queue download " + canonicalUrl.hashCode() + "/" + canonicalUrl
+ " using " + mirrorUrl);
Intent intent = new Intent(context, DownloaderService.class); Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_QUEUE); intent.setAction(ACTION_QUEUE);
intent.setData(Uri.parse(mirrorUrlString)); intent.setData(Uri.parse(mirrorUrl));
intent.putExtra(Downloader.EXTRA_REPO_ID, repoId); intent.putExtra(Downloader.EXTRA_REPO_ID, repoId);
intent.putExtra(Downloader.EXTRA_CANONICAL_URL, urlString); intent.putExtra(Downloader.EXTRA_CANONICAL_URL, canonicalUrl);
context.startService(intent); context.startService(intent);
} }
/**
* Add a package to the download queue, choosing a random mirror to
* download from.
*
* @param canonicalUrl the URL used as the unique ID throughout F-Droid,
* needed here to support canceling active downloads
*/
public static void queueUsingRandomMirror(Context context, long repoId, String canonicalUrl) {
String mirrorUrl = FDroidApp.switchUrlToNewMirror(canonicalUrl,
RepoProvider.Helper.findById(context, repoId));
queue(context, mirrorUrl, repoId, canonicalUrl);
}
/**
* Tries to return a version of {@code urlString} from a mirror, if there
* is an error, it just returns {@code urlString}.
*
* @see FDroidApp#getNewMirrorOnError(String, org.fdroid.fdroid.data.Repo)
*/
public static void queueUsingDifferentMirror(Context context, long repoId, String canonicalUrl) {
try {
String mirrorUrl = FDroidApp.getNewMirrorOnError(canonicalUrl,
RepoProvider.Helper.findById(context, repoId));
queue(context, mirrorUrl, repoId, canonicalUrl);
} catch (IOException e) {
queue(context, canonicalUrl, repoId, canonicalUrl);
}
}
/** /**
* Remove a URL to the download queue, even if it is currently downloading. * Remove a URL to the download queue, even if it is currently downloading.
* <p> * <p>
* All notifications are sent as an {@link Intent} via local broadcasts to be received by * All notifications are sent as an {@link Intent} via local broadcasts to be received by
* *
* @param context this app's {@link Context} * @param context this app's {@link Context}
* @param urlString The URL to remove from the download queue * @param canonicalUrl The URL to remove from the download queue
* @see #queue(Context, String, long, String) * @see #queue(Context, String, long, String)
*/ */
public static void cancel(Context context, String urlString) { public static void cancel(Context context, String canonicalUrl) {
if (TextUtils.isEmpty(urlString)) { if (TextUtils.isEmpty(canonicalUrl)) {
return; return;
} }
Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download"); Utils.debugLog(TAG, "Send cancel for " + canonicalUrl.hashCode() + "/" + canonicalUrl);
Intent intent = new Intent(context, DownloaderService.class); Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_CANCEL); intent.setAction(ACTION_CANCEL);
intent.setData(Uri.parse(urlString)); intent.setData(Uri.parse(canonicalUrl));
intent.putExtra(Downloader.EXTRA_CANONICAL_URL, canonicalUrl);
context.startService(intent); context.startService(intent);
} }
@ -321,21 +368,21 @@ public class DownloaderService extends Service {
* This is useful for checking whether to re-register {@link android.content.BroadcastReceiver}s * This is useful for checking whether to re-register {@link android.content.BroadcastReceiver}s
* in {@link android.app.Activity#onResume()}. * in {@link android.app.Activity#onResume()}.
*/ */
public static boolean isQueuedOrActive(String urlString) { public static boolean isQueuedOrActive(String canonicalUrl) {
if (TextUtils.isEmpty(urlString)) { //NOPMD - suggests unreadable format if (TextUtils.isEmpty(canonicalUrl)) { //NOPMD - suggests unreadable format
return false; return false;
} }
if (serviceHandler == null) { if (serviceHandler == null) {
return false; // this service is not even running return false; // this service is not even running
} }
return serviceHandler.hasMessages(urlString.hashCode()) || isActive(urlString); return serviceHandler.hasMessages(canonicalUrl.hashCode()) || isActive(canonicalUrl);
} }
/** /**
* Check if a URL is actively being downloaded. * Check if a URL is actively being downloaded.
*/ */
private static boolean isActive(String urlString) { private static boolean isActive(String downloadUrl) {
return downloader != null && TextUtils.equals(urlString, downloader.urlString); return downloader != null && TextUtils.equals(downloadUrl, activeCanonicalUrl);
} }
public static void setTimeout(int ms) { public static void setTimeout(int ms) {
@ -345,10 +392,10 @@ public class DownloaderService extends Service {
/** /**
* Get a prepared {@link IntentFilter} for use for matching this service's action events. * Get a prepared {@link IntentFilter} for use for matching this service's action events.
* *
* @param urlString The full file URL to match. * @param canonicalUrl the URL used as the unique ID for the specific package
*/ */
public static IntentFilter getIntentFilter(String urlString) { public static IntentFilter getIntentFilter(String canonicalUrl) {
Uri uri = Uri.parse(urlString); Uri uri = Uri.parse(canonicalUrl);
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Downloader.ACTION_STARTED); intentFilter.addAction(Downloader.ACTION_STARTED);
intentFilter.addAction(Downloader.ACTION_PROGRESS); intentFilter.addAction(Downloader.ACTION_PROGRESS);

View File

@ -171,9 +171,9 @@ public class AppDetailsActivity extends AppCompatActivity
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this); AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(app.packageName)) { for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(app.packageName)) {
if (status.status == AppUpdateStatusManager.Status.Installed) { if (status.status == AppUpdateStatusManager.Status.Installed) {
ausm.removeApk(status.getUniqueKey()); ausm.removeApk(status.getCanonicalUrl());
} else { } else {
ausm.refreshApk(status.getUniqueKey()); ausm.refreshApk(status.getCanonicalUrl());
} }
} }
} }
@ -449,7 +449,7 @@ public class AppDetailsActivity extends AppCompatActivity
if (justReceived) { if (justReceived) {
adapter.setIndeterminateProgress(R.string.installing); adapter.setIndeterminateProgress(R.string.installing);
localBroadcastManager.registerReceiver(installReceiver, localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(Uri.parse(newStatus.getUniqueKey()))); Installer.getInstallIntentFilter(newStatus.getCanonicalUrl()));
} }
break; break;
@ -459,7 +459,7 @@ public class AppDetailsActivity extends AppCompatActivity
Toast.makeText(this, R.string.details_notinstalled, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
} else { } else {
String msg = newStatus.errorText; String msg = newStatus.errorText;
if (!newStatus.getUniqueKey().equals(msg)) msg += " " + newStatus.getUniqueKey(); if (!newStatus.getCanonicalUrl().equals(msg)) msg += " " + newStatus.getCanonicalUrl();
Toast.makeText(this, R.string.download_error, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.download_error, Toast.LENGTH_SHORT).show();
Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
} }
@ -491,9 +491,9 @@ public class AppDetailsActivity extends AppCompatActivity
AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED); AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
if (currentStatus != null if (currentStatus != null
&& isRemoving && isRemoving
&& !TextUtils.equals(status.getUniqueKey(), currentStatus.getUniqueKey())) { && !TextUtils.equals(status.getCanonicalUrl(), currentStatus.getCanonicalUrl())) {
Utils.debugLog(TAG, "Ignoring app status change because it belongs to " Utils.debugLog(TAG, "Ignoring app status change because it belongs to "
+ status.getUniqueKey() + " not " + currentStatus.getUniqueKey()); + status.getCanonicalUrl() + " not " + currentStatus.getCanonicalUrl());
} else if (status != null && !TextUtils.equals(status.apk.packageName, app.packageName)) { } else if (status != null && !TextUtils.equals(status.apk.packageName, app.packageName)) {
Utils.debugLog(TAG, "Ignoring app status change because it belongs to " Utils.debugLog(TAG, "Ignoring app status change because it belongs to "
+ status.apk.packageName + " not " + app.packageName); + status.apk.packageName + " not " + app.packageName);
@ -650,7 +650,7 @@ public class AppDetailsActivity extends AppCompatActivity
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this); AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) { for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
if (status.status == AppUpdateStatusManager.Status.Installed) { if (status.status == AppUpdateStatusManager.Status.Installed) {
ausm.removeApk(status.getUniqueKey()); ausm.removeApk(status.getCanonicalUrl());
} }
} }
if (app == null) { if (app == null) {
@ -720,7 +720,7 @@ public class AppDetailsActivity extends AppCompatActivity
@Override @Override
public void installCancel() { public void installCancel() {
if (currentStatus != null) { if (currentStatus != null) {
InstallManagerService.cancel(this, currentStatus.getUniqueKey()); InstallManagerService.cancel(this, currentStatus.getCanonicalUrl());
} }
} }

View File

@ -477,15 +477,15 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
// Once it is explicitly launched by the user, then we can pretty much forget about // Once it is explicitly launched by the user, then we can pretty much forget about
// any sort of notification that the app was successfully installed. It should be // any sort of notification that the app was successfully installed. It should be
// apparent to the user because they just launched it. // apparent to the user because they just launched it.
AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getUniqueKey()); AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getCanonicalUrl());
} }
return; return;
} }
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) { if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
String urlString = currentStatus.apk.getUrl(); String canonicalUrl = currentStatus.apk.getCanonicalUrl();
File apkFilePath = ApkCache.getApkDownloadPath(activity, urlString); File apkFilePath = ApkCache.getApkDownloadPath(activity, canonicalUrl);
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() + Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getCanonicalUrl() +
" to " + apkFilePath); " to " + apkFilePath);
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity); final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity);
@ -505,10 +505,10 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
} }
}; };
Uri apkDownloadUri = Uri.parse(urlString); Uri canonicalUri = Uri.parse(canonicalUrl);
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(apkDownloadUri)); broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(canonicalUri));
Installer installer = InstallerFactory.create(activity, currentStatus.apk); Installer installer = InstallerFactory.create(activity, currentStatus.apk);
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), apkDownloadUri); installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), canonicalUri);
} else { } else {
final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app);
InstallManagerService.queue(activity, app, suggestedApk); InstallManagerService.queue(activity, app, suggestedApk);
@ -534,6 +534,6 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
return; return;
} }
InstallManagerService.cancel(activity, currentStatus.getUniqueKey()); InstallManagerService.cancel(activity, currentStatus.getCanonicalUrl());
} }
} }

View File

@ -479,7 +479,8 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
} }
// Check if we have moved into the ReadyToInstall or Installed state. // Check if we have moved into the ReadyToInstall or Installed state.
AppUpdateStatus status = manager.get(intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL)); AppUpdateStatus status = manager.get(
intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL));
boolean isStatusChange = intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false); boolean isStatusChange = intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false);
if (isStatusChange if (isStatusChange
&& status != null && status != null

View File

@ -59,7 +59,7 @@ public class AppStatusListItemController extends AppListItemController {
CharSequence message = null; CharSequence message = null;
if (status != null) { if (status != null) {
AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(activity); AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(activity);
manager.removeApk(status.getUniqueKey()); manager.removeApk(status.getCanonicalUrl());
switch (status.status) { switch (status.status) {
case Downloading: case Downloading:
cancelDownload(); cancelDownload();

View File

@ -5,12 +5,10 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.view.View; import android.view.View;
import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
@ -73,8 +71,8 @@ public class KnownVulnAppListItemController extends AppListItemController {
Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app);
if (shouldUpgradeInsteadOfUninstall(app, suggestedApk)) { if (shouldUpgradeInsteadOfUninstall(app, suggestedApk)) {
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity); LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity);
Uri uri = Uri.parse(suggestedApk.getUrl()); manager.registerReceiver(installReceiver,
manager.registerReceiver(installReceiver, Installer.getInstallIntentFilter(uri)); Installer.getInstallIntentFilter(suggestedApk.getCanonicalUrl()));
InstallManagerService.queue(activity, app, suggestedApk); InstallManagerService.queue(activity, app, suggestedApk);
} else { } else {
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity); LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity);
@ -141,7 +139,8 @@ public class KnownVulnAppListItemController extends AppListItemController {
try { try {
uninstallPendingIntent.send(); uninstallPendingIntent.send();
} catch (PendingIntent.CanceledException ignored) { } } catch (PendingIntent.CanceledException ignored) {
}
break; break;
} }
} }