From 9e2f7edff4a23182214813e8dbc591a7a570a4bd Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 15 Mar 2017 14:40:22 +1100 Subject: [PATCH 01/21] Updated icons in bottom nav to newest versions. Fixes issue #838. --- .../fdroid/views/apps/CategorySpan.java | 2 +- app/src/main/res/drawable/ic_categories.xml | 6 +++++ app/src/main/res/drawable/ic_category.xml | 9 ------- app/src/main/res/drawable/ic_latest.xml | 9 +++++++ app/src/main/res/drawable/ic_my_apps.xml | 6 ----- app/src/main/res/drawable/ic_nearby.xml | 26 ++++++++++++------- app/src/main/res/drawable/ic_overview.xml | 6 ----- app/src/main/res/drawable/ic_settings.xml | 10 +++---- app/src/main/res/drawable/ic_updates.xml | 6 +++++ .../main/res/menu/main_activity_screens.xml | 6 ++--- 10 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 app/src/main/res/drawable/ic_categories.xml delete mode 100644 app/src/main/res/drawable/ic_category.xml create mode 100644 app/src/main/res/drawable/ic_latest.xml delete mode 100644 app/src/main/res/drawable/ic_my_apps.xml delete mode 100644 app/src/main/res/drawable/ic_overview.xml create mode 100644 app/src/main/res/drawable/ic_updates.xml diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/CategorySpan.java b/app/src/main/java/org/fdroid/fdroid/views/apps/CategorySpan.java index d2c145f58..a864c483b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/CategorySpan.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/CategorySpan.java @@ -119,7 +119,7 @@ public class CategorySpan extends ReplacementSpan { canvas.drawRoundRect(iconBackgroundRect, cornerRadius, cornerRadius, iconBackgroundPaint); // Category icon on top of the circular background which was just drawn. - Drawable icon = ContextCompat.getDrawable(context, R.drawable.ic_category); + Drawable icon = ContextCompat.getDrawable(context, R.drawable.ic_categories); icon.setBounds(iconPadding, iconPadding, iconPadding + iconSize, iconPadding + iconSize); icon.draw(canvas); diff --git a/app/src/main/res/drawable/ic_categories.xml b/app/src/main/res/drawable/ic_categories.xml new file mode 100644 index 000000000..ab39a6725 --- /dev/null +++ b/app/src/main/res/drawable/ic_categories.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_category.xml b/app/src/main/res/drawable/ic_category.xml deleted file mode 100644 index b71523ac0..000000000 --- a/app/src/main/res/drawable/ic_category.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_latest.xml b/app/src/main/res/drawable/ic_latest.xml new file mode 100644 index 000000000..c9df81f3a --- /dev/null +++ b/app/src/main/res/drawable/ic_latest.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/ic_my_apps.xml b/app/src/main/res/drawable/ic_my_apps.xml deleted file mode 100644 index 8bb1ec68b..000000000 --- a/app/src/main/res/drawable/ic_my_apps.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_nearby.xml b/app/src/main/res/drawable/ic_nearby.xml index c05402310..cbf95c45b 100644 --- a/app/src/main/res/drawable/ic_nearby.xml +++ b/app/src/main/res/drawable/ic_nearby.xml @@ -1,12 +1,18 @@ - + + + + + - - + android:pathData="m4.47,27.01c-0.16,-0.98 -0.25,-1.98 -0.25,-3.01 0,-10.22 8.31,-18.53 18.53,-18.53 4.68,0 8.97,1.75 12.23,4.63C35.81,9.08 36.85,8.24 38.03,7.64 34.02,3.9 28.65,1.6 22.75,1.6 10.4,1.6 0.35,11.65 0.35,24c0,2.1 0.3,4.13 0.84,6.05 0.87,-1.23 1.99,-2.27 3.28,-3.04" + android:strokeColor="#00000000" android:strokeWidth="1"/> diff --git a/app/src/main/res/drawable/ic_overview.xml b/app/src/main/res/drawable/ic_overview.xml deleted file mode 100644 index 8aec73afc..000000000 --- a/app/src/main/res/drawable/ic_overview.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index 7fb01b86a..583bfe9f1 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/ic_updates.xml b/app/src/main/res/drawable/ic_updates.xml new file mode 100644 index 000000000..7da41aced --- /dev/null +++ b/app/src/main/res/drawable/ic_updates.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/menu/main_activity_screens.xml b/app/src/main/res/menu/main_activity_screens.xml index 5573400ce..54cb02f4a 100644 --- a/app/src/main/res/menu/main_activity_screens.xml +++ b/app/src/main/res/menu/main_activity_screens.xml @@ -3,12 +3,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> Date: Sat, 11 Mar 2017 08:38:02 +1100 Subject: [PATCH 02/21] Added dependency to make working with complex RecyclerViews more manageable. There were a few different options around, but some of the best ones which provided the most flexibility when adding diverse/complex viewTypes to a recycler view target a minsdk above 10. The "adapterdelegates" library still offers a big improvement on vanila adapters, especially for the Updates view. --- app/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index d7bedaf25..6f3b48811 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,6 +47,10 @@ dependencies { } compile 'io.reactivex:rxjava:1.1.0' compile 'io.reactivex:rxandroid:0.23.0' + compile('com.hannesdorfmann:adapterdelegates3:3.0.1') { + exclude module: 'support-annotations' + exclude module: 'recyclerview-v7' + } testCompile 'junit:junit:4.12' @@ -120,6 +124,7 @@ if (!hasProperty('sourceDeps')) { 'com.android.support:support-vector-drawable:071ae3695bf8427d3cbfc8791492a3d9c804a4b111aa2a72fbfe7790ea268e5d', 'com.android.support:transition:9fd1e6d27cb70b3c5cd19f842b48bbb05cb4e5c93a22372769c342523393e8ea', 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259', + 'com.hannesdorfmann:adapterdelegates3:1b20d099d6e7afe57aceca13b713b386959d94a247c3c06a7aeb65b866ece02f', 'com.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b', 'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905', 'com.madgag.spongycastle:prov:029f26cd6b67c06ffa05702d426d472c141789001bcb15b7262ed86c868e5643', From d4716e63877a9f4b718b9198dbb973371104232f Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 14 Mar 2017 08:27:41 +1100 Subject: [PATCH 03/21] Added comments to further explain broadcasts. --- .../fdroid/fdroid/AppUpdateStatusManager.java | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 8ca9fa56d..91d5b965f 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -35,12 +35,39 @@ import java.util.Map; */ public final class AppUpdateStatusManager { - static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange"; - static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add"; - static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change"; - static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove"; - static final String EXTRA_APK_URL = "urlstring"; - static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate"; + /** + * Broadcast when: + * * The user clears the list of installed apps from notification manager. + * * The user clears the list of apps available to update from the notification manager. + * * A repo update is completed and a bunch of new apps are ready to be updated. + */ + public static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange"; + + /** + * Broadcast when an app begins the download/install process (either manually or via an automatic download). + */ + public static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add"; + + /** + * When the {@link AppUpdateStatus#status} of an app changes or the download progress for an app advances. + */ + public static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change"; + + /** + * Broadcast when: + * * The associated app has the {@link Status#Installed} status, and the user either visits + * that apps details page or clears the individual notification for the app. + * * The download for an app is cancelled. + */ + public static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove"; + + public static final String EXTRA_APK_URL = "urlstring"; + + /** + * 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. + */ + public static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate"; private static final String LOGTAG = "AppUpdateStatusManager"; From 7659c3a33f3e798009571fb95c812a0e3b393128 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 14 Mar 2017 08:30:31 +1100 Subject: [PATCH 04/21] Renaming 'My Apps' to 'Updates'. This doesn't change the `MyAppsAdapter` and friends, because they will be gutted in a subsequent commit. --- .../org/fdroid/fdroid/views/main/MainActivity.java | 10 +++++----- .../org/fdroid/fdroid/views/main/MainViewAdapter.java | 8 ++++---- .../fdroid/fdroid/views/main/MainViewController.java | 2 +- app/src/main/res/menu/main_activity_screens.xml | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) 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 8d62b8f9b..cfcd37f8b 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 @@ -35,7 +35,7 @@ import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; * + Whats new * + Categories list * + App swap - * + My apps + * + Updates * + Settings * * Users navigate between items by using the bottom navigation bar, or by swiping left and right. @@ -46,7 +46,7 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV private static final String TAG = "MainActivity"; - public static final String EXTRA_VIEW_MY_APPS = "org.fdroid.fdroid.views.main.MainActivity.VIEW_MY_APPS"; + public static final String EXTRA_VIEW_UPDATES = "org.fdroid.fdroid.views.main.MainActivity.VIEW_UPDATES"; private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled"; @@ -99,9 +99,9 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV FDroidApp.checkStartTor(this); - if (getIntent().hasExtra(EXTRA_VIEW_MY_APPS)) { - getIntent().removeExtra(EXTRA_VIEW_MY_APPS); - pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.my_apps)); + if (getIntent().hasExtra(EXTRA_VIEW_UPDATES)) { + getIntent().removeExtra(EXTRA_VIEW_UPDATES); + pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.updates)); } // AppDetails 2 and RepoDetailsActivity set different NFC actions, so reset here diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java index 7f3762204..98c30d726 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewAdapter.java @@ -13,7 +13,7 @@ import org.fdroid.fdroid.R; * + Whats new * + Categories * + Nearby - * + My Apps + * + Updates * + Settings * * It is responsible for understanding the relationship between each main view that is reachable @@ -35,7 +35,7 @@ class MainViewAdapter extends RecyclerView.Adapter { positionToId.put(0, R.id.whats_new); positionToId.put(1, R.id.categories); positionToId.put(2, R.id.nearby); - positionToId.put(3, R.id.my_apps); + positionToId.put(3, R.id.updates); positionToId.put(4, R.id.settings); } @@ -52,8 +52,8 @@ class MainViewAdapter extends RecyclerView.Adapter { case R.id.nearby: holder.bindSwapView(); break; - case R.id.my_apps: - holder.bindMyApps(); + case R.id.updates: + holder.bindUpdates(); break; case R.id.settings: holder.bindSettingsView(); 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 cecced816..ef006d4ef 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 @@ -40,7 +40,7 @@ class MainViewController extends RecyclerView.ViewHolder { /** * @see MyAppsViewBinder */ - public void bindMyApps() { + public void bindUpdates() { new MyAppsViewBinder(activity, frame); } diff --git a/app/src/main/res/menu/main_activity_screens.xml b/app/src/main/res/menu/main_activity_screens.xml index 54cb02f4a..a172646df 100644 --- a/app/src/main/res/menu/main_activity_screens.xml +++ b/app/src/main/res/menu/main_activity_screens.xml @@ -17,10 +17,10 @@ app:showAsAction="ifRoom|withText" android:id="@+id/nearby" /> + android:id="@+id/updates" /> Date: Tue, 14 Mar 2017 08:32:56 +1100 Subject: [PATCH 05/21] Updates: Implemented new UI for "Updates" screen. Alows for more flexibility in what we are able to display, including: * Prompting users to donate to frequently updated apps * Showing messages from package maintainers to users * Marking apps for later installation when offline Most of these are not yet implemented, but will be able to when required, whereas they were not able to in the previous UI. --- .../fdroid/fdroid/AppUpdateStatusManager.java | 30 +- .../fdroid/views/main/MainViewController.java | 6 +- .../myapps/InstalledHeaderController.java | 10 - .../fdroid/views/myapps/MyAppsAdapter.java | 92 ----- .../fdroid/views/myapps/MyAppsViewBinder.java | 77 ---- .../views/myapps/UpdatesHeaderController.java | 38 -- .../fdroid/views/updates/UpdatesAdapter.java | 355 ++++++++++++++++++ .../views/updates/UpdatesViewBinder.java | 26 ++ .../views/updates/items/AppNotification.java | 56 +++ .../fdroid/views/updates/items/AppStatus.java | 57 +++ .../views/updates/items/AppUpdateData.java | 16 + .../views/updates/items/DonationPrompt.java | 54 +++ .../views/updates/items/UpdateableApp.java | 57 +++ .../updates/items/UpdateableAppsHeader.java | 120 ++++++ .../{main_tabs.xml => main_tab_updates.xml} | 10 +- .../res/layout/my_apps_updates_header.xml | 33 -- .../res/layout/updateable_app_list_item.xml | 62 +++ .../res/layout/updateable_app_status_item.xml | 87 +++++ app/src/main/res/layout/updates_header.xml | 61 +++ app/src/main/res/values/ids.xml | 3 - app/src/main/res/values/strings.xml | 15 +- 21 files changed, 990 insertions(+), 275 deletions(-) delete mode 100644 app/src/main/java/org/fdroid/fdroid/views/myapps/InstalledHeaderController.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsAdapter.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/views/myapps/MyAppsViewBinder.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/views/myapps/UpdatesHeaderController.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesViewBinder.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/items/AppNotification.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatus.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/items/AppUpdateData.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/items/DonationPrompt.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java create mode 100644 app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppsHeader.java rename app/src/main/res/layout/{main_tabs.xml => main_tab_updates.xml} (59%) delete mode 100644 app/src/main/res/layout/my_apps_updates_header.xml create mode 100644 app/src/main/res/layout/updateable_app_list_item.xml create mode 100644 app/src/main/res/layout/updateable_app_status_item.xml create mode 100644 app/src/main/res/layout/updates_header.xml 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 @@ - - - -