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;