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

View File

@ -314,10 +314,8 @@ public class SwapAppsView extends ListView implements
}
if (apk != null) {
String urlString = apk.getUrl();
// 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);
}

View File

@ -836,9 +836,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
public void install(@NonNull final App app, @NonNull final Apk apk) {
Uri downloadUri = Uri.parse(apk.getUrl());
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(downloadUri));
Installer.getInstallIntentFilter(apk.getCanonicalUrl()));
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 EXTRA_APK_URL = "urlstring";
public static final String EXTRA_STATUS = "status";
public static final String EXTRA_REASON_FOR_CHANGE = "reason";
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
* also known as {@code urlString}.
* also known as {@code canonicalUrl}.
* @see org.fdroid.fdroid.installer.InstallManagerService
*/
public String getUniqueKey() {
return apk.getUrl();
public String getCanonicalUrl() {
return apk.getCanonicalUrl();
}
/**
@ -225,9 +223,9 @@ public final class AppUpdateStatusManager {
}
@Nullable
public AppUpdateStatus get(String key) {
public AppUpdateStatus get(String canonicalUrl) {
synchronized (appMapping) {
return appMapping.get(key);
return appMapping.get(canonicalUrl);
}
}
@ -264,7 +262,7 @@ public final class AppUpdateStatusManager {
notifyChange(entry, isStatusUpdate);
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());
AppUpdateStatus entry = createAppEntry(apk, status, intent);
setEntryContentIntentIfEmpty(entry);
appMapping.put(entry.getUniqueKey(), entry);
appMapping.put(entry.getCanonicalUrl(), entry);
notifyAdd(entry);
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) {
if (!isBatchUpdating) {
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());
localBroadcastManager.sendBroadcast(broadcastIntent);
}
@ -300,7 +298,7 @@ public final class AppUpdateStatusManager {
private void notifyChange(AppUpdateStatus entry, boolean isStatusUpdate) {
if (!isBatchUpdating) {
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_IS_STATUS_UPDATE, isStatusUpdate);
localBroadcastManager.sendBroadcast(broadcastIntent);
@ -310,7 +308,7 @@ public final class AppUpdateStatusManager {
private void notifyRemove(AppUpdateStatus entry) {
if (!isBatchUpdating) {
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());
localBroadcastManager.sendBroadcast(broadcastIntent);
}
@ -321,7 +319,7 @@ public final class AppUpdateStatusManager {
ContentResolver resolver = context.getContentResolver();
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repoId);
AppUpdateStatus ret = new AppUpdateStatus(app, apk, status, intent);
appMapping.put(apk.getUrl(), ret);
appMapping.put(apk.getCanonicalUrl(), ret);
return ret;
}
}
@ -347,7 +345,7 @@ public final class AppUpdateStatusManager {
}
synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(apk.getUrl());
AppUpdateStatus entry = appMapping.get(apk.getCanonicalUrl());
if (entry != null) {
updateApkInternal(entry, status, pendingIntent);
} else {
@ -359,9 +357,9 @@ public final class AppUpdateStatusManager {
/**
* @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) {
AppUpdateStatus entry = appMapping.get(key);
AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) {
updateApkInternal(entry, status, pendingIntent);
}
@ -369,9 +367,9 @@ public final class AppUpdateStatusManager {
}
@Nullable
public Apk getApk(String key) {
public Apk getApk(String canonicalUrl) {
synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(key);
AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) {
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}
*
* @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
*/
public void removeApk(String key) {
public void removeApk(String canonicalUrl) {
synchronized (appMapping) {
InstallManagerService.removePendingInstall(context, key);
AppUpdateStatus entry = appMapping.remove(key);
InstallManagerService.removePendingInstall(context, canonicalUrl);
AppUpdateStatus entry = appMapping.remove(canonicalUrl);
if (entry != null) {
Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.apkName);
notifyRemove(entry);
@ -396,9 +394,9 @@ public final class AppUpdateStatusManager {
}
}
public void refreshApk(String key) {
public void refreshApk(String canonicalUrl) {
synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(key);
AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) {
Utils.debugLog(LOGTAG, "Refresh APK " + entry.apk.apkName);
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) {
AppUpdateStatus entry = appMapping.get(key);
AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) {
entry.progressMax = max;
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.
*/
public void setDownloadError(String url, @Nullable String errorText) {
public void setDownloadError(String canonicalUrl, @Nullable String errorText) {
synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(url);
AppUpdateStatus entry = appMapping.get(canonicalUrl);
if (entry != null) {
entry.status = Status.DownloadInterrupted;
entry.errorText = errorText;
entry.intent = null;
notifyChange(entry, true);
removeApk(url);
removeApk(canonicalUrl);
}
}
}
public void setApkError(Apk apk, String errorText) {
synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(apk.getUrl());
AppUpdateStatus entry = appMapping.get(apk.getCanonicalUrl());
if (entry == null) {
entry = createAppEntry(apk, Status.InstallError, null);
}
@ -444,7 +442,7 @@ public final class AppUpdateStatusManager {
entry.intent = getAppErrorIntent(entry);
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);
JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexUpdater.DATA_FILE_NAME);
indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
processIndexListener, repo.address, (int) indexEntry.getSize());
processIndexListener, (int) indexEntry.getSize());
// Process the index...
SAXParserFactory factory = SAXParserFactory.newInstance();
@ -254,14 +254,14 @@ public class IndexUpdater {
protected final ProgressListener downloadListener = new ProgressListener() {
@Override
public void onProgress(String urlString, long bytesRead, long totalBytes) {
public void onProgress(long bytesRead, long totalBytes) {
UpdateService.reportDownloadProgress(context, IndexUpdater.this, bytesRead, totalBytes);
}
};
protected final ProgressListener processIndexListener = new ProgressListener() {
@Override
public void onProgress(String urlString, long bytesRead, long totalBytes) {
public void onProgress(long bytesRead, long 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);
JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME);
InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry),
processIndexListener, repo.address, (int) indexEntry.getSize());
processIndexListener, (int) indexEntry.getSize());
processIndexV1(indexInputStream, indexEntry, cacheTag);
}

View File

@ -14,7 +14,7 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
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()) {
case NotificationHelper.BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED:
manager.clearAllUpdates();
@ -25,13 +25,13 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver {
case NotificationHelper.BROADCAST_NOTIFICATIONS_UPDATE_CLEARED:
// If clearing apps in state "InstallError" (like when auto-cancelling) we
// 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) {
manager.removeApk(notificationKey);
manager.removeApk(canonicalUrl);
}
break;
case NotificationHelper.BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED:
manager.removeApk(notificationKey);
manager.removeApk(canonicalUrl);
break;
}
}

