Merge branch 'fix-962--notify-of-downloaded' into 'master'

Show downloaded + not installed apps in "Updates"

Closes #962

See merge request !488
This commit is contained in:
Hans-Christoph Steiner 2017-04-28 08:38:12 +00:00
commit 561a18ad2b
4 changed files with 150 additions and 14 deletions

View File

@ -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<String, AppUpdateStatus> 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);
}
}

View File

@ -3,17 +3,18 @@ package org.fdroid.fdroid;
import android.app.IntentService;
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;
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 +24,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 +43,79 @@ public class AppUpdateStatusService extends IntentService {
@Override
protected void onHandleIntent(@Nullable Intent intent) {
List<App> apps = AppProvider.Helper.findCanUpdate(this, Schema.AppMetadataTable.Cols.ALL);
Utils.debugLog(TAG, "Scanning apk cache to see if we need to prompt the user to install any apks.");
List<Apk> 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);
if (apksReadyToInstall.size() > 0) {
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;
}
Utils.debugLog(TAG, "Found package for " + downloadedInfo.packageName + ", checking its hash to see if it downloaded correctly.");
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;
}
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;
}
}
/**
* 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<Apk> 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;
}
}

View File

@ -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<Apk> 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;

View File

@ -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 {