Listen to AppUpdateStatusManager events instead of DownloadManager events.

Also, make sure to correctly update the app details view when te user
leaves then returns to the view. Prior to this, the user would need to
wait for a download event to be received. However even that was broken,
because the download listener was not being added correctly once the
user returned to the app details screen.
This commit is contained in:
Peter Serwylo 2017-05-26 09:21:42 +10:00
parent 7c0c5b2490
commit ee7055e118
7 changed files with 153 additions and 74 deletions

View File

@ -28,6 +28,7 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.database.ContentObserver; import android.database.ContentObserver;
@ -35,6 +36,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout; import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.CoordinatorLayout;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
@ -65,13 +67,13 @@ import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerFactory; import org.fdroid.fdroid.installer.InstallerFactory;
import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter; import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter;
import org.fdroid.fdroid.views.OverscrollLinearLayoutManager; import org.fdroid.fdroid.views.OverscrollLinearLayoutManager;
import org.fdroid.fdroid.views.ShareChooserDialog; import org.fdroid.fdroid.views.ShareChooserDialog;
import org.fdroid.fdroid.views.apps.FeatureImage; import org.fdroid.fdroid.views.apps.FeatureImage;
import java.util.Iterator;
public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks { public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks {
public static final String EXTRA_APPID = "appid"; public static final String EXTRA_APPID = "appid";
@ -88,7 +90,7 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
private RecyclerView recyclerView; private RecyclerView recyclerView;
private AppDetailsRecyclerViewAdapter adapter; private AppDetailsRecyclerViewAdapter adapter;
private LocalBroadcastManager localBroadcastManager; private LocalBroadcastManager localBroadcastManager;
private String activeDownloadUrlString; private AppUpdateStatusManager.AppUpdateStatus currentStatus;
/** /**
* Check if {@code packageName} is currently visible to the user. * Check if {@code packageName} is currently visible to the user.
@ -125,7 +127,10 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
OverscrollLinearLayoutManager lm = new OverscrollLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); OverscrollLinearLayoutManager lm = new OverscrollLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
lm.setStackFromEnd(false); lm.setStackFromEnd(false);
/** The recyclerView/AppBarLayout combo has a bug that prevents a "fling" from the bottom // Has to be invoked after AppDetailsRecyclerViewAdapter is created.
refreshStatus();
/* The recyclerView/AppBarLayout combo has a bug that prevents a "fling" from the bottom
* to continue all the way to the top by expanding the AppBarLayout. It will instead stop * to continue all the way to the top by expanding the AppBarLayout. It will instead stop
* with the app bar in a collapsed state. See here: https://code.google.com/p/android/issues/detail?id=177729 * with the app bar in a collapsed state. See here: https://code.google.com/p/android/issues/detail?id=177729
* Not sure this is the exact issue, but it is true that while in a fling the RecyclerView will * Not sure this is the exact issue, but it is true that while in a fling the RecyclerView will
@ -234,6 +239,28 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
appObserver); appObserver);
updateNotificationsForApp(); updateNotificationsForApp();
refreshStatus();
registerAppStatusReceiver();
}
/**
* Figures out the current install/update/download/etc status for the app we are viewing.
* Then, asks the view to update itself to reflect this status.
*/
private void refreshStatus() {
Iterator<AppUpdateStatusManager.AppUpdateStatus> statuses = AppUpdateStatusManager.getInstance(this).getByPackageName(app.packageName).iterator();
if (statuses.hasNext()) {
AppUpdateStatusManager.AppUpdateStatus status = statuses.next();
updateAppStatus(status, false);
}
currentStatus = null;
}
@Override
protected void onPause() {
super.onPause();
unregisterAppStatusReceiver();
} }
protected void onStop() { protected void onStop() {
@ -395,6 +422,11 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
} }
private void initiateInstall(Apk apk) { private void initiateInstall(Apk apk) {
if (isAppDownloading()) {
Log.i(TAG, "Ignoring request to install " + apk.packageName + " version " + apk.versionName + ", as we are already downloading (either that or a different version).");
return;
}
Installer installer = InstallerFactory.create(this, apk); Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getPermissionScreen(); Intent intent = installer.getPermissionScreen();
if (intent != null) { if (intent != null) {
@ -408,8 +440,6 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
} }
private void startInstall(Apk apk) { private void startInstall(Apk apk) {
activeDownloadUrlString = apk.getUrl();
registerDownloaderReceiver();
InstallManagerService.queue(this, app, apk); InstallManagerService.queue(this, app, apk);
} }
@ -427,52 +457,87 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
localBroadcastManager.unregisterReceiver(uninstallReceiver); localBroadcastManager.unregisterReceiver(uninstallReceiver);
} }
private void registerDownloaderReceiver() { private void registerAppStatusReceiver() {
if (activeDownloadUrlString != null) { // if a download is active IntentFilter filter = new IntentFilter();
String url = activeDownloadUrlString; filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
localBroadcastManager.registerReceiver(downloadReceiver, filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED);
DownloaderService.getIntentFilter(url)); filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED);
} localBroadcastManager.registerReceiver(appStatusReceiver, filter);
} }
private void unregisterDownloaderReceiver() { private void unregisterAppStatusReceiver() {
localBroadcastManager.unregisterReceiver(downloadReceiver); localBroadcastManager.unregisterReceiver(appStatusReceiver);
} }
private void unregisterInstallReceiver() { private void unregisterInstallReceiver() {
localBroadcastManager.unregisterReceiver(installReceiver); localBroadcastManager.unregisterReceiver(installReceiver);
} }
private final BroadcastReceiver downloadReceiver = new BroadcastReceiver() { private void updateAppStatus(@Nullable AppUpdateStatusManager.AppUpdateStatus newStatus, boolean justReceived) {
@Override this.currentStatus = newStatus;
public void onReceive(Context context, Intent intent) { if (this.currentStatus == null) {
switch (intent.getAction()) { return;
case Downloader.ACTION_STARTED: }
switch (newStatus.status) {
case PendingDownload:
adapter.setProgress(-1, -1, R.string.download_pending); adapter.setProgress(-1, -1, R.string.download_pending);
break; break;
case Downloader.ACTION_PROGRESS:
adapter.setProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1), case Downloading:
intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1), 0); if (newStatus.progressMax == 0) {
break; // The first progress notification we get telling us our status is "Downloading"
case Downloader.ACTION_COMPLETE: adapter.setProgress(-1, -1, 0);
// Starts the install process once the download is complete. } else {
cleanUpFinishedDownload(); adapter.setProgress(newStatus.progressCurrent, newStatus.progressMax, 0);
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(intent.getData()));
break;
case Downloader.ACTION_INTERRUPTED:
if (intent.hasExtra(Downloader.EXTRA_ERROR_MESSAGE)) {
String msg = intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)
+ " " + intent.getDataString();
Toast.makeText(context, R.string.download_error, Toast.LENGTH_SHORT).show();
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
} else { // user canceled
Toast.makeText(context, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
} }
cleanUpFinishedDownload();
break; break;
default:
throw new RuntimeException("intent action not handled!"); case ReadyToInstall:
if (justReceived) {
adapter.clearProgress();
localBroadcastManager.registerReceiver(installReceiver, Installer.getInstallIntentFilter(Uri.parse(newStatus.getUniqueKey())));
}
break;
case DownloadInterrupted:
if (justReceived) {
if (TextUtils.isEmpty(newStatus.errorText)) {
Toast.makeText(this, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
} else {
String msg = newStatus.errorText + " " + newStatus.getUniqueKey();
Toast.makeText(this, R.string.download_error, Toast.LENGTH_SHORT).show();
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
adapter.clearProgress();
}
break;
case Installing:
case Installed:
case UpdateAvailable:
case InstallError:
// Ignore.
break;
}
}
private final BroadcastReceiver appStatusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String apkUrl = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
AppUpdateStatusManager.AppUpdateStatus status = AppUpdateStatusManager.getInstance(context).get(apkUrl);
boolean isRemoving = TextUtils.equals(intent.getAction(), AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
// !TextUtils.equals(status.apk.packageName, app.packageName)
if (status == null && currentStatus != null && isRemoving && !TextUtils.equals(apkUrl, currentStatus.getUniqueKey())) {
Utils.debugLog(TAG, "Ignoring app status change because it belongs to " + apkUrl + " not " + currentStatus.getUniqueKey());
} 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);
} else {
updateAppStatus(status, true);
} }
} }
}; };
@ -602,8 +667,6 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
Utils.debugLog(TAG, "Getting application details for " + packageName); Utils.debugLog(TAG, "Getting application details for " + packageName);
App newApp = null; App newApp = null;
calcActiveDownloadUrlString(packageName);
if (!TextUtils.isEmpty(packageName)) { if (!TextUtils.isEmpty(packageName)) {
newApp = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), packageName); newApp = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), packageName);
} }
@ -612,25 +675,6 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
return this.app != null; return this.app != null;
} }
private void calcActiveDownloadUrlString(String packageName) {
String urlString = getPreferences(MODE_PRIVATE).getString(packageName, null);
if (DownloaderService.isQueuedOrActive(urlString)) {
activeDownloadUrlString = urlString;
} else {
// this URL is no longer active, remove it
getPreferences(MODE_PRIVATE).edit().remove(packageName).apply();
}
}
/**
* Remove progress listener, suppress progress bar, set downloadHandler to null.
*/
private void cleanUpFinishedDownload() {
activeDownloadUrlString = null;
adapter.clearProgress();
unregisterDownloaderReceiver();
}
private void onAppChanged() { private void onAppChanged() {
recyclerView.post(new Runnable() { recyclerView.post(new Runnable() {
@Override @Override
@ -641,6 +685,7 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
} }
AppDetailsRecyclerViewAdapter adapter = (AppDetailsRecyclerViewAdapter) recyclerView.getAdapter(); AppDetailsRecyclerViewAdapter adapter = (AppDetailsRecyclerViewAdapter) recyclerView.getAdapter();
adapter.updateItems(app); adapter.updateItems(app);
refreshStatus();
supportInvalidateOptionsMenu(); supportInvalidateOptionsMenu();
} }
}); });
@ -648,7 +693,8 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
@Override @Override
public boolean isAppDownloading() { public boolean isAppDownloading() {
return !TextUtils.isEmpty(activeDownloadUrlString); return currentStatus != null &&
(currentStatus.status == AppUpdateStatusManager.Status.PendingDownload || currentStatus.status == AppUpdateStatusManager.Status.Downloading);
} }
@Override @Override
@ -675,8 +721,9 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
@Override @Override
public void installCancel() { public void installCancel() {
if (!TextUtils.isEmpty(activeDownloadUrlString)) { if (isAppDownloading()) {
InstallManagerService.cancel(this, activeDownloadUrlString); InstallManagerService.cancel(this, currentStatus.getUniqueKey());
adapter.clearProgress();
} }
} }

