From 79d526ead16394c432535f307a633ed0d5e12163 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 20 Apr 2017 16:28:17 +1000 Subject: [PATCH 1/5] Correctly notify user of downloaded but not installed apks Previously this only worked for apps which were being upgraded to the latest available version. Now it works for apks being upgraded from an old version to a newer-but-not-newest version. Perhaps more importantly, it also now works for newly installed apps. This has a problem where if a user installs, then uninstalls an app, they are still notified about installing it. This is because the apk is in the cache and the code doesn't know whether it is there from the initial install, or because it has been redownloaded by the user to install some time after uninstalling. --- .../fdroid/fdroid/AppUpdateStatusService.java | 77 +++++++++++++++---- .../org/fdroid/fdroid/data/ApkProvider.java | 11 +++ 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java index b3b43b54a..416543ebe 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java @@ -3,17 +3,17 @@ package org.fdroid.fdroid; import android.app.IntentService; import android.content.Context; import android.content.Intent; -import android.net.Uri; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.support.annotation.Nullable; +import android.util.Log; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.installer.ApkCache; import org.fdroid.fdroid.installer.InstallManagerService; +import java.io.File; import java.util.ArrayList; import java.util.List; @@ -23,12 +23,11 @@ import java.util.List; * {@link AppUpdateStatusManager.Status#ReadyToInstall}. This is an {@link IntentService} so as to * run on a background thread, as it hits the disk a bit to figure out the hash of each downloaded * file. - * - * TODO: Deal with more than just the suggested version. It should also work for people downloading earlier versions (but still newer than their current) - * TODO: Identify new apps which have not been installed before, but which have been downloading. Currently only works for updates. */ public class AppUpdateStatusService extends IntentService { + private static final String TAG = "AppUpdateStatusService"; + /** * Queue up a background scan of all downloaded apk files to see if we should notify the user * that they are ready to install. @@ -43,17 +42,69 @@ public class AppUpdateStatusService extends IntentService { @Override protected void onHandleIntent(@Nullable Intent intent) { - List apps = AppProvider.Helper.findCanUpdate(this, Schema.AppMetadataTable.Cols.ALL); List apksReadyToInstall = new ArrayList<>(); - for (App app : apps) { - Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode); - Uri downloadUri = Uri.parse(apk.getUrl()); - if (ApkCache.apkIsCached(ApkCache.getApkDownloadPath(this, downloadUri), apk)) { - apksReadyToInstall.add(apk); + File cacheDir = ApkCache.getApkCacheDir(this); + for (String repoDirName : cacheDir.list()) { + File repoDir = new File(cacheDir, repoDirName); + for (String apkFileName : repoDir.list()) { + Apk apk = processDownloadedApk(new File(repoDir, apkFileName)); + if (apk != null) { + Log.i(TAG, "Found downloaded apk " + apk.packageName + ". Notifying user that it should be installed."); + apksReadyToInstall.add(apk); + } } } AppUpdateStatusManager.getInstance(this).addApks(apksReadyToInstall, AppUpdateStatusManager.Status.ReadyToInstall); InstallManagerService.managePreviouslyDownloadedApks(this); } + + @Nullable + private Apk processDownloadedApk(File apkPath) { + Utils.debugLog(TAG, "Checking " + apkPath); + PackageInfo downloadedInfo = getPackageManager().getPackageArchiveInfo(apkPath.getAbsolutePath(), PackageManager.GET_GIDS); + if (downloadedInfo == null) { + Utils.debugLog(TAG, "Skipping " + apkPath + " because PackageManager was unable to read it."); + return null; + } + + // NOTE: This presumes SHA256 is the only supported hash. It seems like that is an assumption + // in more than one place in the F-Droid client. If this becomes a problem in the future, we + // can query the Apk table for `SELECT DISTINCT hashType FROM fdroid_apk` and then we can just + // try each of the hash types that have been specified in the metadata. Seems a bit overkill + // at the time of writing though. + Utils.debugLog(TAG, "Found package for " + downloadedInfo.packageName + ", checking its hash to see if it downloaded correctly."); + String hash = Utils.getBinaryHash(apkPath, "sha256"); + + List apksMatchingHash = ApkProvider.Helper.findApksByHash(this, hash); + Utils.debugLog(TAG, "Found " + apksMatchingHash.size() + " apk(s) matching the hash " + hash); + + if (apksMatchingHash.size() == 0) { + Utils.debugLog(TAG, "Either the apk wasn't downloaded fully, or the repo it came from has been disabled. Either way, not notifying the user about it."); + return null; + } + + // It makes zero difference whether we return the apk from one repo or another. The hash + // calculation shows that they are exactly the same binary. + Apk downloadedApk = apksMatchingHash.get(0); + + PackageInfo installedInfo = null; + try { + installedInfo = getPackageManager().getPackageInfo(downloadedApk.packageName, PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException ignored) {} + + if (installedInfo == null) { + // This will return some false positives, because it is possible that + // the user downloaded + installed, tried the apk, and then uninstalled it. + Utils.debugLog(TAG, downloadedApk.packageName + " is not installed, so presuming we need to notify the user about installing it."); + return downloadedApk; + } + + if (installedInfo.versionCode >= downloadedInfo.versionCode) { + Utils.debugLog(TAG, downloadedApk.packageName + " already installed with versionCode " + installedInfo.versionCode + " and downloaded apk is only " + downloadedInfo.versionCode + ", so ignoring downloaded apk."); + return null; + } + + return downloadedApk; + } } \ No newline at end of file diff --git a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java index 138f96132..655510cf2 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.net.Uri; +import android.support.annotation.NonNull; import android.util.Log; import org.fdroid.fdroid.data.Schema.ApkTable; @@ -173,6 +174,16 @@ public class ApkProvider extends FDroidProvider { } return apk; } + + @NonNull + public static List findApksByHash(Context context, String apkHash) { + ContentResolver resolver = context.getContentResolver(); + final Uri uri = getContentUri(); + String selection = " apk." + Cols.HASH + " = ? "; + String[] selectionArgs = new String[]{apkHash}; + Cursor cursor = resolver.query(uri, Cols.ALL, selection, selectionArgs, null); + return cursorToList(cursor); + } } private static final int CODE_PACKAGE = CODE_SINGLE + 1; From ccdd8a168ce9492ff8893dc14db73c2ecba094a7 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 21 Apr 2017 15:51:55 +1000 Subject: [PATCH 2/5] Don't prompt user to install, if apk is in cache from a previous install Use SharedPreferences to keep track of whether we are in the middle of an install for a particular apk or not. If sothen the presence of an .apk file in the cache means we need to tell the user (in the updates tab) that a file is ready to install. --- .../fdroid/fdroid/AppUpdateStatusManager.java | 57 +++++++++++++++++++ .../fdroid/fdroid/AppUpdateStatusService.java | 19 ++++--- .../installer/InstallManagerService.java | 6 ++ 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 39f45d5da..9ce22abb9 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -5,6 +5,7 @@ import android.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -35,6 +36,8 @@ import java.util.Map; */ public final class AppUpdateStatusManager { + private static final String TAG = "AppUpdateStatusManager"; + /** * Broadcast when: * * The user clears the list of installed apps from notification manager. @@ -124,9 +127,13 @@ public final class AppUpdateStatusManager { private final HashMap appMapping = new HashMap<>(); private boolean isBatchUpdating; + /** @see #isPendingInstall(String) */ + private final SharedPreferences apksPendingInstall; + private AppUpdateStatusManager(Context context) { this.context = context; localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext()); + apksPendingInstall = context.getSharedPreferences("apks-pending-install", Context.MODE_PRIVATE); } @Nullable @@ -419,4 +426,54 @@ public final class AppUpdateStatusManager { errorDialogIntent, PendingIntent.FLAG_UPDATE_CURRENT); } + + /** + * Note that this could technically be made private and automatically invoked when + * {@link #addApk(Apk, Status, PendingIntent)} is called, but that would greatly reduce + * the maintainability of this class. Right now it is used by two clients: the notification + * manager, and the Updates tab. They have different requirements, with the Updates information + * being more permanent than the notification info. As such, the different clients should be + * aware of their requirements when invoking general-sounding methods like "addApk()", rather + * than this class trying to second-guess why they added an apk. + * @see #isPendingInstall(String) + */ + public void markAsPendingInstall(String uniqueKey) { + AppUpdateStatus entry = get(uniqueKey); + if (entry != null) { + Utils.debugLog(TAG, "Marking " + entry.apk.packageName + " as pending install."); + apksPendingInstall.edit().putBoolean(entry.apk.hash, true).apply(); + } + } + + /** + * @see #markAsNoLongerPendingInstall(AppUpdateStatus) + * @see #isPendingInstall(String) + */ + public void markAsNoLongerPendingInstall(String uniqueKey) { + AppUpdateStatus entry = get(uniqueKey); + if (entry != null) { + markAsNoLongerPendingInstall(entry); + } + } + + /** + * @see #markAsNoLongerPendingInstall(AppUpdateStatus) + * @see #isPendingInstall(String) + */ + public void markAsNoLongerPendingInstall(@NonNull AppUpdateStatus entry) { + Utils.debugLog(TAG, "Marking " + entry.apk.packageName + " as NO LONGER pending install."); + apksPendingInstall.edit().remove(entry.apk.hash).apply(); + } + + /** + * Keep track of the list of apks for which an install was initiated (i.e. a download + install). + * This is used when F-Droid starts, so that it can look through the cached apks and decide whether + * the presence of a .apk file means we should tell the user to press "Install" to complete the + * process, or whether it is purely there because it was installed some time ago and is no longer + * needed. + */ + public boolean isPendingInstall(String apkHash) { + return apksPendingInstall.contains(apkHash); + } + } diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java index 416543ebe..0b41dbfa7 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java @@ -84,20 +84,25 @@ public class AppUpdateStatusService extends IntentService { return null; } - // It makes zero difference whether we return the apk from one repo or another. The hash - // calculation shows that they are exactly the same binary. + // It makes zero difference which apk we get from this list. By definition they all have + // the exact same hash, and are thus the same binary. Apk downloadedApk = apksMatchingHash.get(0); PackageInfo installedInfo = null; try { installedInfo = getPackageManager().getPackageInfo(downloadedApk.packageName, PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException ignored) {} + } catch (PackageManager.NameNotFoundException ignored) { } if (installedInfo == null) { - // This will return some false positives, because it is possible that - // the user downloaded + installed, tried the apk, and then uninstalled it. - Utils.debugLog(TAG, downloadedApk.packageName + " is not installed, so presuming we need to notify the user about installing it."); - return downloadedApk; + if (AppUpdateStatusManager.getInstance(this).isPendingInstall(hash)) { + Utils.debugLog(TAG, downloadedApk.packageName + " is not installed, so presuming we need to notify the user about installing it."); + return downloadedApk; + } else { + // It was probably downloaded for a previous install, and then subsequently removed + // (but stayed in the cache, as is the designed behaviour). Under these circumstances + // we don't want to tell the user to try and install it. + return null; + } } if (installedInfo.versionCode >= downloadedInfo.versionCode) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 1237d5181..ae78434af 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -138,6 +138,7 @@ public class InstallManagerService extends Service { DownloaderService.cancel(this, apk.getPatchObbUrl()); DownloaderService.cancel(this, apk.getMainObbUrl()); } + appUpdateStatusManager.markAsNoLongerPendingInstall(urlString); appUpdateStatusManager.removeApk(urlString); return START_NOT_STICKY; } else if (!ACTION_INSTALL.equals(action)) { @@ -164,7 +165,9 @@ public class InstallManagerService extends Service { Utils.debugLog(TAG, "Intent had null EXTRA_APP and/or EXTRA_APK: " + intent); return START_NOT_STICKY; } + appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Unknown, null); + appUpdateStatusManager.markAsPendingInstall(urlString); registerApkDownloaderReceivers(urlString); getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256); @@ -295,6 +298,7 @@ public class InstallManagerService extends Service { } break; case Downloader.ACTION_INTERRUPTED: + appUpdateStatusManager.markAsNoLongerPendingInstall(urlString); appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Unknown, null); localBroadcastManager.unregisterReceiver(this); break; @@ -334,6 +338,7 @@ public class InstallManagerService extends Service { appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installing, null); break; case Installer.ACTION_INSTALL_COMPLETE: + appUpdateStatusManager.markAsNoLongerPendingInstall(downloadUrl); appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null); Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl); @@ -350,6 +355,7 @@ public class InstallManagerService extends Service { apk = intent.getParcelableExtra(Installer.EXTRA_APK); String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); + appUpdateStatusManager.markAsNoLongerPendingInstall(downloadUrl); if (!TextUtils.isEmpty(errorMessage)) { appUpdateStatusManager.setApkError(apk, errorMessage); } else { From 0d1e00b6cf5dabdd44610534a00ca8028664efc0 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 28 Apr 2017 08:33:58 +1000 Subject: [PATCH 3/5] Don't accidentally return Apk instances from the wrong repo There may be multiple apk files with the same hash. Although it is not a security issue to install one or the other (they are exactly the same binary), they may have different metadata to display in the client. Thus, it may result in weirdness if one has a different name/description/summary etc). This change takes each of the matching Apk objects from the database, then asks them where they expect to be downloaded. It matches this against the File that we are looking at and only returns if they match. --- .../fdroid/fdroid/AppUpdateStatusService.java | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java index 0b41dbfa7..2bb2991bc 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; @@ -68,33 +69,20 @@ public class AppUpdateStatusService extends IntentService { return null; } - // NOTE: This presumes SHA256 is the only supported hash. It seems like that is an assumption - // in more than one place in the F-Droid client. If this becomes a problem in the future, we - // can query the Apk table for `SELECT DISTINCT hashType FROM fdroid_apk` and then we can just - // try each of the hash types that have been specified in the metadata. Seems a bit overkill - // at the time of writing though. Utils.debugLog(TAG, "Found package for " + downloadedInfo.packageName + ", checking its hash to see if it downloaded correctly."); - String hash = Utils.getBinaryHash(apkPath, "sha256"); - - List apksMatchingHash = ApkProvider.Helper.findApksByHash(this, hash); - Utils.debugLog(TAG, "Found " + apksMatchingHash.size() + " apk(s) matching the hash " + hash); - - if (apksMatchingHash.size() == 0) { + Apk downloadedApk = findApkMatchingHash(apkPath); + if (downloadedApk == null) { Utils.debugLog(TAG, "Either the apk wasn't downloaded fully, or the repo it came from has been disabled. Either way, not notifying the user about it."); return null; } - // It makes zero difference which apk we get from this list. By definition they all have - // the exact same hash, and are thus the same binary. - Apk downloadedApk = apksMatchingHash.get(0); - PackageInfo installedInfo = null; try { installedInfo = getPackageManager().getPackageInfo(downloadedApk.packageName, PackageManager.GET_META_DATA); } catch (PackageManager.NameNotFoundException ignored) { } if (installedInfo == null) { - if (AppUpdateStatusManager.getInstance(this).isPendingInstall(hash)) { + if (AppUpdateStatusManager.getInstance(this).isPendingInstall(downloadedApk.hash)) { Utils.debugLog(TAG, downloadedApk.packageName + " is not installed, so presuming we need to notify the user about installing it."); return downloadedApk; } else { @@ -112,4 +100,35 @@ public class AppUpdateStatusService extends IntentService { return downloadedApk; } + + /** + * There could be multiple apks with the same hash, provided by different repositories. + * This method looks for all matching records in the database. It then asks each of these + * {@link Apk} instances where they expect to be downloaded. If they expect to be downloaded + * to {@param apkPath} then that instance is returned. + * + * If no files have a matching hash, or only those which don't belong to the correct repo, then + * this will return null. + */ + @Nullable + private Apk findApkMatchingHash(File apkPath) { + + // NOTE: This presumes SHA256 is the only supported hash. It seems like that is an assumption + // in more than one place in the F-Droid client. If this becomes a problem in the future, we + // can query the Apk table for `SELECT DISTINCT hashType FROM fdroid_apk` and then we can just + // try each of the hash types that have been specified in the metadata. Seems a bit overkill + // at the time of writing though. + String hash = Utils.getBinaryHash(apkPath, "sha256"); + + List apksMatchingHash = ApkProvider.Helper.findApksByHash(this, hash); + Utils.debugLog(TAG, "Found " + apksMatchingHash.size() + " apk(s) matching the hash " + hash); + + for (Apk apk : apksMatchingHash) { + if (apkPath.equals(ApkCache.getApkDownloadPath(this, Uri.parse(apk.getUrl())))) { + return apk; + } + } + + return null; + } } \ No newline at end of file From 25edfffcbec6ef24b2a82177e9691a7e94f8d5c9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 28 Apr 2017 08:38:02 +1000 Subject: [PATCH 4/5] Remove version check, allow version downgrades to appear in Updates tab correctly. The version check guarded against downgrades, and would not notify the user if it found a downgrade in the apk cache. However this was from before we could ask `AppUpdateStatusManager#isPendingInstall(hash)`. Now we don't need to care whether it is an upgrade or a downgrade, because there is a more authoritative source as to whether this apk is interesting to us or not. --- .../fdroid/fdroid/AppUpdateStatusService.java | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java index 2bb2991bc..bb767a64c 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java @@ -43,6 +43,7 @@ public class AppUpdateStatusService extends IntentService { @Override protected void onHandleIntent(@Nullable Intent intent) { + Utils.debugLog(TAG, "Scanning apk cache to see if we need to prompt the user to install any apks."); List apksReadyToInstall = new ArrayList<>(); File cacheDir = ApkCache.getApkCacheDir(this); for (String repoDirName : cacheDir.list()) { @@ -76,29 +77,13 @@ public class AppUpdateStatusService extends IntentService { return null; } - PackageInfo installedInfo = null; - try { - installedInfo = getPackageManager().getPackageInfo(downloadedApk.packageName, PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException ignored) { } - - if (installedInfo == null) { - if (AppUpdateStatusManager.getInstance(this).isPendingInstall(downloadedApk.hash)) { - Utils.debugLog(TAG, downloadedApk.packageName + " is not installed, so presuming we need to notify the user about installing it."); - return downloadedApk; - } else { - // It was probably downloaded for a previous install, and then subsequently removed - // (but stayed in the cache, as is the designed behaviour). Under these circumstances - // we don't want to tell the user to try and install it. - return null; - } - } - - if (installedInfo.versionCode >= downloadedInfo.versionCode) { - Utils.debugLog(TAG, downloadedApk.packageName + " already installed with versionCode " + installedInfo.versionCode + " and downloaded apk is only " + downloadedInfo.versionCode + ", so ignoring downloaded apk."); + if (AppUpdateStatusManager.getInstance(this).isPendingInstall(downloadedApk.hash)) { + Utils.debugLog(TAG, downloadedApk.packageName + " is pending install, so we need to notify the user about installing it."); + return downloadedApk; + } else { + Utils.debugLog(TAG, downloadedApk.packageName + " is NOT pending install, probably just left over from a previous install."); return null; } - - return downloadedApk; } /** From 69816b5b823fac4f70bd01d3956c0056d5d891d3 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 28 Apr 2017 09:12:53 +1000 Subject: [PATCH 5/5] Only and if there are any there. --- .../main/java/org/fdroid/fdroid/AppUpdateStatusService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java index bb767a64c..76e2eb2a4 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusService.java @@ -57,8 +57,10 @@ public class AppUpdateStatusService extends IntentService { } } - AppUpdateStatusManager.getInstance(this).addApks(apksReadyToInstall, AppUpdateStatusManager.Status.ReadyToInstall); - InstallManagerService.managePreviouslyDownloadedApks(this); + if (apksReadyToInstall.size() > 0) { + AppUpdateStatusManager.getInstance(this).addApks(apksReadyToInstall, AppUpdateStatusManager.Status.ReadyToInstall); + InstallManagerService.managePreviouslyDownloadedApks(this); + } } @Nullable