diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 3019ace1e..328854bf4 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -362,7 +362,7 @@ public class AppDetails2 extends AppCompatActivity if (resultCode == Activity.RESULT_OK) { Uri uri = data.getData(); Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL); - startInstall(apk); + InstallManagerService.queue(this, app, apk); } break; case REQUEST_UNINSTALL_DIALOG: @@ -373,6 +373,12 @@ public class AppDetails2 extends AppCompatActivity } } + @Override + public void installApk() { + Apk apkToInstall = ApkProvider.Helper.findSuggestedApk(this, app); + installApk(apkToInstall); + } + // Install the version of this app denoted by 'app.curApk'. @Override public void installApk(final Apk apk) { @@ -426,12 +432,6 @@ public class AppDetails2 extends AppCompatActivity } 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); Intent intent = installer.getPermissionScreen(); if (intent != null) { @@ -441,10 +441,6 @@ public class AppDetails2 extends AppCompatActivity return; } - startInstall(apk); - } - - private void startInstall(Apk apk) { InstallManagerService.queue(this, app, apk); } @@ -485,6 +481,7 @@ public class AppDetails2 extends AppCompatActivity } switch (newStatus.status) { + case PendingInstall: case Downloading: if (newStatus.progressMax == 0) { // The first progress notification we get telling us our status is "Downloading" @@ -708,11 +705,6 @@ public class AppDetails2 extends AppCompatActivity }); } - @Override - public boolean isAppDownloading() { - return currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Downloading; - } - @Override public void enableAndroidBeam() { NfcHelper.setAndroidBeam(this, app.packageName); @@ -737,7 +729,7 @@ public class AppDetails2 extends AppCompatActivity @Override public void installCancel() { - if (isAppDownloading()) { + if (currentStatus != null) { InstallManagerService.cancel(this, currentStatus.getUniqueKey()); } } @@ -753,18 +745,6 @@ public class AppDetails2 extends AppCompatActivity } } - @Override - public void installApk() { - Apk apkToInstall = ApkProvider.Helper.findSuggestedApk(this, app); - installApk(apkToInstall); - } - - @Override - public void upgradeApk() { - Apk apkToInstall = ApkProvider.Helper.findSuggestedApk(this, app); - installApk(apkToInstall); - } - /** * Uninstall the app from the current screen. Since there are many ways * to uninstall an app, including from Google Play, {@code adb uninstall}, diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 939a47a39..c7ebc23da 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -91,13 +91,14 @@ public final class AppUpdateStatusManager { private static final String LOGTAG = "AppUpdateStatusManager"; public enum Status { + PendingInstall, DownloadInterrupted, UpdateAvailable, Downloading, ReadyToInstall, Installing, Installed, - InstallError + InstallError, } public static AppUpdateStatusManager getInstance(Context context) { @@ -426,6 +427,7 @@ public final class AppUpdateStatusManager { entry.errorText = errorText; entry.intent = null; notifyChange(entry, true); + removeApk(url); } } } diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index aab59045a..b3b6c47f0 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -230,6 +230,7 @@ class NotificationHelper { case UpdateAvailable: return new NotificationCompat.Action(R.drawable.ic_file_download, context.getString(R.string.notification_action_update), entry.intent); + case PendingInstall: case Downloading: case Installing: return new NotificationCompat.Action(R.drawable.ic_cancel, context.getString(R.string.notification_action_cancel), entry.intent); @@ -245,6 +246,7 @@ class NotificationHelper { switch (status) { case UpdateAvailable: return context.getString(R.string.notification_title_single_update_available); + case PendingInstall: case Downloading: return app.name; case ReadyToInstall: @@ -263,6 +265,7 @@ class NotificationHelper { switch (status) { case UpdateAvailable: return app.name; + case PendingInstall: case Downloading: return context.getString(app.isInstalled(context) ? R.string.notification_content_single_downloading_update : R.string.notification_content_single_downloading, app.name); case ReadyToInstall: @@ -281,6 +284,7 @@ class NotificationHelper { switch (status) { case UpdateAvailable: return context.getString(R.string.notification_title_summary_update_available); + case PendingInstall: case Downloading: return context.getString(app.isInstalled(context) ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading); case ReadyToInstall: diff --git a/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java index 2e51f267f..d610c4371 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java @@ -145,7 +145,6 @@ public class FileInstallerActivity extends FragmentActivity { private void installPackage(Uri localApkUri, Uri downloadUri, Apk apk) { Utils.debugLog(TAG, "Installing: " + localApkUri.getPath()); - installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED); File path = apk.getMediaInstallPath(activity.getApplicationContext()); path.mkdirs(); try { 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 2c5440dee..50d63e3e4 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -34,7 +34,13 @@ import java.io.IOException; /** * Manages the whole process when a background update triggers an install or the user * requests an APK to be installed. It handles checking whether the APK is cached, - * downloading it, putting up and maintaining a {@link Notification}, and more. + * downloading it, putting up and maintaining a {@link Notification}, and more. This + * {@code Service} tracks packages that are in the process as "Pending Installs". + * Then {@link DownloaderService} and {@link InstallerService} individually track + * packages for those phases of the whole install process. Each of those + * {@code Services} have their own related events. For tracking status during the + * whole process, {@link AppUpdateStatusManager} tracks the status as represented by + * {@link AppUpdateStatusManager.AppUpdateStatus}. *
* The {@link App} and {@link Apk} instances are sent via * {@link Intent#putExtra(String, android.os.Bundle)} @@ -103,7 +109,6 @@ public class InstallManagerService extends Service { @Override public void onCreate() { super.onCreate(); - Utils.debugLog(TAG, "creating Service"); localBroadcastManager = LocalBroadcastManager.getInstance(this); appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); @@ -171,7 +176,6 @@ public class InstallManagerService extends Service { DownloaderService.cancel(this, apk.getPatchObbUrl()); DownloaderService.cancel(this, apk.getMainObbUrl()); } - appUpdateStatusManager.removeApk(urlString); return START_NOT_STICKY; } else if (ACTION_INSTALL.equals(action)) { if (!isPendingInstall(urlString)) { @@ -455,14 +459,12 @@ public class InstallManagerService extends Service { */ public static void queue(Context context, App app, @NonNull Apk apk) { String urlString = apk.getUrl(); + AppUpdateStatusManager.getInstance(context).addApk(apk, AppUpdateStatusManager.Status.PendingInstall, null); putPendingInstall(context, urlString, apk.packageName); - Uri downloadUri = Uri.parse(urlString); - Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk, - null, null); Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString); Intent intent = new Intent(context, InstallManagerService.class); intent.setAction(ACTION_INSTALL); - intent.setData(downloadUri); + intent.setData(Uri.parse(urlString)); intent.putExtra(EXTRA_APP, app); intent.putExtra(EXTRA_APK, apk); context.startService(intent); @@ -487,12 +489,25 @@ public class InstallManagerService extends Service { return pendingInstalls.contains(urlString); } + /** + * Look up by {@code packageName} whether it is a Pending Install. + * + * @see #isPendingInstall(String) + */ + public static boolean isPendingInstall(Context context, String packageName) { + if (pendingInstalls == null) { + pendingInstalls = getPendingInstalls(context); + } + return pendingInstalls.getAll().values().contains(packageName); + } + /** * Mark a given APK as in the process of being installed, with * the {@code urlString} of the download used as the unique ID, * and the file hash used to verify that things are the same. * * @see #isPendingInstall(String) + * @see #isPendingInstall(Context, String) */ public static void putPendingInstall(Context context, String urlString, String packageName) { if (pendingInstalls == null) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java index ee47f5050..7002e435f 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java @@ -115,6 +115,8 @@ public class InstallerService extends JobIntentService { * @see #uninstall(Context, Apk) */ public static void install(Context context, Uri localApkUri, Uri downloadUri, Apk apk) { + Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk, + null, null); Intent intent = new Intent(context, InstallerService.class); intent.setAction(ACTION_INSTALL); intent.setData(localApkUri); diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java index 84bdbf378..150aabbc5 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java @@ -43,6 +43,7 @@ import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.privileged.views.AppDiff; import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; import org.fdroid.fdroid.views.main.MainActivity; @@ -57,8 +58,6 @@ public class AppDetailsRecyclerViewAdapter public interface AppDetailsRecyclerViewAdapterCallbacks { - boolean isAppDownloading(); - void enableAndroidBeam(); void disableAndroidBeam(); @@ -69,8 +68,6 @@ public class AppDetailsRecyclerViewAdapter void installApk(Apk apk); - void upgradeApk(); - void uninstallApk(); void installCancel(); @@ -378,11 +375,17 @@ public class AppDetailsRecyclerViewAdapter progressLabel.setText(resIdString); progressLabel.setContentDescription(context.getString(R.string.downloading)); progressPercent.setText(""); + if (resIdString == R.string.installing || resIdString == R.string.uninstalling) { + progressCancel.setVisibility(View.GONE); + } else { + progressCancel.setVisibility(View.VISIBLE); + } } public void setProgress(long bytesDownloaded, long totalBytes) { progressLayout.setVisibility(View.VISIBLE); buttonLayout.setVisibility(View.GONE); + progressCancel.setVisibility(View.VISIBLE); progressBar.setMax(Utils.bytesToKb(totalBytes)); progressBar.setProgress(Utils.bytesToKb(bytesDownloaded)); @@ -484,9 +487,11 @@ public class AppDetailsRecyclerViewAdapter buttonSecondaryView.setOnClickListener(onUnInstallClickListener); buttonPrimaryView.setText(R.string.menu_install); buttonPrimaryView.setVisibility(versions.size() > 0 ? View.VISIBLE : View.GONE); - if (callbacks.isAppDownloading()) { + if (InstallManagerService.isPendingInstall(context, app.packageName)) { buttonPrimaryView.setText(R.string.downloading); buttonPrimaryView.setEnabled(false); + buttonLayout.setVisibility(View.GONE); + progressLayout.setVisibility(View.VISIBLE); } else if (!app.isInstalled(context) && suggestedApk != null) { // Check count > 0 due to incompatible apps resulting in an empty list. callbacks.disableAndroidBeam(); @@ -494,6 +499,8 @@ public class AppDetailsRecyclerViewAdapter buttonPrimaryView.setText(R.string.menu_install); buttonPrimaryView.setOnClickListener(onInstallClickListener); buttonPrimaryView.setEnabled(true); + buttonLayout.setVisibility(View.VISIBLE); + progressLayout.setVisibility(View.GONE); } else if (app.isInstalled(context)) { callbacks.enableAndroidBeam(); if (app.canAndWantToUpdate(context) && suggestedApk != null) { @@ -508,10 +515,8 @@ public class AppDetailsRecyclerViewAdapter } } buttonPrimaryView.setEnabled(true); - } - if (callbacks.isAppDownloading()) { - buttonLayout.setVisibility(View.GONE); - progressLayout.setVisibility(View.VISIBLE); + buttonLayout.setVisibility(View.VISIBLE); + progressLayout.setVisibility(View.GONE); } else { buttonLayout.setVisibility(View.VISIBLE); progressLayout.setVisibility(View.GONE); @@ -1097,7 +1102,7 @@ public class AppDetailsRecyclerViewAdapter private final View.OnClickListener onUpgradeClickListener = new View.OnClickListener() { @Override public void onClick(View v) { - callbacks.upgradeApk(); + callbacks.installApk(); } }; diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java index 1e110ffd3..eaa78f14e 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java @@ -56,7 +56,7 @@ import java.util.Iterator; * *
* The state of the UI is defined in a dumb {@link AppListItemState} class, then applied to the UI - * in the {@link #refreshView(App, AppUpdateStatus)} method. + * in the {@link #updateAppStatus(App, AppUpdateStatus)} method. */ public abstract class AppListItemController extends RecyclerView.ViewHolder { @@ -220,15 +220,10 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { } /** - * Updates both the progress bar and the circular install button (which shows progress around the outside of - * the circle). Also updates the app label to indicate that the app is being downloaded. - */ - private void updateAppStatus(@NonNull App app, @Nullable AppUpdateStatus status) { - currentStatus = status; - refreshView(app, status); - } - - /** + * Updates both the progress bar and the circular install button (which + * shows progress around the outside of the circle). Also updates the app + * label to indicate that the app is being downloaded. + *
* Queries the current state via {@link #getCurrentViewState(App, AppUpdateStatus)} * and then updates the relevant widgets depending on that state. *
@@ -238,7 +233,8 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { * @see AppListItemState * @see #getCurrentViewState(App, AppUpdateStatus) */ - private void refreshView(@NonNull App app, @Nullable AppUpdateStatus appStatus) { + private void updateAppStatus(@NonNull App app, @Nullable AppUpdateStatus appStatus) { + currentStatus = appStatus; AppListItemState viewState = getCurrentViewState(app, appStatus); @@ -289,12 +285,14 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { if (viewState.shouldShowActionButton()) { installButton.setVisibility(View.GONE); } else if (viewState.showProgress()) { + installButton.setEnabled(false); installButton.setVisibility(View.VISIBLE); installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress)); int progressAsDegrees = viewState.getProgressMax() <= 0 ? 0 : (int) (((float) viewState.getProgressCurrent() / viewState.getProgressMax()) * 360); installButton.setImageLevel(progressAsDegrees); } else if (viewState.shouldShowInstall()) { + installButton.setEnabled(true); installButton.setVisibility(View.VISIBLE); installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download)); } else { @@ -332,9 +330,13 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { case ReadyToInstall: return getViewStateReadyToInstall(app); + case PendingInstall: case Downloading: return getViewStateDownloading(app, appStatus); + case Installing: + return getViewStateInstalling(app); + case Installed: return getViewStateInstalled(app); @@ -344,6 +346,16 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { } } + protected AppListItemState getViewStateInstalling(@NonNull App app) { + CharSequence mainText = activity.getString( + R.string.app_list__name__downloading_in_progress, app.name); + + return new AppListItemState(app) + .setMainText(mainText) + .showActionButton(null) + .setStatusText(activity.getString(R.string.notification_content_single_installing, app.name)); + } + protected AppListItemState getViewStateInstalled(@NonNull App app) { CharSequence mainText = activity.getString( R.string.app_list__name__successfully_installed, app.name); diff --git a/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java b/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java index 8a904f4e4..1a829fddf 100644 --- a/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java @@ -101,10 +101,6 @@ public class AppDetailsAdapterTest extends FDroidProviderTest { } private final AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks dummyCallbacks = new AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks() { // NOCHECKSTYLE LineLength - @Override - public boolean isAppDownloading() { - return false; - } @Override public void enableAndroidBeam() { @@ -131,11 +127,6 @@ public class AppDetailsAdapterTest extends FDroidProviderTest { } - @Override - public void upgradeApk() { - - } - @Override public void uninstallApk() {