View File

@ -83,7 +83,8 @@ public final class AppUpdateStatusManager {
private static final String LOGTAG = "AppUpdateStatusManager"; private static final String LOGTAG = "AppUpdateStatusManager";
public enum Status { public enum Status {
Unknown, PendingDownload,
DownloadInterrupted,
UpdateAvailable, UpdateAvailable,
Downloading, Downloading,
ReadyToInstall, ReadyToInstall,
@ -120,6 +121,13 @@ public final class AppUpdateStatusManager {
public String getUniqueKey() { public String getUniqueKey() {
return apk.getUrl(); return apk.getUrl();
} }
/**
* Dumps some information about the status for debugging purposes.
*/
public String toString() {
return app.packageName + " [Status: " + status + ", Progress: " + progressCurrent + " / " + progressMax + "]";
}
} }
private final Context context; private final Context context;
@ -316,6 +324,21 @@ 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) {
synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(url);
if (entry != null) {
entry.status = Status.DownloadInterrupted;
entry.errorText = errorText;
entry.intent = null;
notifyChange(entry, true);
}
}
}
public void setApkError(Apk apk, String errorText) { public void setApkError(Apk apk, String errorText) {
synchronized (appMapping) { synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(apk.getUrl()); AppUpdateStatus entry = appMapping.get(apk.getUrl());

View File

@ -148,9 +148,12 @@ class NotificationHelper {
private boolean shouldIgnoreEntry(AppUpdateStatusManager.AppUpdateStatus entry) { private boolean shouldIgnoreEntry(AppUpdateStatusManager.AppUpdateStatus entry) {
// Ignore unknown status // Ignore unknown status
if (entry.status == AppUpdateStatusManager.Status.Unknown) { if (entry.status == AppUpdateStatusManager.Status.DownloadInterrupted) {
return true; return true;
} else if ((entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.ReadyToInstall || entry.status == AppUpdateStatusManager.Status.InstallError) && } else if ((entry.status == AppUpdateStatusManager.Status.Downloading ||
entry.status == AppUpdateStatusManager.Status.PendingDownload ||
entry.status == AppUpdateStatusManager.Status.ReadyToInstall ||
entry.status == AppUpdateStatusManager.Status.InstallError) &&
AppDetails2.isAppVisible(entry.app.packageName)) { AppDetails2.isAppVisible(entry.app.packageName)) {
// Ignore downloading, readyToInstall and installError if we are showing the details screen for this app // Ignore downloading, readyToInstall and installError if we are showing the details screen for this app
return true; return true;

View File

@ -166,7 +166,7 @@ public class InstallManagerService extends Service {
return START_NOT_STICKY; return START_NOT_STICKY;
} }
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Unknown, null); appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.PendingDownload, null);
appUpdateStatusManager.markAsPendingInstall(urlString); appUpdateStatusManager.markAsPendingInstall(urlString);
registerApkDownloaderReceivers(urlString); registerApkDownloaderReceivers(urlString);
@ -270,7 +270,7 @@ public class InstallManagerService extends Service {
switch (intent.getAction()) { switch (intent.getAction()) {
case Downloader.ACTION_STARTED: case Downloader.ACTION_STARTED:
// App should currently be in the "Unknown" state, so this changes it to "Downloading". // App should currently be in the "PendingDownload" state, so this changes it to "Downloading".
Intent intentObject = new Intent(context, InstallManagerService.class); Intent intentObject = new Intent(context, InstallManagerService.class);
intentObject.setAction(ACTION_CANCEL); intentObject.setAction(ACTION_CANCEL);
intentObject.setData(downloadUri); intentObject.setData(downloadUri);
@ -299,7 +299,7 @@ public class InstallManagerService extends Service {
break; break;
case Downloader.ACTION_INTERRUPTED: case Downloader.ACTION_INTERRUPTED:
appUpdateStatusManager.markAsNoLongerPendingInstall(urlString); appUpdateStatusManager.markAsNoLongerPendingInstall(urlString);
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Unknown, null); appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
break; break;
default: default:

View File

@ -33,6 +33,7 @@ import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.installer.ApkCache; import org.fdroid.fdroid.installer.ApkCache;
@ -200,7 +201,11 @@ public class DownloaderService extends Service {
} }
}); });
downloader.download(); downloader.download();
if (downloader.isNotFound()) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile, getString(R.string.download_404));
} else {
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile);
} catch (IOException e) { } catch (IOException e) {

View File

@ -98,7 +98,7 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
* {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#UpdateAvailable} are not interesting here. * {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#UpdateAvailable} are not interesting here.
*/ */
private boolean shouldShowStatus(AppUpdateStatusManager.AppUpdateStatus status) { private boolean shouldShowStatus(AppUpdateStatusManager.AppUpdateStatus status) {
return status.status == AppUpdateStatusManager.Status.Unknown || return status.status == AppUpdateStatusManager.Status.PendingDownload ||
status.status == AppUpdateStatusManager.Status.Downloading || status.status == AppUpdateStatusManager.Status.Downloading ||
status.status == AppUpdateStatusManager.Status.Installed || status.status == AppUpdateStatusManager.Status.Installed ||
status.status == AppUpdateStatusManager.Status.ReadyToInstall; status.status == AppUpdateStatusManager.Status.ReadyToInstall;

View File

@ -255,6 +255,7 @@
- Downloaded size (human readable) - Downloaded size (human readable)
--> -->
<string name="status_download_unknown_size">Downloading\n%2$s from\n%1$s</string> <string name="status_download_unknown_size">Downloading\n%2$s from\n%1$s</string>
<string name="download_404">The requested file was not found.</string>
<string name="update_notification_title">Updating repositories</string> <string name="update_notification_title">Updating repositories</string>
<string name="status_processing_xml_percent">Processing %2$s / %3$s (%4$d%%) from %1$s</string> <string name="status_processing_xml_percent">Processing %2$s / %3$s (%4$d%%) from %1$s</string>
<string name="status_connecting_to_repo">Connecting to\n%1$s</string> <string name="status_connecting_to_repo">Connecting to\n%1$s</string>