From d83c15d0d4336d6555ae741d0e85f90cf9886bc4 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 5 Apr 2017 10:45:15 +1000 Subject: [PATCH 1/7] Show "Updating repositories" banner in main UI. Previously this was only shown in the notifications. This does not show the full progress of the update, but at least it provides a rudimentary level of feedback. In the future it can be modified to show more substantial feedback if required. --- .../java/org/fdroid/fdroid/UpdateService.java | 13 +++ .../fdroid/views/BannerUpdatingRepos.java | 81 +++++++++++++++++++ .../main/res/layout/main_tab_categories.xml | 34 ++++++-- .../main/res/layout/main_tab_whats_new.xml | 22 +++-- app/src/main/res/values/dimens.xml | 2 + 5 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/views/BannerUpdatingRepos.java diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 265651e6f..de53b6432 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -90,6 +90,8 @@ public class UpdateService extends IntentService { private NotificationCompat.Builder notificationBuilder; private AppUpdateStatusManager appUpdateStatusManager; + private static boolean updating; + public UpdateService() { super("UpdateService"); } @@ -136,6 +138,14 @@ public class UpdateService extends IntentService { } + /** + * Whether or not a repo update is currently in progress. Used to show feedback throughout + * the app to users, so they know something is happening. + */ + public static boolean isUpdating() { + return updating; + } + @Override public void onCreate() { super.onCreate(); @@ -365,6 +375,7 @@ public class UpdateService extends IntentService { return; } + updating = true; notificationManager.notify(NOTIFY_ID_UPDATING, notificationBuilder.build()); LocalBroadcastManager.getInstance(this).registerReceiver(updateStatusReceiver, new IntentFilter(LOCAL_ACTION_STATUS)); @@ -452,6 +463,8 @@ public class UpdateService extends IntentService { } catch (Exception e) { Log.e(TAG, "Exception during update processing", e); sendStatus(this, STATUS_ERROR_GLOBAL, e.getMessage()); + } finally { + updating = false; } long time = System.currentTimeMillis() - startTime; diff --git a/app/src/main/java/org/fdroid/fdroid/views/BannerUpdatingRepos.java b/app/src/main/java/org/fdroid/fdroid/views/BannerUpdatingRepos.java new file mode 100644 index 000000000..a5eff101f --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/BannerUpdatingRepos.java @@ -0,0 +1,81 @@ +package org.fdroid.fdroid.views; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.v4.content.LocalBroadcastManager; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; + +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; + +/** + * Widget which reflects whether or not a repo update is currently in progress or not. If so, shows + * some sort of feedback to the user. + */ +public class BannerUpdatingRepos extends android.support.v7.widget.AppCompatTextView { + + public BannerUpdatingRepos(Context context) { + this(context, null); + } + + public BannerUpdatingRepos(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + public BannerUpdatingRepos(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + int padding = (int) getResources().getDimension(R.dimen.banner__padding); + setPadding(padding, padding, padding, padding); + setBackgroundColor(0xFF4A4A4A); + setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); + setText(R.string.update_notification_title); + setTextColor(0xFFFFFFFF); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + monitorRepoUpdates(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + stopMonitoringRepoUpdates(); + } + + private void monitorRepoUpdates() { + if (isInEditMode()) { + return; + } + + LocalBroadcastManager.getInstance(getContext()).registerReceiver(onRepoFeedback, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS)); + setBannerIsVisible(UpdateService.isUpdating()); + } + + private void setBannerIsVisible(boolean isUpdating) { + if (isUpdating) { + setVisibility(View.VISIBLE); + } else { + setVisibility(View.GONE); + } + } + + private void stopMonitoringRepoUpdates() { + LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onRepoFeedback); + } + + private final BroadcastReceiver onRepoFeedback = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Anything other than a STATUS_INFO broadcast signifies that it was complete (and out + // banner should be removed). + boolean isInfo = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, 0) == UpdateService.STATUS_INFO; + setBannerIsVisible(isInfo); + } + }; +} diff --git a/app/src/main/res/layout/main_tab_categories.xml b/app/src/main/res/layout/main_tab_categories.xml index 2d9310292..22257c8b3 100644 --- a/app/src/main/res/layout/main_tab_categories.xml +++ b/app/src/main/res/layout/main_tab_categories.xml @@ -1,5 +1,5 @@ - + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:scrollbars="vertical" + tools:layout_editor_absoluteX="0dp" /> - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/main_tab_whats_new.xml b/app/src/main/res/layout/main_tab_whats_new.xml index 073f0f9a0..e0cc009e2 100644 --- a/app/src/main/res/layout/main_tab_whats_new.xml +++ b/app/src/main/res/layout/main_tab_whats_new.xml @@ -12,15 +12,23 @@ android:layout_height="match_parent" android:id="@+id/swipe_to_refresh"> - + android:orientation="vertical"> + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 547e4acc4..054dd03be 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -30,4 +30,6 @@ 4dp 72dp 18dp + + 4dp From 510ec5f7c773f5f4712fe0ec81551d65d5cf31b9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 5 Apr 2017 12:03:50 +1000 Subject: [PATCH 2/7] Adding ability to query last repo update and number of repos. This will be used to improve the empty state handling of the main list of latest apps. --- .../org/fdroid/fdroid/data/RepoProvider.java | 35 ++++++++++++ .../fdroid/fdroid/data/RepoProviderTest.java | 53 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java index e8491f00b..ac0dd0198 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java @@ -16,6 +16,7 @@ import org.fdroid.fdroid.data.Schema.RepoTable; import org.fdroid.fdroid.data.Schema.RepoTable.Cols; import java.util.ArrayList; +import java.util.Date; import java.util.List; public class RepoProvider extends FDroidProvider { @@ -251,6 +252,40 @@ public class RepoProvider extends FDroidProvider { } return count; } + + @Nullable + public static Date lastUpdate(Context context) { + ContentResolver resolver = context.getContentResolver(); + final String[] projection = {Cols.LAST_UPDATED}; + final String selection = Cols.IN_USE + " = 1"; + Cursor cursor = resolver.query(getContentUri(), projection, selection, null, Cols.LAST_UPDATED + " DESC"); + + Date lastUpdate = null; + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + lastUpdate = Utils.parseDate(cursor.getString(0), null); + } + cursor.close(); + } + + return lastUpdate; + } + + public static int countEnabledRepos(Context context) { + ContentResolver resolver = context.getContentResolver(); + final String[] projection = {Cols._ID}; + final String selection = Cols.IN_USE + " = 1"; + Cursor cursor = resolver.query(getContentUri(), projection, selection, null, null); + + int count = 0; + if (cursor != null) { + count = cursor.getCount(); + cursor.close(); + } + + return count; + } } private static final String PROVIDER_NAME = "RepoProvider"; diff --git a/app/src/test/java/org/fdroid/fdroid/data/RepoProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/RepoProviderTest.java index ae6495920..2570ade73 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/RepoProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/RepoProviderTest.java @@ -36,6 +36,7 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; @@ -48,6 +49,58 @@ public class RepoProviderTest extends FDroidProviderTest { private static final String[] COLS = RepoTable.Cols.ALL; + @Test + public void countEnabledRepos() { + + // By default, f-droid is enabled. + assertEquals(1, RepoProvider.Helper.countEnabledRepos(context)); + + Repo gpRepo = RepoProvider.Helper.findByAddress(context, "https://guardianproject.info/fdroid/repo"); + gpRepo = setEnabled(gpRepo, true); + assertEquals(2, RepoProvider.Helper.countEnabledRepos(context)); + + Repo fdroidRepo = RepoProvider.Helper.findByAddress(context, "https://f-droid.org/repo"); + setEnabled(fdroidRepo, false); + setEnabled(gpRepo, false); + + assertEquals(0, RepoProvider.Helper.countEnabledRepos(context)); + } + + private Repo setEnabled(Repo repo, boolean enabled) { + ContentValues enable = new ContentValues(1); + enable.put(RepoTable.Cols.IN_USE, enabled); + RepoProvider.Helper.update(context, repo, enable); + return RepoProvider.Helper.findByAddress(context, repo.address); + } + + @Test + public void lastUpdated() { + assertNull(RepoProvider.Helper.lastUpdate(context)); + + Repo gpRepo = RepoProvider.Helper.findByAddress(context, "https://guardianproject.info/fdroid/repo"); + + // Set date to 2017-04-05 11:56:38 + setLastUpdate(gpRepo, new Date(1491357408643L)); + + // GP is not yet enabled, so it is not counted. + assertNull(RepoProvider.Helper.lastUpdate(context)); + + // Set date to 2017-04-04 11:56:38 + Repo fdroidRepo = RepoProvider.Helper.findByAddress(context, "https://f-droid.org/repo"); + setLastUpdate(fdroidRepo, new Date(1491357408643L - (1000 * 60 * 60 * 24))); + assertEquals("2017-04-04", Utils.formatDate(RepoProvider.Helper.lastUpdate(context), null)); + + setEnabled(gpRepo, true); + assertEquals("2017-04-05", Utils.formatDate(RepoProvider.Helper.lastUpdate(context), null)); + } + + private Repo setLastUpdate(Repo repo, Date date) { + ContentValues values = new ContentValues(1); + values.put(RepoTable.Cols.LAST_UPDATED, Utils.formatDate(date, null)); + RepoProvider.Helper.update(context, repo, values); + return RepoProvider.Helper.findByAddress(context, repo.address); + } + @Test public void findByUrl() { From ab5ea39f7c5b1af22032f2a1184aae7f43c8754c Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 5 Apr 2017 12:05:29 +1000 Subject: [PATCH 3/7] Show empty state feedback for latest + categories The text is more comprehensive on the main screen than on categories, because this is the view that all users will see when they first open F-Droid. Fixes #879. --- .../views/main/CategoriesViewBinder.java | 15 ++++++- .../fdroid/views/main/WhatsNewViewBinder.java | 43 ++++++++++++++++++- .../main/res/layout/main_tab_categories.xml | 12 ++++++ .../main/res/layout/main_tab_whats_new.xml | 8 ++++ app/src/main/res/values/strings.xml | 6 +++ app/src/main/res/values/styles.xml | 7 +++ 6 files changed, 89 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java index 9528f7ffc..462cf096b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java @@ -12,6 +12,7 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.FrameLayout; +import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.CategoryProvider; @@ -30,6 +31,8 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { private final CategoryAdapter categoryAdapter; private final AppCompatActivity activity; + private final TextView emptyState; + private final RecyclerView categoriesList; CategoriesViewBinder(final AppCompatActivity activity, FrameLayout parent) { this.activity = activity; @@ -38,7 +41,9 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { categoryAdapter = new CategoryAdapter(activity, activity.getSupportLoaderManager()); - RecyclerView categoriesList = (RecyclerView) categoriesView.findViewById(R.id.category_list); + emptyState = (TextView) categoriesView.findViewById(R.id.empty_state); + + categoriesList = (RecyclerView) categoriesView.findViewById(R.id.category_list); categoriesList.setHasFixedSize(true); categoriesList.setLayoutManager(new LinearLayoutManager(activity)); categoriesList.setAdapter(categoryAdapter); @@ -77,6 +82,14 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { } categoryAdapter.setCategoriesCursor(cursor); + + if (categoryAdapter.getItemCount() == 0) { + emptyState.setVisibility(View.VISIBLE); + categoriesList.setVisibility(View.GONE); + } else { + emptyState.setVisibility(View.GONE); + categoriesList.setVisibility(View.VISIBLE); + } } @Override diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java index 62cbe6906..4aaf66606 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/WhatsNewViewBinder.java @@ -13,14 +13,20 @@ import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.FrameLayout; +import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.whatsnew.WhatsNewAdapter; +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.TimeUnit; + /** * Loads a list of newly added or recently updated apps and displays them to the user. */ @@ -30,6 +36,8 @@ class WhatsNewViewBinder implements LoaderManager.LoaderCallbacks { private final WhatsNewAdapter whatsNewAdapter; private final AppCompatActivity activity; + private final TextView emptyState; + private final RecyclerView appList; WhatsNewViewBinder(final AppCompatActivity activity, FrameLayout parent) { this.activity = activity; @@ -41,7 +49,9 @@ class WhatsNewViewBinder implements LoaderManager.LoaderCallbacks { GridLayoutManager layoutManager = new GridLayoutManager(activity, 2); layoutManager.setSpanSizeLookup(new WhatsNewAdapter.SpanSizeLookup()); - RecyclerView appList = (RecyclerView) whatsNewView.findViewById(R.id.app_list); + emptyState = (TextView) whatsNewView.findViewById(R.id.empty_state); + + appList = (RecyclerView) whatsNewView.findViewById(R.id.app_list); appList.setHasFixedSize(true); appList.setLayoutManager(layoutManager); appList.setAdapter(whatsNewAdapter); @@ -89,6 +99,37 @@ class WhatsNewViewBinder implements LoaderManager.LoaderCallbacks { } whatsNewAdapter.setAppsCursor(cursor); + + if (whatsNewAdapter.getItemCount() == 0) { + emptyState.setVisibility(View.VISIBLE); + appList.setVisibility(View.GONE); + explainEmptyStateToUser(); + } else { + emptyState.setVisibility(View.GONE); + appList.setVisibility(View.VISIBLE); + } + } + + private void explainEmptyStateToUser() { + StringBuilder emptyStateText = new StringBuilder(); + emptyStateText.append(activity.getString(R.string.latest__empty_state__no_recent_apps)); + emptyStateText.append("\n\n"); + + int repoCount = RepoProvider.Helper.countEnabledRepos(activity); + if (repoCount == 0) { + emptyStateText.append(activity.getString(R.string.latest__empty_state__no_enabled_repos)); + } else { + Date lastUpdate = RepoProvider.Helper.lastUpdate(activity); + if (lastUpdate == null) { + emptyStateText.append(activity.getString(R.string.latest__empty_state__never_updated)); + } else { + long msDiff = Calendar.getInstance().getTimeInMillis() - lastUpdate.getTime(); + int daysDiff = (int) TimeUnit.MILLISECONDS.toDays(msDiff); + emptyStateText.append(activity.getResources().getQuantityString(R.plurals.details_last_update_days, daysDiff, daysDiff)); + } + } + + emptyState.setText(emptyStateText.toString()); } @Override diff --git a/app/src/main/res/layout/main_tab_categories.xml b/app/src/main/res/layout/main_tab_categories.xml index 22257c8b3..27ca6f052 100644 --- a/app/src/main/res/layout/main_tab_categories.xml +++ b/app/src/main/res/layout/main_tab_categories.xml @@ -16,6 +16,18 @@ app:layout_constraintEnd_toEndOf="parent" tools:layout_editor_absoluteX="8dp" /> + + + + Categories Nearby + No recent apps found + Once your list of apps has been updated, the latest apps should show here + Once you enable a repository and let it update, the latest apps should show here + + No categories to display + My Apps Manage Installed Apps diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e46554b20..dcb4eaee0 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -93,6 +93,13 @@ +