View File

@ -46,13 +46,6 @@ class NotificationHelper {
private static final int MAX_UPDATES_TO_SHOW = 5;
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_INSTALLED = "installed";
@ -93,14 +86,14 @@ class NotificationHelper {
case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED:
updateStatusLists();
createSummaryNotifications();
url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
url = intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
entry = appUpdateStatusManager.get(url);
if (entry != null) {
createNotification(entry);
}
break;
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);
updateStatusLists();
if (entry != null) {
@ -111,7 +104,7 @@ class NotificationHelper {
}
break;
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_UPDATES);
updateStatusLists();
@ -164,8 +157,8 @@ class NotificationHelper {
private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
if (shouldIgnoreEntry(entry)) {
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
return;
}
@ -177,23 +170,23 @@ class NotificationHelper {
if (entry.status == AppUpdateStatusManager.Status.Installed) {
if (useStackedNotifications()) {
notification = createInstalledNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES);
notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_INSTALLED, notification);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.notify(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED, notification);
} else if (installed.size() == 1) {
notification = createInstalledNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification);
}
} else {
if (useStackedNotifications()) {
notification = createUpdateNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED);
notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_UPDATES, notification);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES, notification);
} else if (updates.size() == 1) {
notification = createUpdateNotification(entry);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification);
}
}
@ -346,7 +339,7 @@ class NotificationHelper {
}
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);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted);
@ -435,7 +428,7 @@ class NotificationHelper {
}
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);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted);
@ -540,7 +533,7 @@ class NotificationHelper {
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
// 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.
AppUpdateStatusManager.AppUpdateStatus oldEntry = appUpdateStatusManager.get(entry.getUniqueKey());
AppUpdateStatusManager.AppUpdateStatus oldEntry = appUpdateStatusManager.get(entry.getCanonicalUrl());
if (oldEntry != null
&& oldEntry.app != null
&& oldEntry.app.iconUrl != null

View File

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

View File

@ -19,6 +19,6 @@ import java.net.URL;
*/
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()
public String getUrl() {
public String getCanonicalUrl() {
checkRepoAddress();
return repoAddress + "/" + apkName.replace(" ", "%20");
}
@ -527,7 +536,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public File getMediaInstallPath(Context context) {
File path = Environment.getExternalStoragePublicDirectory(
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;
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
String[] mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension).split("/");

View File

@ -237,7 +237,7 @@ public class InstalledAppProviderService extends JobIntentService {
PackageInfo packageInfo = getPackageInfo(intent, packageName);
if (packageInfo != null) {
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);
if (apk == null) {
@ -258,7 +258,7 @@ public class InstalledAppProviderService extends JobIntentService {
} else if (ACTION_DELETE.equals(action)) {
deleteAppFromDb(this, 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);

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) {
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());
if (!dir.exists()) {
dir.mkdirs();

View File

@ -43,11 +43,11 @@ public class DefaultInstaller extends Installer {
}
@Override
protected void installPackageInternal(Uri localApkUri, Uri downloadUri) {
protected void installPackageInternal(Uri localApkUri, Uri canonicalUri) {
Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
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.setData(localApkUri);
@ -57,7 +57,7 @@ public class DefaultInstaller extends Installer {
installIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_USER_INTERACTION,
sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_USER_INTERACTION,
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_UNINSTALL = 1;
private Uri downloadUri;
/**
* @see InstallManagerService
*/
private Uri canonicalUri;
// for the broadcasts
private DefaultInstaller installer;
@ -63,7 +66,7 @@ public class DefaultInstallerActivity extends FragmentActivity {
installer = new DefaultInstaller(this, apk);
if (ACTION_INSTALL_PACKAGE.equals(action)) {
Uri localApkUri = intent.getData();
downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI);
canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
installPackage(localApkUri);
} else if (ACTION_UNINSTALL_PACKAGE.equals(action)) {
uninstallPackage(apk.packageName);
@ -120,7 +123,7 @@ public class DefaultInstallerActivity extends FragmentActivity {
startActivityForResult(intent, REQUEST_CODE_INSTALL);
} catch (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!");
finish();
}
@ -169,23 +172,23 @@ public class DefaultInstallerActivity extends FragmentActivity {
* never executed on Androids < 4.0
*/
if (Build.VERSION.SDK_INT < 14) {
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_COMPLETE);
installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_COMPLETE);
break;
}
switch (resultCode) {
case Activity.RESULT_OK:
installer.sendBroadcastInstall(downloadUri,
installer.sendBroadcastInstall(canonicalUri,
Installer.ACTION_INSTALL_COMPLETE);
break;
case Activity.RESULT_CANCELED:
installer.sendBroadcastInstall(downloadUri,
installer.sendBroadcastInstall(canonicalUri,
Installer.ACTION_INSTALL_INTERRUPTED);
break;
case Activity.RESULT_FIRST_USER:
default:
// AOSP returns Activity.RESULT_FIRST_USER on error
installer.sendBroadcastInstall(downloadUri,
installer.sendBroadcastInstall(canonicalUri,
Installer.ACTION_INSTALL_INTERRUPTED,
getString(R.string.install_error_unknown));
break;

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import android.content.pm.PackageInfo;
import android.net.Uri;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
@ -66,9 +65,10 @@ import java.io.IOException;
* APK on different servers, signed by different keys, or even different builds.
* <p><ul>
* <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()}
* <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>
* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both
* 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) {
Utils.debugLog(TAG, "onStartCommand " + intent);
String urlString = intent.getDataString();
if (TextUtils.isEmpty(urlString)) {
Utils.debugLog(TAG, "empty urlString, nothing to do");
String canonicalUrl = intent.getDataString();
if (TextUtils.isEmpty(canonicalUrl)) {
Utils.debugLog(TAG, "empty canonicalUrl, nothing to do");
return START_NOT_STICKY;
}
String action = intent.getAction();
if (ACTION_CANCEL.equals(action)) {
DownloaderService.cancel(this, urlString);
Apk apk = appUpdateStatusManager.getApk(urlString);
DownloaderService.cancel(this, canonicalUrl);
Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
if (apk != null) {
Utils.debugLog(TAG, "also canceling OBB downloads");
DownloaderService.cancel(this, apk.getPatchObbUrl());
DownloaderService.cancel(this, apk.getMainObbUrl());
}
return START_NOT_STICKY;
} else if (ACTION_INSTALL.equals(action)) {
if (!isPendingInstall(urlString)) {
if (!isPendingInstall(canonicalUrl)) {
Log.i(TAG, "Ignoring INSTALL that is not Pending Install: " + intent);
return START_NOT_STICKY;
}
@ -174,14 +175,14 @@ public class InstallManagerService extends Service {
}
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;
}
if ((flags & START_FLAG_REDELIVERY) == START_FLAG_REDELIVERY
&& !DownloaderService.isQueuedOrActive(urlString)) {
Utils.debugLog(TAG, urlString + " finished downloading while InstallManagerService was killed.");
appUpdateStatusManager.removeApk(urlString);
&& !DownloaderService.isQueuedOrActive(canonicalUrl)) {
Utils.debugLog(TAG, canonicalUrl + " finished downloading while InstallManagerService was killed.");
appUpdateStatusManager.removeApk(canonicalUrl);
return START_NOT_STICKY;
}
@ -205,23 +206,23 @@ public class InstallManagerService extends Service {
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null);
registerPackageDownloaderReceivers(urlString);
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256);
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256);
registerPackageDownloaderReceivers(canonicalUrl);
getMainObb(canonicalUrl, apk);
getPatchObb(canonicalUrl, apk);
File apkFilePath = ApkCache.getApkDownloadPath(this, apk.getUrl());
File apkFilePath = ApkCache.getApkDownloadPath(this, apk.getCanonicalUrl());
long apkFileSize = apkFilePath.length();
if (!apkFilePath.exists() || apkFileSize < apk.size) {
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
DownloaderService.queue(this, switchUrlToNewMirror(urlString, apk.repoId), apk.repoId, urlString);
Utils.debugLog(TAG, "download " + canonicalUrl + " " + apkFilePath);
DownloaderService.queueUsingRandomMirror(this, apk.repoId, canonicalUrl);
} 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_COMPLETE, apkFilePath);
} else {
Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath);
Utils.debugLog(TAG, "delete and download again " + canonicalUrl + " " + apkFilePath);
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
@ -234,22 +235,12 @@ public class InstallManagerService extends Service {
localBroadcastManager.sendBroadcast(intent);
}
/**
* 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 String getNewMirrorOnError(@Nullable String urlString, long repoId) {
try {
return FDroidApp.getNewMirrorOnError(urlString, RepoProvider.Helper.findById(this, repoId));
} catch (IOException e) {
return urlString;
}
private void getMainObb(final String canonicalUrl, Apk apk) {
getObb(canonicalUrl, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256, apk.repoId);
}
public String switchUrlToNewMirror(@Nullable String urlString, long repoId) {
return FDroidApp.switchUrlToNewMirror(urlString, RepoProvider.Helper.findById(this, repoId));
private void getPatchObb(final String canonicalUrl, Apk apk) {
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>
*/
private void getObb(final String urlString, String obbUrlString,
final File obbDestFile, final String hash) {
private void getObb(final String canonicalUrl, String obbUrlString,
final File obbDestFile, final String hash, final long repoId) {
if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) {
return;
}
@ -278,7 +269,7 @@ public class InstallManagerService extends Service {
long bytesRead = intent.getLongExtra(Downloader.EXTRA_BYTES_READ, 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)) {
localBroadcastManager.unregisterReceiver(this);
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)) {
localBroadcastManager.unregisterReceiver(this);
} else if (Downloader.ACTION_CONNECTION_FAILED.equals(action)) {
DownloaderService.queue(context, getNewMirrorOnError(urlString, 0), 0, urlString);
DownloaderService.queueUsingDifferentMirror(context, repoId, canonicalUrl);
} else {
throw new RuntimeException("intent action not handled!");
}
}
};
DownloaderService.queue(this, switchUrlToNewMirror(obbUrlString, 0), 0, obbUrlString);
DownloaderService.queueUsingRandomMirror(this, repoId, obbUrlString);
localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(obbUrlString));
}
/**
* 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() {
@Override
@ -334,52 +325,58 @@ public class InstallManagerService extends Service {
localBroadcastManager.unregisterReceiver(this);
return;
}
Uri downloadUri = intent.getData();
String urlString = downloadUri.toString();
Uri canonicalUri = intent.getData();
String canonicalUrl = intent.getDataString();
long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0);
String mirrorUrlString = intent.getStringExtra(Downloader.EXTRA_MIRROR_URL);
switch (intent.getAction()) {
case Downloader.ACTION_STARTED:
// App should currently be in the "PendingDownload" state, so this changes it to "Downloading".
Intent intentObject = new Intent(context, InstallManagerService.class);
intentObject.setAction(ACTION_CANCEL);
intentObject.setData(downloadUri);
intentObject.setData(canonicalUri);
PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0);
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Downloading, action);
appUpdateStatusManager.updateApk(canonicalUrl,
AppUpdateStatusManager.Status.Downloading, action);
break;
case Downloader.ACTION_PROGRESS:
long bytesRead = intent.getLongExtra(Downloader.EXTRA_BYTES_READ, 0);
long totalBytes = intent.getLongExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
appUpdateStatusManager.updateApkProgress(urlString, totalBytes, bytesRead);
appUpdateStatusManager.updateApkProgress(canonicalUrl, totalBytes, bytesRead);
break;
case Downloader.ACTION_COMPLETE:
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
Uri localApkUri = Uri.fromFile(localFile);
Utils.debugLog(TAG, "download completed of " + mirrorUrlString + " to " + localApkUri);
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null);
Utils.debugLog(TAG, "download completed of "
+ intent.getStringExtra(Downloader.EXTRA_MIRROR_URL) + " to " + localApkUri);
appUpdateStatusManager.updateApk(canonicalUrl,
AppUpdateStatusManager.Status.ReadyToInstall, null);
localBroadcastManager.unregisterReceiver(this);
registerInstallReceiver(downloadUri);
registerInstallReceiver(canonicalUrl);
Apk apk = appUpdateStatusManager.getApk(urlString);
Apk apk = appUpdateStatusManager.getApk(canonicalUrl);
if (apk != null) {
InstallerService.install(context, localApkUri, downloadUri, apk);
InstallerService.install(context, localApkUri, canonicalUri, apk);
}
break;
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);
break;
case Downloader.ACTION_CONNECTION_FAILED:
// TODO move this logic into DownloaderService to hide the mirror URL stuff from this class
try {
String currentUrlString = FDroidApp.getNewMirrorOnError(mirrorUrlString,
String currentUrlString = FDroidApp.getNewMirrorOnError(
intent.getStringExtra(Downloader.EXTRA_MIRROR_URL),
RepoProvider.Helper.findById(InstallManagerService.this, repoId));
DownloaderService.queue(context, currentUrlString, repoId, urlString);
DownloaderService.queue(context, currentUrlString, repoId, canonicalUrl);
DownloaderService.setTimeout(FDroidApp.getTimeout());
} catch (IOException e) {
appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
appUpdateStatusManager.setDownloadError(canonicalUrl,
intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
localBroadcastManager.unregisterReceiver(this);
}
break;
@ -390,14 +387,14 @@ public class InstallManagerService extends Service {
};
localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(urlString));
DownloaderService.getIntentFilter(canonicalUrl));
}
/**
* Register a {@link BroadcastReceiver} for tracking install progress for a
* 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() {
@Override
@ -406,15 +403,17 @@ public class InstallManagerService extends Service {
localBroadcastManager.unregisterReceiver(this);
return;
}
String downloadUrl = intent.getDataString();
String canonicalUrl = intent.getDataString();
Apk apk;
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installing, null);
appUpdateStatusManager.updateApk(canonicalUrl,
AppUpdateStatusManager.Status.Installing, null);
break;
case Installer.ACTION_INSTALL_COMPLETE:
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null);
Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl);
appUpdateStatusManager.updateApk(canonicalUrl,
AppUpdateStatusManager.Status.Installed, null);
Apk apkComplete = appUpdateStatusManager.getApk(canonicalUrl);
if (apkComplete != null && apkComplete.isApk()) {
try {
@ -432,7 +431,7 @@ public class InstallManagerService extends Service {
if (!TextUtils.isEmpty(errorMessage)) {
appUpdateStatusManager.setApkError(apk, errorMessage);
} else {
appUpdateStatusManager.removeApk(downloadUrl);
appUpdateStatusManager.removeApk(canonicalUrl);
}
localBroadcastManager.unregisterReceiver(this);
break;
@ -448,7 +447,7 @@ public class InstallManagerService extends Service {
};
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}
*/
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);
putPendingInstall(context, urlString, apk.packageName);
Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString);
putPendingInstall(context, canonicalUrl, apk.packageName);
Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + canonicalUrl);
Intent intent = new Intent(context, InstallManagerService.class);
intent.setAction(ACTION_INSTALL);
intent.setData(Uri.parse(urlString));
intent.setData(Uri.parse(canonicalUrl));
intent.putExtra(EXTRA_APP, app);
intent.putExtra(EXTRA_APK, apk);
context.startService(intent);
}
public static void cancel(Context context, String urlString) {
removePendingInstall(context, urlString);
public static void cancel(Context context, String canonicalUrl) {
removePendingInstall(context, canonicalUrl);
Intent intent = new Intent(context, InstallManagerService.class);
intent.setAction(ACTION_CANCEL);
intent.setData(Uri.parse(urlString));
intent.setData(Uri.parse(canonicalUrl));
context.startService(intent);
}
@ -491,29 +490,29 @@ public class InstallManagerService extends Service {
* completed, or the device lost power in the middle of the install
* process.
*/
public boolean isPendingInstall(String urlString) {
return pendingInstalls.contains(urlString);
public boolean isPendingInstall(String canonicalUrl) {
return pendingInstalls.contains(canonicalUrl);
}
/**
* 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.
*
* @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) {
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) {
pendingInstalls = getPendingInstalls(context);
}
pendingInstalls.edit().remove(urlString).apply();
pendingInstalls.edit().remove(canonicalUrl).apply();
}
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_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_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";
@ -165,23 +156,23 @@ public abstract class Installer {
return intent;
}
void sendBroadcastInstall(Uri downloadUri, String action, PendingIntent pendingIntent) {
sendBroadcastInstall(context, downloadUri, action, apk, pendingIntent, null);
void sendBroadcastInstall(Uri canonicalUri, String action, PendingIntent pendingIntent) {
sendBroadcastInstall(context, canonicalUri, action, apk, pendingIntent, null);
}
void sendBroadcastInstall(Uri downloadUri, String action) {
sendBroadcastInstall(context, downloadUri, action, apk, null, null);
void sendBroadcastInstall(Uri canonicalUri, String action) {
sendBroadcastInstall(context, canonicalUri, action, apk, null, null);
}
void sendBroadcastInstall(Uri downloadUri, String action, String errorMessage) {
sendBroadcastInstall(context, downloadUri, action, apk, null, errorMessage);
void sendBroadcastInstall(Uri canonicalUri, String action, String errorMessage) {
sendBroadcastInstall(context, canonicalUri, action, apk, null, errorMessage);
}
static void sendBroadcastInstall(Context context,
Uri downloadUri, String action, Apk apk,
Uri canonicalUri, String action, Apk apk,
PendingIntent pendingIntent, String errorMessage) {
Intent intent = new Intent(action);
intent.setData(downloadUri);
intent.setData(canonicalUri);
intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
intent.putExtra(Installer.EXTRA_APK, apk);
if (!TextUtils.isEmpty(errorMessage)) {
@ -226,20 +217,34 @@ public abstract class Installer {
/**
* 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.addAction(Installer.ACTION_INSTALL_STARTED);
intentFilter.addAction(Installer.ACTION_INSTALL_COMPLETE);
intentFilter.addAction(Installer.ACTION_INSTALL_INTERRUPTED);
intentFilter.addAction(Installer.ACTION_INSTALL_USER_INTERACTION);
intentFilter.addDataScheme(uri.getScheme());
intentFilter.addDataAuthority(uri.getHost(), String.valueOf(uri.getPort()));
intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
intentFilter.addDataScheme(canonicalUri.getScheme());
intentFilter.addDataAuthority(canonicalUri.getHost(), String.valueOf(canonicalUri.getPort()));
intentFilter.addDataPath(canonicalUri.getPath(), PatternMatcher.PATTERN_LITERAL);
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) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Installer.ACTION_UNINSTALL_STARTED);
@ -263,19 +268,19 @@ public abstract class Installer {
* permissions that the APK is requesting.
*
* @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
* @see InstallManagerService
* @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;
try {
sanitizedUri = ApkFileProvider.getSafeUri(context, localApkUri, apk);
} catch (IOException e) {
Utils.debugLog(TAG, e.getMessage(), e);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage());
sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage());
return;
}
@ -285,7 +290,7 @@ public abstract class Installer {
apkVerifier.verifyApk();
} catch (ApkVerifier.ApkVerificationException e) {
Utils.debugLog(TAG, e.getMessage(), e);
sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage());
sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED, e.getMessage());
return;
} catch (ApkVerifier.ApkPermissionUnequalException e) {
// 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, "Falling back to AOSP DefaultInstaller!");
DefaultInstaller defaultInstaller = new DefaultInstaller(context, apk);
defaultInstaller.installPackageInternal(sanitizedUri, downloadUri);
defaultInstaller.installPackageInternal(sanitizedUri, canonicalUri);
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

View File

@ -28,10 +28,10 @@ import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.views.AppDetailsActivity;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.AppDetailsActivity;
import java.io.File;
import java.io.FileFilter;
@ -74,8 +74,8 @@ public class InstallerService extends JobIntentService {
if (ACTION_INSTALL.equals(intent.getAction())) {
Uri uri = intent.getData();
Uri downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI);
installer.installPackage(uri, downloadUri);
Uri canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
installer.installPackage(uri, canonicalUri);
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
installer.uninstallPackage();
new Thread() {
@ -113,17 +113,18 @@ public class InstallerService extends JobIntentService {
*
* @param context this app's {@link Context}
* @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
* @see #uninstall(Context, Apk)
* @see InstallManagerService
*/
public static void install(Context context, Uri localApkUri, Uri downloadUri, Apk apk) {
Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk,
public static void install(Context context, Uri localApkUri, Uri canonicalUri, Apk apk) {
Installer.sendBroadcastInstall(context, canonicalUri, Installer.ACTION_INSTALL_STARTED, apk,
null, null);
Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_INSTALL);
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);
enqueueWork(context, intent);
}

View File

@ -308,7 +308,7 @@ public class PrivilegedInstaller extends Installer {
}
@Override
protected void installPackageInternal(final Uri localApkUri, final Uri downloadUri) {
protected void installPackageInternal(final Uri localApkUri, final Uri canonicalUri) {
ServiceConnection mServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
@ -317,9 +317,9 @@ public class PrivilegedInstaller extends Installer {
@Override
public void handleResult(String packageName, int returnCode) throws RemoteException {
if (returnCode == INSTALL_SUCCEEDED) {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_COMPLETE);
sendBroadcastInstall(canonicalUri, ACTION_INSTALL_COMPLETE);
} else {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED,
sendBroadcastInstall(canonicalUri, ACTION_INSTALL_INTERRUPTED,
"Error " + returnCode + ": "
+ INSTALL_RETURN_CODES.get(returnCode));
}
@ -329,7 +329,7 @@ public class PrivilegedInstaller extends Installer {
try {
boolean hasPermissions = privService.hasPrivilegedPermissions();
if (!hasPermissions) {
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED,
sendBroadcastInstall(canonicalUri, ACTION_INSTALL_INTERRUPTED,
context.getString(R.string.system_install_denied_permissions));
return;
}
@ -338,7 +338,7 @@ public class PrivilegedInstaller extends Installer {
null, callback);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException", e);
sendBroadcastInstall(downloadUri, ACTION_INSTALL_INTERRUPTED,
sendBroadcastInstall(canonicalUri, ACTION_INSTALL_INTERRUPTED,
"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_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_REPO_ID = "org.fdroid.fdroid.net.Downloader.extra.ERROR_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.ERROR_MIRROR_URL";
public static final String EXTRA_REPO_ID = "org.fdroid.fdroid.net.Downloader.extra.REPO_ID";
public static final String EXTRA_MIRROR_URL = "org.fdroid.fdroid.net.Downloader.extra.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 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.
*/
private final TimerTask progressTask = new TimerTask() {
private long lastBytesRead = Long.MIN_VALUE;
private long lastTotalBytes = Long.MIN_VALUE;
@Override
public void run() {
if (downloaderProgressListener != null) {
downloaderProgressListener.onProgress(urlString, bytesRead, totalBytes);
if (downloaderProgressListener != null
&& (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.text.TextUtils;
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.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.SanitizedFile;
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
* only one request will be processed at a time.
* <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
* 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,
* {@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
* than with APKs since there is not reliable standard for a unique ID for
* media files, unlike APKs with {@code packageName} and {@code versionCode}.
*
* @see android.app.IntentService
* @see org.fdroid.fdroid.installer.InstallManagerService
*/
public class DownloaderService extends Service {
private static final String TAG = "DownloaderService";
@ -94,10 +99,13 @@ public class DownloaderService extends Service {
private volatile Looper serviceLooper;
private static volatile ServiceHandler serviceHandler;
private static volatile Downloader downloader;
private static volatile String activeCanonicalUrl;
private LocalBroadcastManager localBroadcastManager;
private static volatile int timeout;
private final class ServiceHandler extends Handler {
static final String TAG = "ServiceHandler";
ServiceHandler(Looper looper) {
super(looper);
}
@ -119,6 +127,9 @@ public class DownloaderService extends Service {
thread.start();
serviceLooper = thread.getLooper();
if (BuildConfig.DEBUG) {
serviceLooper.setMessageLogging(new LogPrinter(Log.DEBUG, ServiceHandler.TAG));
}
serviceHandler = new ServiceHandler(serviceLooper);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
}
@ -131,21 +142,27 @@ public class DownloaderService extends Service {
return START_NOT_STICKY;
}
String uriString = intent.getDataString();
if (uriString == null) {
String downloadUrl = intent.getDataString();
if (downloadUrl == null) {
Utils.debugLog(TAG, "Received Intent with no URI: " + intent);
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())) {
Utils.debugLog(TAG, "Cancelling download of " + uriString);
Integer whatToRemove = uriString.hashCode();
Utils.debugLog(TAG, "Cancelling download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
+ " downloading from " + downloadUrl);
Integer whatToRemove = canonicalUrl.hashCode();
if (serviceHandler.hasMessages(whatToRemove)) {
Utils.debugLog(TAG, "Removing download with ID of " + whatToRemove
+ " from service handler, then sending interrupted event.");
serviceHandler.removeMessages(whatToRemove);
sendBroadcast(intent.getData(), Downloader.ACTION_INTERRUPTED);
} else if (isActive(uriString)) {
sendCancelledBroadcast(intent.getData(), canonicalUrl);
} else if (isActive(canonicalUrl)) {
downloader.cancelDownload();
} else {
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();
msg.arg1 = startId;
msg.obj = intent;
msg.what = uriString.hashCode();
msg.what = canonicalUrl.hashCode();
serviceHandler.sendMessage(msg);
Utils.debugLog(TAG, "Queued download of " + uriString);
Utils.debugLog(TAG, "Queued download of " + canonicalUrl.hashCode() + "/" + canonicalUrl
+ " using " + downloadUrl);
} else {
Utils.debugLog(TAG, "Received Intent with unknown action: " + intent);
}
@ -198,18 +216,19 @@ public class DownloaderService extends Service {
*/
private void handleIntent(Intent intent) {
final Uri uri = intent.getData();
long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0);
String canonicalUrlString = intent.getStringExtra(Downloader.EXTRA_CANONICAL_URL);
final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, canonicalUrlString);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile, repoId, canonicalUrlString);
final long repoId = intent.getLongExtra(Downloader.EXTRA_REPO_ID, 0);
final Uri canonicalUrl = Uri.parse(intent.getStringExtra(Downloader.EXTRA_CANONICAL_URL));
final SanitizedFile localFile = ApkCache.getApkDownloadPath(this, canonicalUrl);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile, repoId, canonicalUrl);
try {
activeCanonicalUrl = canonicalUrl.toString();
downloader = DownloaderFactory.create(this, uri, localFile);
downloader.setListener(new ProgressListener() {
@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.setData(uri);
intent.setData(canonicalUrl);
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes);
localBroadcastManager.sendBroadcast(intent);
@ -219,47 +238,44 @@ public class DownloaderService extends Service {
downloader.download();
if (downloader.isNotFound()) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, getString(R.string.download_404),
repoId, canonicalUrlString);
repoId, canonicalUrl);
} else {
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile, repoId, canonicalUrlString);
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile, repoId, canonicalUrl);
}
} catch (InterruptedException e) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, repoId, canonicalUrlString);
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, repoId, canonicalUrl);
} catch (ConnectException | HttpRetryException | NoRouteToHostException | SocketTimeoutException
| SSLHandshakeException | SSLKeyException | SSLPeerUnverifiedException | SSLProtocolException
| ProtocolException | UnknownHostException e) {
// if the above list of exceptions changes, also change it in IndexV1Updater.update()
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) {
e.printStackTrace();
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile,
e.getLocalizedMessage(), repoId, canonicalUrlString);
e.getLocalizedMessage(), repoId, canonicalUrl);
} finally {
if (downloader != null) {
downloader.close();
}
}
downloader = null;
activeCanonicalUrl = null;
}
private void sendBroadcast(Uri uri, String action) {
sendBroadcast(uri, action, null, null);
private void sendCancelledBroadcast(Uri uri, String canonicalUrl) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, null, 0, Uri.parse(canonicalUrl));
}
private void sendBroadcast(Uri uri, String action, File file, long repoId, String originalUrlString) {
sendBroadcast(uri, action, file, null, repoId, originalUrlString);
}
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, long repoId, Uri canonicalUrl) {
sendBroadcast(uri, action, file, null, repoId, canonicalUrl);
}
private void sendBroadcast(Uri uri, String action, File file, String errorMessage, long repoId,
String originalUrlString) {
Uri canonicalUrl) {
Intent intent = new Intent(action);
if (originalUrlString != null) {
intent.setData(Uri.parse(originalUrlString));
if (canonicalUrl != null) {
intent.setData(canonicalUrl);
}
if (file != null) {
intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
@ -278,41 +294,72 @@ public class DownloaderService extends Service {
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
*
* @param context this app's {@link Context}
* @param 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 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)
*/
public static void queue(Context context, String mirrorUrlString, long repoId, String urlString) {
if (TextUtils.isEmpty(mirrorUrlString)) {
public static void queue(Context context, String mirrorUrl, long repoId, String canonicalUrl) {
if (TextUtils.isEmpty(mirrorUrl)) {
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.setAction(ACTION_QUEUE);
intent.setData(Uri.parse(mirrorUrlString));
intent.setData(Uri.parse(mirrorUrl));
intent.putExtra(Downloader.EXTRA_REPO_ID, repoId);
intent.putExtra(Downloader.EXTRA_CANONICAL_URL, urlString);
intent.putExtra(Downloader.EXTRA_CANONICAL_URL, canonicalUrl);
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.
* <p>
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
*
* @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)
*/
public static void cancel(Context context, String urlString) {
if (TextUtils.isEmpty(urlString)) {
public static void cancel(Context context, String canonicalUrl) {
if (TextUtils.isEmpty(canonicalUrl)) {
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.setAction(ACTION_CANCEL);
intent.setData(Uri.parse(urlString));
intent.setData(Uri.parse(canonicalUrl));
intent.putExtra(Downloader.EXTRA_CANONICAL_URL, canonicalUrl);
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
* in {@link android.app.Activity#onResume()}.
*/
public static boolean isQueuedOrActive(String urlString) {
if (TextUtils.isEmpty(urlString)) { //NOPMD - suggests unreadable format
public static boolean isQueuedOrActive(String canonicalUrl) {
if (TextUtils.isEmpty(canonicalUrl)) { //NOPMD - suggests unreadable format
return false;
}
if (serviceHandler == null) {
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.
*/
private static boolean isActive(String urlString) {
return downloader != null && TextUtils.equals(urlString, downloader.urlString);
private static boolean isActive(String downloadUrl) {
return downloader != null && TextUtils.equals(downloadUrl, activeCanonicalUrl);
}
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.
*
* @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) {
Uri uri = Uri.parse(urlString);
public static IntentFilter getIntentFilter(String canonicalUrl) {
Uri uri = Uri.parse(canonicalUrl);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Downloader.ACTION_STARTED);
intentFilter.addAction(Downloader.ACTION_PROGRESS);

View File

@ -171,9 +171,9 @@ public class AppDetailsActivity extends AppCompatActivity
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(app.packageName)) {
if (status.status == AppUpdateStatusManager.Status.Installed) {
ausm.removeApk(status.getUniqueKey());
ausm.removeApk(status.getCanonicalUrl());
} else {
ausm.refreshApk(status.getUniqueKey());
ausm.refreshApk(status.getCanonicalUrl());
}
}
}
@ -449,7 +449,7 @@ public class AppDetailsActivity extends AppCompatActivity
if (justReceived) {
adapter.setIndeterminateProgress(R.string.installing);
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(Uri.parse(newStatus.getUniqueKey())));
Installer.getInstallIntentFilter(newStatus.getCanonicalUrl()));
}
break;
@ -459,7 +459,7 @@ public class AppDetailsActivity extends AppCompatActivity
Toast.makeText(this, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
} else {
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, msg, Toast.LENGTH_LONG).show();
}
@ -491,9 +491,9 @@ public class AppDetailsActivity extends AppCompatActivity
AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
if (currentStatus != null
&& isRemoving
&& !TextUtils.equals(status.getUniqueKey(), currentStatus.getUniqueKey())) {
&& !TextUtils.equals(status.getCanonicalUrl(), currentStatus.getCanonicalUrl())) {
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)) {
Utils.debugLog(TAG, "Ignoring app status change because it belongs to "
+ status.apk.packageName + " not " + app.packageName);
@ -650,7 +650,7 @@ public class AppDetailsActivity extends AppCompatActivity
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
if (status.status == AppUpdateStatusManager.Status.Installed) {
ausm.removeApk(status.getUniqueKey());
ausm.removeApk(status.getCanonicalUrl());
}
}
if (app == null) {
@ -720,7 +720,7 @@ public class AppDetailsActivity extends AppCompatActivity
@Override
public void installCancel() {
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
// any sort of notification that the app was successfully installed. It should be
// apparent to the user because they just launched it.
AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getUniqueKey());
AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getCanonicalUrl());
}
return;
}
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
String urlString = currentStatus.apk.getUrl();
File apkFilePath = ApkCache.getApkDownloadPath(activity, urlString);
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() +
String canonicalUrl = currentStatus.apk.getCanonicalUrl();
File apkFilePath = ApkCache.getApkDownloadPath(activity, canonicalUrl);
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getCanonicalUrl() +
" to " + apkFilePath);
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity);
@ -505,10 +505,10 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
}
};
Uri apkDownloadUri = Uri.parse(urlString);
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(apkDownloadUri));
Uri canonicalUri = Uri.parse(canonicalUrl);
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(canonicalUri));
Installer installer = InstallerFactory.create(activity, currentStatus.apk);
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), apkDownloadUri);
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), canonicalUri);
} else {
final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app);
InstallManagerService.queue(activity, app, suggestedApk);
@ -534,6 +534,6 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
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.
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);
if (isStatusChange
&& status != null

View File

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

View File

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