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 b9a0b1168..935a0f720 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 @@ -32,6 +32,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import org.fdroid.fdroid.AppDetails2; import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; @@ -41,6 +42,7 @@ import org.fdroid.fdroid.installer.ApkCache; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.InstallerFactory; +import org.fdroid.fdroid.views.updates.DismissResult; import java.io.File; import java.util.Iterator; @@ -57,7 +59,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, AppUpdateStatusManager.AppUpdateStatus)} method. + * in the {@link #refreshView(App, AppUpdateStatus)} method. */ public abstract class AppListItemController extends RecyclerView.ViewHolder { @@ -102,7 +104,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { private App currentApp; @Nullable - private AppUpdateStatusManager.AppUpdateStatus currentStatus; + private AppUpdateStatus currentStatus; @TargetApi(21) public AppListItemController(final Activity activity, View itemView) { @@ -159,6 +161,11 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { itemView.setOnClickListener(onAppClicked); } + @Nullable + protected final AppUpdateStatus getCurrentStatus() { + return currentStatus; + } + public void bindModel(@NonNull App app) { currentApp = app; @@ -166,10 +173,10 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { // 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. - Iterator statuses = + Iterator statuses = AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName).iterator(); if (statuses.hasNext()) { - AppUpdateStatusManager.AppUpdateStatus status = statuses.next(); + AppUpdateStatus status = statuses.next(); updateAppStatus(app, status); } else { updateAppStatus(app, null); @@ -186,27 +193,56 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { broadcastManager.registerReceiver(onStatusChanged, intentFilter); } + /** To be overridden if required */ + public boolean canDismiss() { + return false; + } + + /** + * If able, forwards the request onto {@link #onDismissApp(App)}. + * This mainly exists to keep the API consistent, in that the {@link App} is threaded through to the relevant + * method with a guarantee that it is not null, rather than every method having to check if it is null or not. + */ + @NonNull + public final DismissResult onDismiss() { + if (currentApp != null && canDismiss()) { + return onDismissApp(currentApp); + } + + return new DismissResult(); + } + + /** + * Override to respond to the user swiping an app to dismiss it from the list. + * @return Optionally return a description of what you did if it is not obvious to the user. It will be shown as + * a {@link android.widget.Toast} for a {@link android.widget.Toast#LENGTH_SHORT} time. + * @see #canDismiss() This must also be overriden and should return true. + */ + @NonNull + protected DismissResult onDismissApp(@NonNull App app) { + return new DismissResult(); + } + /** * 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 AppUpdateStatusManager.AppUpdateStatus status) { + private void updateAppStatus(@NonNull App app, @Nullable AppUpdateStatus status) { currentStatus = status; refreshView(app, status); } /** - * Queries the current state via {@link #getCurrentViewState(App, AppUpdateStatusManager.AppUpdateStatus)} + * Queries the current state via {@link #getCurrentViewState(App, AppUpdateStatus)} * and then updates the relevant widgets depending on that state. * * Should contain little to no business logic, this all belongs to - * {@link #getCurrentViewState(App, AppUpdateStatusManager.AppUpdateStatus)}. + * {@link #getCurrentViewState(App, AppUpdateStatus)}. * * @see AppListItemState - * @see #getCurrentViewState(App, AppUpdateStatusManager.AppUpdateStatus) + * @see #getCurrentViewState(App, AppUpdateStatus) */ - private void refreshView(@NonNull App app, - @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) { + private void refreshView(@NonNull App app, @Nullable AppUpdateStatus appStatus) { AppListItemState viewState = getCurrentViewState(app, appStatus); @@ -292,8 +328,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { } @NonNull - protected AppListItemState getCurrentViewState( - @NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) { + protected AppListItemState getCurrentViewState(@NonNull App app, @Nullable AppUpdateStatus appStatus) { if (appStatus == null) { return getViewStateDefault(app); } else { @@ -328,8 +363,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { return state; } - protected AppListItemState getViewStateDownloading( - @NonNull App app, @NonNull AppUpdateStatusManager.AppUpdateStatus currentStatus) { + protected AppListItemState getViewStateDownloading(@NonNull App app, @NonNull AppUpdateStatus currentStatus) { CharSequence mainText = activity.getString( R.string.app_list__name__downloading_in_progress, app.name); @@ -384,8 +418,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { private final BroadcastReceiver onStatusChanged = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - AppUpdateStatusManager.AppUpdateStatus newStatus = - intent.getParcelableExtra(AppUpdateStatusManager.EXTRA_STATUS); + AppUpdateStatus newStatus = intent.getParcelableExtra(AppUpdateStatusManager.EXTRA_STATUS); if (currentApp == null || !TextUtils.equals(newStatus.app.packageName, currentApp.packageName) @@ -477,11 +510,15 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { private final View.OnClickListener onCancelDownload = new View.OnClickListener() { @Override public void onClick(View v) { - if (currentStatus == null || currentStatus.status != AppUpdateStatusManager.Status.Downloading) { - return; - } - - InstallManagerService.cancel(activity, currentStatus.getUniqueKey()); + cancelDownload(); } }; + + protected final void cancelDownload() { + if (currentStatus == null || currentStatus.status != AppUpdateStatusManager.Status.Downloading) { + return; + } + + InstallManagerService.cancel(activity, currentStatus.getUniqueKey()); + } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java index 5a5cc512c..50636c861 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java @@ -23,6 +23,7 @@ import com.ashokvarma.bottomnavigation.BottomNavigationItem; import org.fdroid.fdroid.AppDetails2; import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; import org.fdroid.fdroid.Preferences; @@ -106,9 +107,9 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB .addItem(new BottomNavigationItem(R.drawable.ic_settings, R.string.menu_settings)) .initialise(); - IntentFilter updateableAppsFilter = new IntentFilter( - AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED); + IntentFilter updateableAppsFilter = new IntentFilter(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED); updateableAppsFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED); + updateableAppsFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED); LocalBroadcastManager.getInstance(this).registerReceiver(onUpdateableAppsChanged, updateableAppsFilter); if (savedInstanceState != null) { @@ -378,15 +379,28 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(context); String reason = intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE); - if (AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED.equals(intent.getAction()) && - (AppUpdateStatusManager.REASON_READY_TO_INSTALL.equals(reason) || - AppUpdateStatusManager.REASON_REPO_DISABLED.equals(reason))) { - updateBadge = true; + switch (intent.getAction()) { + // Apps which are added/removed from the list due to becoming ready to install or a repo being + // disabled both cause us to increase/decrease our badge count respectively. + case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED: + if (AppUpdateStatusManager.REASON_READY_TO_INSTALL.equals(reason) || + AppUpdateStatusManager.REASON_REPO_DISABLED.equals(reason)) { + updateBadge = true; + } + break; + + // Apps which were previously "Ready to install" but have been removed. We need to lower our badge + // count in response to this. + case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: + AppUpdateStatus status = intent.getParcelableExtra(AppUpdateStatusManager.EXTRA_STATUS); + if (status != null && status.status == AppUpdateStatusManager.Status.ReadyToInstall) { + updateBadge = true; + } + break; } // Check if we have moved into the ReadyToInstall or Installed state. - AppUpdateStatusManager.AppUpdateStatus status = manager.get( - intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL)); + AppUpdateStatus status = manager.get(intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL)); boolean isStatusChange = intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false); if (isStatusChange && status != null @@ -396,7 +410,7 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB if (updateBadge) { int count = 0; - for (AppUpdateStatusManager.AppUpdateStatus s : manager.getAll()) { + for (AppUpdateStatus s : manager.getAll()) { if (s.status == AppUpdateStatusManager.Status.ReadyToInstall) { count++; } diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/DismissResult.java b/app/src/main/java/org/fdroid/fdroid/views/updates/DismissResult.java new file mode 100644 index 000000000..c68870128 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/DismissResult.java @@ -0,0 +1,29 @@ +package org.fdroid.fdroid.views.updates; + +import android.support.annotation.Nullable; + +/** + * When dismissing an item from the Updates tab, there is two different things we need to return. + * This is a dumb data object to represent these things, because a method is only allowed to return one thing. + */ +public class DismissResult { + + @Nullable + public final CharSequence message; + + public final boolean requiresAdapterRefresh; + + public DismissResult() { + this(null, false); + } + + public DismissResult(boolean requiresAdapterRefresh) { + this(null, requiresAdapterRefresh); + } + + public DismissResult(@Nullable CharSequence message, boolean requiresAdapterRefresh) { + this.message = message; + this.requiresAdapterRefresh = requiresAdapterRefresh; + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java index a993910a6..c657fe498 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java @@ -341,4 +341,11 @@ public class UpdatesAdapter extends RecyclerView.Adapter + *
  • + * {@link KnownVulnAppListItemController}: Will be marked as "Ignored" and wont warn the user in the future. + *
  • + *
  • + * {@link UpdateableAppListItemController}: Will get marked as "Ignore this update". + *
  • + *
  • + * {@link AppStatusListItemController}: + *
      + *
    • If downloading or queued to download, cancel the download.
    • + *
    • If downloaded waiting to install, forget that we downloaded it.
    • + *
    • If installed ready to run, stop prompting the user to run the app.
    • + *
    + *
  • + * + */ +public class UpdatesItemTouchCallback extends ItemTouchHelper.Callback { + + private final Context context; + private final UpdatesAdapter adapter; + + public UpdatesItemTouchCallback(Context context, UpdatesAdapter adapter) { + this.context = context; + this.adapter = adapter; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + int swipeFlags = 0; + if (viewHolder instanceof AppListItemController) { + AppListItemController controller = (AppListItemController) viewHolder; + if (controller.canDismiss()) { + swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; + } + } + return makeMovementFlags(0, swipeFlags); + } + + @Override + public boolean onMove( + RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + AppListItemController controller = (AppListItemController) viewHolder; + DismissResult result = controller.onDismiss(); + + if (result.message != null) { + Toast.makeText(context, result.message, Toast.LENGTH_SHORT).show(); + } + + if (result.requiresAdapterRefresh) { + adapter.refreshStatuses(); + } + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesViewBinder.java index 784210e59..295f3ccf0 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesViewBinder.java @@ -4,6 +4,7 @@ import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; @@ -30,6 +31,9 @@ public class UpdatesViewBinder { list.setLayoutManager(new LinearLayoutManager(activity)); list.setAdapter(adapter); + ItemTouchHelper touchHelper = new ItemTouchHelper(new UpdatesItemTouchCallback(activity, adapter)); + touchHelper.attachToRecyclerView(list); + emptyState = (TextView) view.findViewById(R.id.empty_state); emptyImage = (ImageView) view.findViewById(R.id.image); diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java index ade2990fe..981409eff 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java @@ -6,10 +6,12 @@ import android.support.annotation.Nullable; import android.view.View; import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.views.apps.AppListItemController; import org.fdroid.fdroid.views.apps.AppListItemState; +import org.fdroid.fdroid.views.updates.DismissResult; /** * Shows apps which are: @@ -24,15 +26,14 @@ public class AppStatusListItemController extends AppListItemController { @NonNull @Override - protected AppListItemState getCurrentViewState( - @NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) { + protected AppListItemState getCurrentViewState(@NonNull App app, @Nullable AppUpdateStatus appStatus) { return super.getCurrentViewState(app, appStatus) .setStatusText(getStatusText(appStatus)); } @Nullable - private CharSequence getStatusText(@Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) { + private CharSequence getStatusText(@Nullable AppUpdateStatus appStatus) { if (appStatus != null) { switch (appStatus.status) { case ReadyToInstall: @@ -45,4 +46,34 @@ public class AppStatusListItemController extends AppListItemController { return null; } + + @Override + public boolean canDismiss() { + return true; + } + + @NonNull + @Override + protected DismissResult onDismissApp(@NonNull App app) { + AppUpdateStatus status = getCurrentStatus(); + CharSequence message = null; + if (status != null) { + AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(activity); + manager.removeApk(status.getUniqueKey()); + switch (status.status) { + case ReadyToInstall: + manager.markAsNoLongerPendingInstall(status); + // Do this silently, because it should be pretty obvious based on the context + // of a "Ready to install" app being dismissed. + break; + + case Downloading: + cancelDownload(); + message = activity.getString(R.string.app_list__dismiss_downloading_app); + break; + } + } + + return new DismissResult(message, true); + } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java index 23f7bea45..cd06c167a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java @@ -24,6 +24,7 @@ import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.views.apps.AppListItemController; import org.fdroid.fdroid.views.apps.AppListItemState; +import org.fdroid.fdroid.views.updates.DismissResult; /** * Tell the user that an app they have installed has a known vulnerability. @@ -82,8 +83,23 @@ public class KnownVulnAppListItemController extends AppListItemController { } } + @Override + public boolean canDismiss() { + return true; + } + + @Override + protected DismissResult onDismissApp(@NonNull App app) { + this.ignoreVulnerableApp(app); + return new DismissResult(activity.getString(R.string.app_list__dismiss_vulnerable_app), false); + } + @Override protected void onSecondaryButtonPressed(@NonNull App app) { + this.ignoreVulnerableApp(app); + } + + private void ignoreVulnerableApp(@NonNull App app) { AppPrefs prefs = app.getPrefs(activity); prefs.ignoreVulnerabilities = true; AppPrefsProvider.Helper.update(activity, app, prefs); diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java index 42a45b1a1..5baa5f89e 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java @@ -6,9 +6,13 @@ import android.support.annotation.Nullable; import android.view.View; import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppPrefs; +import org.fdroid.fdroid.data.AppPrefsProvider; import org.fdroid.fdroid.views.apps.AppListItemController; import org.fdroid.fdroid.views.apps.AppListItemState; +import org.fdroid.fdroid.views.updates.DismissResult; /** * Very trimmed down list item. Only displays the app icon, name, and a download button. @@ -28,4 +32,21 @@ public class UpdateableAppListItemController extends AppListItemController { return new AppListItemState(app) .setShowInstallButton(true); } + + @Override + public boolean canDismiss() { + return true; + } + + @Override + @NonNull + protected DismissResult onDismissApp(@NonNull App app) { + AppPrefs prefs = app.getPrefs(activity); + prefs.ignoreThisUpdate = app.suggestedVersionCode; + + // The act of updating here will trigger a re-query of the "can update" apps, so no need to do anything else + // to update the UI in response to this. + AppPrefsProvider.Helper.update(activity, app, prefs); + return new DismissResult(activity.getString(R.string.app_list__dismiss_app_update), false); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7503ea30f..abf63d459 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,6 +87,9 @@ This often occurs with apps installed via Google Play or other sources, if they Downloading %1$s %1$s installed Downloaded, ready to install + Update ignored + Vulnerability ignored + Download canceled Installed Apps Updates ignored