diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 91d5b965f..8ec8cd1e4 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -63,6 +63,13 @@ public final class AppUpdateStatusManager { public static final String EXTRA_APK_URL = "urlstring"; + public static final String EXTRA_REASON_FOR_CHANGE = "reason"; + + public static final String REASON_READY_TO_INSTALL = "readytoinstall"; + public static final String REASON_UPDATES_AVAILABLE = "updatesavailable"; + public static final String REASON_CLEAR_ALL_UPDATES = "clearallupdates"; + public static final String REASON_CLEAR_ALL_INSTALLED = "clearallinstalled"; + /** * If this is present and true, then the broadcast has been sent in response to the {@link AppUpdateStatus#status} * changing. In comparison, if it is just the download progress of an app then this should not be true. @@ -174,9 +181,11 @@ public final class AppUpdateStatusManager { notifyAdd(entry); } - private void notifyChange() { + private void notifyChange(String reason) { if (!isBatchUpdating) { - localBroadcastManager.sendBroadcast(new Intent(BROADCAST_APPSTATUS_LIST_CHANGED)); + Intent intent = new Intent(BROADCAST_APPSTATUS_LIST_CHANGED); + intent.putExtra(EXTRA_REASON_FOR_CHANGE, reason); + localBroadcastManager.sendBroadcast(intent); } } @@ -220,7 +229,7 @@ public final class AppUpdateStatusManager { for (Apk apk : apksToUpdate) { addApk(apk, status, null); } - endBatchUpdates(); + endBatchUpdates(status); } /** @@ -318,10 +327,17 @@ public final class AppUpdateStatusManager { } } - private void endBatchUpdates() { + private void endBatchUpdates(Status status) { synchronized (appMapping) { isBatchUpdating = false; - notifyChange(); + + String reason = null; + if (status == Status.ReadyToInstall) { + reason = REASON_READY_TO_INSTALL; + } else if (status == Status.UpdateAvailable) { + reason = REASON_UPDATES_AVAILABLE; + } + notifyChange(reason); } } @@ -333,7 +349,7 @@ public final class AppUpdateStatusManager { it.remove(); } } - notifyChange(); + notifyChange(REASON_CLEAR_ALL_UPDATES); } } @@ -345,7 +361,7 @@ public final class AppUpdateStatusManager { it.remove(); } } - notifyChange(); + notifyChange(REASON_CLEAR_ALL_INSTALLED); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java index ef006d4ef..b0a674ac3 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java @@ -10,7 +10,7 @@ import android.widget.FrameLayout; import org.fdroid.fdroid.R; import org.fdroid.fdroid.views.fragments.PreferencesFragment; -import org.fdroid.fdroid.views.myapps.MyAppsViewBinder; +import org.fdroid.fdroid.views.updates.UpdatesViewBinder; import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; /** @@ -38,10 +38,10 @@ class MainViewController extends RecyclerView.ViewHolder { } /** - * @see MyAppsViewBinder + * @see UpdatesViewBinder */ public void bindUpdates() { - new MyAppsViewBinder(activity, frame); + new UpdatesViewBinder(activity, frame); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/InstalledHeaderController.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/InstalledHeaderController.java deleted file mode 100644 index c8adba2ce..000000000 --- a/app/src/main/java/org/fdroid/fdroid/views/myapps/InstalledHeaderController.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.fdroid.fdroid.views.myapps; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -public class InstalledHeaderController extends RecyclerView.ViewHolder { - public InstalledHeaderController(View itemView) { - super(itemView); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsAdapter.java deleted file mode 100644 index abfdfb7e6..000000000 --- a/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsAdapter.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.fdroid.fdroid.views.myapps; - -import android.app.Activity; -import android.database.Cursor; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.views.apps.AppListItemController; -import org.fdroid.fdroid.views.apps.AppListItemDivider; - -/** - * Wraps a cursor which should have a list of "apps which can be updated". Also includes a header - * as the first element which allows for all items to be updated. - */ -public class MyAppsAdapter extends RecyclerView.Adapter { - - private Cursor updatesCursor; - private final Activity activity; - private final AppListItemDivider divider; - - public MyAppsAdapter(Activity activity) { - this.activity = activity; - divider = new AppListItemDivider(activity); - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - LayoutInflater inflater = activity.getLayoutInflater(); - switch (viewType) { - case R.id.my_apps__header: - return new UpdatesHeaderController(activity, inflater.inflate(R.layout.my_apps_updates_header, parent, false)); - - case R.id.my_apps__app: - return new AppListItemController(activity, inflater.inflate(R.layout.app_list_item, parent, false)); - - default: - throw new IllegalArgumentException(); - } - } - - @Override - public int getItemCount() { - return updatesCursor == null ? 0 : updatesCursor.getCount() + 1; - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - switch (getItemViewType(position)) { - case R.id.my_apps__header: - ((UpdatesHeaderController) holder).bindModel(updatesCursor.getCount()); - break; - - case R.id.my_apps__app: - updatesCursor.moveToPosition(position - 1); // Subtract one to account for the header. - ((AppListItemController) holder).bindModel(new App(updatesCursor)); - break; - - default: - throw new IllegalArgumentException(); - } - } - - @Override - public int getItemViewType(int position) { - if (position == 0) { - return R.id.my_apps__header; - } else { - return R.id.my_apps__app; - } - } - - public void setApps(Cursor cursor) { - updatesCursor = cursor; - notifyDataSetChanged(); - } - - @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { - super.onAttachedToRecyclerView(recyclerView); - recyclerView.addItemDecoration(divider); - } - - @Override - public void onDetachedFromRecyclerView(RecyclerView recyclerView) { - recyclerView.removeItemDecoration(divider); - super.onDetachedFromRecyclerView(recyclerView); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsViewBinder.java deleted file mode 100644 index 8e3ec2c73..000000000 --- a/app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsViewBinder.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.fdroid.fdroid.views.myapps; - -import android.app.Activity; -import android.database.Cursor; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.widget.FrameLayout; - -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.Schema; - -public class MyAppsViewBinder implements LoaderManager.LoaderCallbacks { - - private final MyAppsAdapter adapter; - - private final Activity activity; - - public MyAppsViewBinder(AppCompatActivity activity, FrameLayout parent) { - this.activity = activity; - - View myAppsView = activity.getLayoutInflater().inflate(R.layout.main_tabs, parent, true); - - adapter = new MyAppsAdapter(activity); - - RecyclerView list = (RecyclerView) myAppsView.findViewById(R.id.list); - list.setHasFixedSize(true); - list.setLayoutManager(new LinearLayoutManager(activity)); - list.setAdapter(adapter); - - LoaderManager loaderManager = activity.getSupportLoaderManager(); - loaderManager.initLoader(0, null, this); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new CursorLoader( - activity, - AppProvider.getCanUpdateUri(), - new String[]{ - Schema.AppMetadataTable.Cols._ID, // Required for cursor loader to work. - Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME, - Schema.AppMetadataTable.Cols.NAME, - Schema.AppMetadataTable.Cols.SUMMARY, - Schema.AppMetadataTable.Cols.IS_COMPATIBLE, - Schema.AppMetadataTable.Cols.LICENSE, - Schema.AppMetadataTable.Cols.ICON, - Schema.AppMetadataTable.Cols.ICON_URL, - Schema.AppMetadataTable.Cols.InstalledApp.VERSION_CODE, - Schema.AppMetadataTable.Cols.InstalledApp.VERSION_NAME, - Schema.AppMetadataTable.Cols.SuggestedApk.VERSION_NAME, - Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE, - Schema.AppMetadataTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root. - Schema.AppMetadataTable.Cols.ANTI_FEATURES, // Needed for filtering apps that require anti-features. - }, - null, - null, - null - ); - } - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - adapter.setApps(cursor); - } - - @Override - public void onLoaderReset(Loader loader) { - adapter.setApps(null); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/views/myapps/UpdatesHeaderController.java b/app/src/main/java/org/fdroid/fdroid/views/myapps/UpdatesHeaderController.java deleted file mode 100644 index 1bbf61210..000000000 --- a/app/src/main/java/org/fdroid/fdroid/views/myapps/UpdatesHeaderController.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.fdroid.fdroid.views.myapps; - -import android.app.Activity; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.UpdateService; - -public class UpdatesHeaderController extends RecyclerView.ViewHolder { - - private final Activity activity; - private final TextView updatesHeading; - - public UpdatesHeaderController(Activity activity, View itemView) { - super(itemView); - this.activity = activity; - - Button updateAll = (Button) itemView.findViewById(R.id.update_all_button); - updateAll.setOnClickListener(onUpdateAll); - - updatesHeading = (TextView) itemView.findViewById(R.id.updates_heading); - updatesHeading.setText(activity.getString(R.string.updates)); - } - - public void bindModel(int numAppsToUpdate) { - updatesHeading.setText(activity.getResources().getQuantityString(R.plurals.my_apps_header_number_of_updateable, numAppsToUpdate, numAppsToUpdate)); - } - - private final View.OnClickListener onUpdateAll = new View.OnClickListener() { - @Override - public void onClick(View v) { - UpdateService.autoDownloadUpdates(activity); - } - }; -} 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 new file mode 100644 index 000000000..fc449f5a1 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java @@ -0,0 +1,355 @@ +package org.fdroid.fdroid.views.updates; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.ViewGroup; + +import com.hannesdorfmann.adapterdelegates3.AdapterDelegatesManager; + +import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.views.updates.items.AppNotification; +import org.fdroid.fdroid.views.updates.items.AppStatus; +import org.fdroid.fdroid.views.updates.items.AppUpdateData; +import org.fdroid.fdroid.views.updates.items.DonationPrompt; +import org.fdroid.fdroid.views.updates.items.UpdateableApp; +import org.fdroid.fdroid.views.updates.items.UpdateableAppsHeader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Manages the following types of information: + * * Apps marked for downloading (while the user is offline) + * * Currently downloading apps + * * Apps which have been downloaded (and need further action to install). This includes new installs and updates. + * * Reminders to users that they can donate to apps (only shown infrequently after several updates) + * * A list of apps which are eligible to be updated (for when the "Automatic Updates" option is disabled), including: + * + A summary of all apps to update including an "Update all" button and a "Show apps" button. + * + Once "Show apps" is expanded then each app is shown along with its own download button. + * + * It does this by maintaining several different lists of interesting apps. Each list contains wrappers + * around the piece of data it wants to render ({@link AppStatus}, {@link DonationPrompt}, + * {@link AppNotification}, {@link UpdateableApp}). Instead of juggling the various viewTypes + * to find out which position in the adapter corresponds to which view type, this is handled by + * the {@link UpdatesAdapter#delegatesManager}. + * + * There are a series of type-safe lists which hold the specific data this adapter is interested in. + * This data is then collated into a single list (see {@link UpdatesAdapter#populateItems()}) which + * is the actual thing the adapter binds too. At any point it is safe to clear the single list and + * repopulate it from the original source lists of data. When this is done, the adapter will notify + * the recycler view that its data has changed. Sometimes it will also ask the recycler view to + * scroll to the newly added item (if attached to the recycler view). + */ +public class UpdatesAdapter extends RecyclerView.Adapter implements LoaderManager.LoaderCallbacks { + + private final AdapterDelegatesManager> delegatesManager = new AdapterDelegatesManager<>(); + private final List items = new ArrayList<>(); + + private final AppCompatActivity activity; + + @Nullable + private RecyclerView recyclerView; + + private final List appsToShowStatus = new ArrayList<>(); + private final List appsToPromptForDonation = new ArrayList<>(); + private final List appsToNotifyAbout = new ArrayList<>(); + private final List updateableApps = new ArrayList<>(); + + private boolean showAllUpdateableApps = false; + + public UpdatesAdapter(AppCompatActivity activity) { + this.activity = activity; + + delegatesManager.addDelegate(new AppStatus.Delegate(activity)) + .addDelegate(new AppNotification.Delegate()) + .addDelegate(new DonationPrompt.Delegate()) + .addDelegate(new UpdateableApp.Delegate(activity)) + .addDelegate(new UpdateableAppsHeader.Delegate(activity)); + + populateAppStatuses(); + notifyDataSetChanged(); + + activity.getSupportLoaderManager().initLoader(0, null, this); + } + + /** + * There are some statuses managed by {@link AppUpdateStatusManager} which we don't care about + * for the "Updates" view. For example {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#Installed} + * apps are not interesting in the Updates" view at this point in time. Also, although this + * adapter does know about apps with updates availble, it does so by querying the database not + * by querying the app update status manager. As such, apps with the status + * {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#UpdateAvailable} are not interesting here. + */ + private boolean shouldShowStatus(AppUpdateStatusManager.AppUpdateStatus status) { + return status.status == AppUpdateStatusManager.Status.Unknown || + status.status == AppUpdateStatusManager.Status.Downloading || + status.status == AppUpdateStatusManager.Status.ReadyToInstall; + } + + /** + * Adds items from the {@link AppUpdateStatusManager} to {@link UpdatesAdapter#appsToShowStatus}. + * Note that this will then subsequently rebuild the underlying adapter data structure by + * invoking {@link UpdatesAdapter#populateItems}. However as per the populateItems method, it + * does not know how best to notify the recycler view of any changes. That is up to the caller + * of this method. + */ + private void populateAppStatuses() { + for (AppUpdateStatusManager.AppUpdateStatus status : AppUpdateStatusManager.getInstance(activity).getAll()) { + if (shouldShowStatus(status)) { + appsToShowStatus.add(new AppStatus(activity, status)); + } + } + + Collections.sort(appsToShowStatus, new Comparator() { + @Override + public int compare(AppStatus o1, AppStatus o2) { + return o1.status.app.name.compareTo(o2.status.app.name); + } + }); + + populateItems(); + } + + public boolean canViewAllUpdateableApps() { + return showAllUpdateableApps; + } + + public void toggleAllUpdateableApps() { + showAllUpdateableApps = !showAllUpdateableApps; + populateItems(); + + if (showAllUpdateableApps) { + notifyItemRangeInserted(appsToShowStatus.size() + 1, updateableApps.size()); + } else { + notifyItemRangeRemoved(appsToShowStatus.size() + 1, updateableApps.size()); + } + } + + /** + * Completely rebuilds the underlying data structure used by this adapter. Note however, that + * this does not notify the recycler view of any changes. Thus, it is up to other methods which + * initiate a call to this method to make sure they appropriately notify the recyler view. + */ + private void populateItems() { + items.clear(); + + items.addAll(appsToShowStatus); + + if (updateableApps != null && updateableApps.size() > 0) { + items.add(new UpdateableAppsHeader(activity, this, updateableApps)); + if (showAllUpdateableApps) { + items.addAll(updateableApps); + } + } + + items.addAll(appsToPromptForDonation); + items.addAll(appsToNotifyAbout); + } + + @Override + public int getItemCount() { + return items.size(); + } + + @Override + public int getItemViewType(int position) { + return delegatesManager.getItemViewType(items, position); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return delegatesManager.onCreateViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + delegatesManager.onBindViewHolder(items, position, holder); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + this.recyclerView = recyclerView; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new CursorLoader( + activity, + AppProvider.getCanUpdateUri(), + new String[]{ + Schema.AppMetadataTable.Cols._ID, // Required for cursor loader to work. + Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME, + Schema.AppMetadataTable.Cols.NAME, + Schema.AppMetadataTable.Cols.SUMMARY, + Schema.AppMetadataTable.Cols.IS_COMPATIBLE, + Schema.AppMetadataTable.Cols.LICENSE, + Schema.AppMetadataTable.Cols.ICON, + Schema.AppMetadataTable.Cols.ICON_URL, + Schema.AppMetadataTable.Cols.InstalledApp.VERSION_CODE, + Schema.AppMetadataTable.Cols.InstalledApp.VERSION_NAME, + Schema.AppMetadataTable.Cols.SuggestedApk.VERSION_NAME, + Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE, + Schema.AppMetadataTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root. + Schema.AppMetadataTable.Cols.ANTI_FEATURES, // Needed for filtering apps that require anti-features. + }, + null, + null, + Schema.AppMetadataTable.Cols.NAME + ); + } + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) { + int numberRemoved = updateableApps.size(); + boolean hadHeader = updateableApps.size() > 0; + boolean willHaveHeader = cursor.getCount() > 0; + + updateableApps.clear(); + notifyItemRangeRemoved(appsToShowStatus.size(), numberRemoved + (hadHeader ? 1 : 0)); + + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + updateableApps.add(new UpdateableApp(activity, new App(cursor))); + cursor.moveToNext(); + } + + populateItems(); + notifyItemRangeInserted(appsToShowStatus.size(), updateableApps.size() + (willHaveHeader ? 1 : 0)); + } + + @Override + public void onLoaderReset(Loader loader) { } + + /** + * Doesn't listen for {@link AppUpdateStatusManager#BROADCAST_APPSTATUS_CHANGED} because the + * individual items in the recycler view will listen for the appropriate changes in state and + * update themselves accordingly (if they are displayed). + */ + public void listenForStatusUpdates() { + IntentFilter filter = new IntentFilter(); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED); + + LocalBroadcastManager.getInstance(activity).registerReceiver(receiverAppStatusChanges, filter); + } + + private void onManyAppStatusesChanged(String reasonForChange) { + switch (reasonForChange) { + case AppUpdateStatusManager.REASON_UPDATES_AVAILABLE: + onUpdateableAppsChanged(); + break; + + case AppUpdateStatusManager.REASON_READY_TO_INSTALL: + onFoundAppsReadyToInstall(); + break; + } + } + + /** + * Apps have been made available for update which were not available for update before. + * We need to rerun our database query to get a list of apps to update. + */ + private void onUpdateableAppsChanged() { + activity.getSupportLoaderManager().initLoader(0, null, this); + } + + /** + * We have completed a scan of .apk files in the cache, and identified there are + * some which are ready to install. + */ + private void onFoundAppsReadyToInstall() { + if (appsToShowStatus.size() > 0) { + int size = appsToShowStatus.size(); + appsToShowStatus.clear(); + notifyItemRangeRemoved(0, size); + } + + populateAppStatuses(); + notifyItemRangeInserted(0, appsToShowStatus.size()); + + if (recyclerView != null) { + recyclerView.smoothScrollToPosition(0); + } + } + + private void onAppStatusAdded(String apkUrl) { + // We could try and find the specific place where we need to add our new item, but it is + // far simpler to clear the list and rebuild it (sorting it in the process). + appsToShowStatus.clear(); + populateAppStatuses(); + + // After adding the new item to our list (somewhere) we can then look it back up again in + // order to notify the recycler view and scroll to that item. + int positionOfNewApp = 0; + for (int i = 0; i < appsToShowStatus.size(); i++) { + if (TextUtils.equals(appsToShowStatus.get(i).status.getUniqueKey(), apkUrl)) { + positionOfNewApp = i; + break; + } + } + + notifyItemInserted(positionOfNewApp); + + if (recyclerView != null) { + recyclerView.smoothScrollToPosition(positionOfNewApp); + } + } + + private void onAppStatusRemoved(String apkUrl) { + // Find out where the item is in our internal data structure, so that we can remove it and + // also notify the recycler view appropriately. + int positionOfOldApp = 0; + for (int i = 0; i < appsToShowStatus.size(); i++) { + if (TextUtils.equals(appsToShowStatus.get(i).status.getUniqueKey(), apkUrl)) { + positionOfOldApp = i; + break; + } + } + + appsToShowStatus.remove(positionOfOldApp); + + populateItems(); + notifyItemRemoved(positionOfOldApp); + } + + private final BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String apkUrl = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + + switch (intent.getAction()) { + case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED: + onManyAppStatusesChanged(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE)); + break; + + case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED: + onAppStatusAdded(apkUrl); + break; + + case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: + onAppStatusRemoved(apkUrl); + break; + } + } + }; + +} 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 new file mode 100644 index 000000000..9157099a7 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesViewBinder.java @@ -0,0 +1,26 @@ +package org.fdroid.fdroid.views.updates; + +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.FrameLayout; + +import org.fdroid.fdroid.R; + +public class UpdatesViewBinder { + + public UpdatesViewBinder(AppCompatActivity activity, FrameLayout parent) { + View view = activity.getLayoutInflater().inflate(R.layout.main_tab_updates, parent, true); + + UpdatesAdapter adapter = new UpdatesAdapter(activity); + + // TODO: Find the right time to stop listening for status updates. + adapter.listenForStatusUpdates(); + + RecyclerView list = (RecyclerView) view.findViewById(R.id.list); + list.setHasFixedSize(true); + list.setLayoutManager(new LinearLayoutManager(activity)); + list.setAdapter(adapter); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppNotification.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppNotification.java new file mode 100644 index 000000000..dadf209e5 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppNotification.java @@ -0,0 +1,56 @@ +package org.fdroid.fdroid.views.updates.items; + +import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.hannesdorfmann.adapterdelegates3.AdapterDelegate; + +import java.util.List; + +/** + * Each of these apps has a notification to display to the user. + * The notification will have come from the apps metadata, provided by its maintainer. It may be + * something about the app being removed from the repository, or perhaps security problems that + * were identified in the app. + */ +public class AppNotification extends AppUpdateData { + + public AppNotification(Activity activity) { + super(activity); + } + + public static class Delegate extends AdapterDelegate> { + + @Override + protected boolean isForViewType(@NonNull List items, int position) { + return items.get(position) instanceof AppNotification; + } + + @NonNull + @Override + protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new ViewHolder(new TextView(parent.getContext())); + } + + @Override + protected void onBindViewHolder(@NonNull List items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { + AppNotification app = (AppNotification) items.get(position); + ((ViewHolder) holder).bindApp(app); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View itemView) { + super(itemView); + } + + public void bindApp(AppNotification app) { + ((TextView) itemView).setText("Notification for app"); + } + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatus.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatus.java new file mode 100644 index 000000000..22ae812dc --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatus.java @@ -0,0 +1,57 @@ +package org.fdroid.fdroid.views.updates.items; + +import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import com.hannesdorfmann.adapterdelegates3.AdapterDelegate; + +import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.views.apps.AppListItemController; + +import java.util.List; + +/** + * Apps which we want to show some more substantial information about. + * @see R.layout#updateable_app_status_item The view that this binds to + * @see AppListItemController Used for binding the {@link App} to the {@link R.layout#updateable_app_status_item} + */ +public class AppStatus extends AppUpdateData { + + public final AppUpdateStatusManager.AppUpdateStatus status; + + public AppStatus(Activity activity, AppUpdateStatusManager.AppUpdateStatus status) { + super(activity); + this.status = status; + } + + public static class Delegate extends AdapterDelegate> { + + private final Activity activity; + + public Delegate(Activity activity) { + this.activity = activity; + } + + @Override + protected boolean isForViewType(@NonNull List items, int position) { + return items.get(position) instanceof AppStatus; + } + + @NonNull + @Override + protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new AppListItemController(activity, activity.getLayoutInflater().inflate(R.layout.updateable_app_status_item, parent, false)); + } + + @Override + protected void onBindViewHolder(@NonNull List items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { + AppStatus app = (AppStatus) items.get(position); + ((AppListItemController) holder).bindModel(app.status.app); + } + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppUpdateData.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppUpdateData.java new file mode 100644 index 000000000..173cbbeb7 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppUpdateData.java @@ -0,0 +1,16 @@ +package org.fdroid.fdroid.views.updates.items; + +import android.app.Activity; + +/** + * Used as a common base class for all data types in the {@link org.fdroid.fdroid.views.updates.UpdatesAdapter}. + * Doesn't have any functionality of its own, but allows the {@link org.fdroid.fdroid.views.updates.UpdatesAdapter#delegatesManager} + * to specify a data type more specific than just {@link Object}. + */ +public abstract class AppUpdateData { + public final Activity activity; + + public AppUpdateData(Activity activity) { + this.activity = activity; + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/DonationPrompt.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/DonationPrompt.java new file mode 100644 index 000000000..73e306b5f --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/DonationPrompt.java @@ -0,0 +1,54 @@ +package org.fdroid.fdroid.views.updates.items; + +import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.hannesdorfmann.adapterdelegates3.AdapterDelegate; + +import java.util.List; + +/** + * The app (if any) which we should prompt the user about potentially donating to (due to having + * updated several times). + */ +public class DonationPrompt extends AppUpdateData { + + public DonationPrompt(Activity activity) { + super(activity); + } + + public static class Delegate extends AdapterDelegate> { + + @Override + protected boolean isForViewType(@NonNull List items, int position) { + return items.get(position) instanceof DonationPrompt; + } + + @NonNull + @Override + protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new ViewHolder(new TextView(parent.getContext())); + } + + @Override + protected void onBindViewHolder(@NonNull List items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { + DonationPrompt app = (DonationPrompt) items.get(position); + ((ViewHolder) holder).bindApp(app); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View itemView) { + super(itemView); + } + + public void bindApp(DonationPrompt app) { + ((TextView) itemView).setText("Donation prompt for app"); + } + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java new file mode 100644 index 000000000..77fc9ad14 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java @@ -0,0 +1,57 @@ +package org.fdroid.fdroid.views.updates.items; + +import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import com.hannesdorfmann.adapterdelegates3.AdapterDelegate; + +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.views.apps.AppListItemController; + +import java.util.List; + +/** + * List of all apps which can be updated, but have not yet been downloaded. + * @see UpdateableApp The data that is bound to this view. + * @see R.layout#updateable_app_list_item The view that this binds to. + * @see AppListItemController Used for binding the {@link App} to the {@link R.layout#updateable_app_list_item} + */ +public class UpdateableApp extends AppUpdateData { + + public final App app; + + public UpdateableApp(Activity activity, App app) { + super(activity); + this.app = app; + } + + public static class Delegate extends AdapterDelegate> { + + private final Activity activity; + + public Delegate(Activity activity) { + this.activity = activity; + } + + @Override + protected boolean isForViewType(@NonNull List items, int position) { + return items.get(position) instanceof UpdateableApp; + } + + @NonNull + @Override + protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new AppListItemController(activity, activity.getLayoutInflater().inflate(R.layout.updateable_app_list_item, parent, false)); + } + + @Override + protected void onBindViewHolder(@NonNull List items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { + UpdateableApp app = (UpdateableApp) items.get(position); + ((AppListItemController) holder).bindModel(app.app); + } + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppsHeader.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppsHeader.java new file mode 100644 index 000000000..cd1e4c3ed --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppsHeader.java @@ -0,0 +1,120 @@ +package org.fdroid.fdroid.views.updates.items; + +import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.hannesdorfmann.adapterdelegates3.AdapterDelegate; + +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; +import org.fdroid.fdroid.views.updates.UpdatesAdapter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Summary of all apps that can be downloaded. Includes a button to download all of them and also + * a toggle to show or hide the list of each individual item. + * @see R.layout#updates_header The view that this binds to. + * @see UpdateableAppsHeader The data that is bound to this view. + */ +public class UpdateableAppsHeader extends AppUpdateData { + + public final List apps; + public final UpdatesAdapter adapter; + + public UpdateableAppsHeader(Activity activity, UpdatesAdapter updatesAdapter, List updateableApps) { + super(activity); + apps = updateableApps; + adapter = updatesAdapter; + } + + public static class Delegate extends AdapterDelegate> { + + private final LayoutInflater inflater; + + public Delegate(Activity activity) { + inflater = activity.getLayoutInflater(); + } + + @Override + protected boolean isForViewType(@NonNull List items, int position) { + return items.get(position) instanceof UpdateableAppsHeader; + } + + @NonNull + @Override + protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new ViewHolder(inflater.inflate(R.layout.updates_header, parent, false)); + } + + @Override + protected void onBindViewHolder(@NonNull List items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { + UpdateableAppsHeader app = (UpdateableAppsHeader) items.get(position); + ((ViewHolder) holder).bindHeader(app); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + + private UpdateableAppsHeader header; + + private final TextView updatesAvailable; + private final ImageView downloadAll; + private final TextView appsToUpdate; + private final Button toggleAppsToUpdate; + + public ViewHolder(View itemView) { + super(itemView); + + updatesAvailable = (TextView) itemView.findViewById(R.id.text_updates_available); + downloadAll = (ImageView) itemView.findViewById(R.id.button_download_all); + appsToUpdate = (TextView) itemView.findViewById(R.id.text_apps_to_update); + toggleAppsToUpdate = (Button) itemView.findViewById(R.id.button_toggle_apps_to_update); + + toggleAppsToUpdate.setOnClickListener(this); + downloadAll.setOnClickListener(this); + } + + public void bindHeader(UpdateableAppsHeader header) { + this.header = header; + + updatesAvailable.setText(itemView.getResources().getQuantityString(R.plurals.updates__download_updates_for_apps, header.apps.size(), header.apps.size())); + + List appNames = new ArrayList<>(header.apps.size()); + for (UpdateableApp app : header.apps) { + appNames.add(app.app.name); + } + + appsToUpdate.setText(TextUtils.join(", ", appNames)); + updateToggleButtonText(); + } + + @Override + public void onClick(View v) { + if (v == toggleAppsToUpdate) { + header.adapter.toggleAllUpdateableApps(); + updateToggleButtonText(); + } else if (v == downloadAll) { + UpdateService.autoDownloadUpdates(header.activity); + } + } + + private void updateToggleButtonText() { + if (header.adapter.canViewAllUpdateableApps()) { + toggleAppsToUpdate.setText(R.string.updates__hide_updateable_apps); + } else { + toggleAppsToUpdate.setText(R.string.updates__show_updateable_apps); + } + } + } + +} diff --git a/app/src/main/res/layout/main_tabs.xml b/app/src/main/res/layout/main_tab_updates.xml similarity index 59% rename from app/src/main/res/layout/main_tabs.xml rename to app/src/main/res/layout/main_tab_updates.xml index d47fb717d..308ae2a92 100644 --- a/app/src/main/res/layout/main_tabs.xml +++ b/app/src/main/res/layout/main_tab_updates.xml @@ -1,7 +1,6 @@ - @@ -11,9 +10,6 @@ tools:listitem="@layout/app_list_item" android:layout_width="match_parent" android:layout_height="match_parent" - android:scrollbars="vertical" - app:layout_constraintTop_toBottomOf="@+id/update_all_button" - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintRight_toRightOf="parent" /> + android:scrollbars="vertical" /> - + diff --git a/app/src/main/res/layout/my_apps_updates_header.xml b/app/src/main/res/layout/my_apps_updates_header.xml deleted file mode 100644 index 3ce423ae9..000000000 --- a/app/src/main/res/layout/my_apps_updates_header.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - -