diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java index 148ac66e7..a446b2191 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java @@ -1,6 +1,5 @@ package org.fdroid.fdroid.views.categories; -import android.util.Log; import android.view.ViewGroup; import androidx.annotation.NonNull; @@ -14,26 +13,24 @@ import org.fdroid.database.Category; import org.fdroid.database.FDroidDatabase; import org.fdroid.fdroid.R; -import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; -public class CategoryAdapter extends ListAdapter { +public class CategoryAdapter extends ListAdapter { private final AppCompatActivity activity; private final FDroidDatabase db; private final HashMap>> liveData = new HashMap<>(); public CategoryAdapter(AppCompatActivity activity, FDroidDatabase db) { - super(new DiffUtil.ItemCallback() { + super(new DiffUtil.ItemCallback() { @Override - public boolean areItemsTheSame(@NonNull Category oldItem, @NonNull Category newItem) { - return oldItem.equals(newItem); + public boolean areItemsTheSame(@NonNull CategoryItem oldItem, @NonNull CategoryItem newItem) { + return oldItem.category.equals(newItem.category); } @Override - public boolean areContentsTheSame(@NonNull Category oldItem, @NonNull Category newItem) { + public boolean areContentsTheSame(@NonNull CategoryItem oldItem, @NonNull CategoryItem newItem) { return false; } }); @@ -51,31 +48,17 @@ public class CategoryAdapter extends ListAdapter { @Override public void onBindViewHolder(@NonNull CategoryController holder, int position) { - Category category = getItem(position); - holder.bindModel(category, liveData.get(category), this::onNoApps); + CategoryItem item = getItem(position); + holder.bindModel(item, liveData.get(item.category)); } - public void setCategories(@NonNull List categories) { - submitList(categories); - for (Category category : categories) { + public void setCategories(@NonNull List items) { + submitList(items); + for (CategoryItem item : items) { int num = CategoryController.NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW; // we are getting the LiveData here and not in the ViewHolder, so the data gets cached here // this prevents reloads when scrolling - liveData.put(category, db.getAppDao().getAppOverviewItems(category.getId(), num)); + liveData.put(item.category, db.getAppDao().getAppOverviewItems(item.category.getId(), num)); } } - - private void onNoApps(Category category) { - ArrayList categories = new ArrayList<>(getCurrentList()); - Iterator itr = categories.iterator(); - while (itr.hasNext()) { - Category c = itr.next(); - if (c.getId().equals(category.getId())) { - Log.d("CategoryAdapter", "Removing " + category.getId() + " without apps."); - itr.remove(); - break; - } - } - submitList(categories); - } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java index 7d4b579db..3d374f957 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java @@ -13,11 +13,9 @@ import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.os.LocaleListCompat; -import androidx.core.util.Consumer; import androidx.core.view.ViewCompat; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; @@ -27,12 +25,10 @@ import com.bumptech.glide.Glide; import org.fdroid.database.AppOverviewItem; import org.fdroid.database.Category; -import org.fdroid.database.FDroidDatabase; import org.fdroid.database.Repository; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.apps.FeatureImage; import org.fdroid.index.v2.FileV2; @@ -41,11 +37,6 @@ import java.util.List; import java.util.Locale; import java.util.Random; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.schedulers.Schedulers; - public class CategoryController extends RecyclerView.ViewHolder { private static final String TAG = "CategoryController"; @@ -57,18 +48,14 @@ public class CategoryController extends RecyclerView.ViewHolder { private final FrameLayout background; private final AppCompatActivity activity; - private final FDroidDatabase db; static final int NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW = 20; private Category currentCategory; - @Nullable - private Disposable disposable; CategoryController(final AppCompatActivity activity, View itemView) { super(itemView); this.activity = activity; - db = DBHelper.getDb(activity); appCardsAdapter = new AppPreviewAdapter(activity); @@ -89,36 +76,32 @@ public class CategoryController extends RecyclerView.ViewHolder { return categoryNameId == 0 ? categoryName : context.getString(categoryNameId); } - void bindModel(@NonNull Category category, LiveData> liveData, - Consumer onNoApps) { + void bindModel(@NonNull CategoryItem item, LiveData> liveData) { loadAppItems(liveData); - currentCategory = category; + currentCategory = item.category; - String categoryName = category.getName(LocaleListCompat.getDefault()); - if (categoryName == null) categoryName = translateCategory(activity, category.getId()); + String categoryName = item.category.getName(LocaleListCompat.getDefault()); + if (categoryName == null) categoryName = translateCategory(activity, item.category.getId()); heading.setText(categoryName); heading.setContentDescription(activity.getString(R.string.tts_category_name, categoryName)); - viewAll.setVisibility(View.INVISIBLE); - loadNumAppsInCategory(onNoApps); - - @ColorInt int backgroundColour = getBackgroundColour(activity, category.getId()); + @ColorInt int backgroundColour = getBackgroundColour(activity, item.category.getId()); background.setBackgroundColor(backgroundColour); // try to load image from repo first - FileV2 iconFile = category.getIcon(LocaleListCompat.getDefault()); - Repository repo = FDroidApp.getRepoManager(activity).getRepository(category.getRepoId()); + FileV2 iconFile = item.category.getIcon(LocaleListCompat.getDefault()); + Repository repo = FDroidApp.getRepoManager(activity).getRepository(item.category.getRepoId()); if (iconFile != null && repo != null) { - Log.i(TAG, "Loading remote image for: " + category.getId()); + Log.i(TAG, "Loading remote image for: " + item.category.getId()); Glide.with(activity) .load(Utils.getDownloadRequest(repo, iconFile)) .apply(Utils.getAlwaysShowIconRequestOptions()) .into(image); } else { // try to get local image resource - int categoryImageId = getCategoryResource(activity, category.getId(), "drawable", true); + int categoryImageId = getCategoryResource(activity, item.category.getId(), "drawable", true); if (categoryImageId == 0) { - Log.w(TAG, "No image for: " + category.getId()); + Log.w(TAG, "No image for: " + item.category.getId()); image.setColour(backgroundColour); image.setImageDrawable(null); Glide.with(activity).clear(image); @@ -127,6 +110,10 @@ public class CategoryController extends RecyclerView.ViewHolder { Glide.with(activity).load(categoryImageId).into(image); } } + Resources r = activity.getResources(); + viewAll.setText(r.getQuantityString(R.plurals.button_view_all_apps_in_category, item.numApps, item.numApps)); + viewAll.setContentDescription(r.getQuantityString(R.plurals.tts_view_all_in_category, item.numApps, + item.numApps, currentCategory)); } private void loadAppItems(LiveData> liveData) { @@ -141,14 +128,6 @@ public class CategoryController extends RecyclerView.ViewHolder { }); } - private void loadNumAppsInCategory(Consumer onNoApps) { - if (disposable != null) disposable.dispose(); - disposable = Single.fromCallable(() -> db.getAppDao().getNumberOfAppsInCategory(currentCategory.getId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(numApps -> setNumAppsInCategory(numApps, onNoApps)); - } - /** * @param requiresLowerCaseId Previously categories were translated using strings such as "category_Reading" * for the "Reading" category. Now we also need to have drawable resources such as @@ -181,19 +160,6 @@ public class CategoryController extends RecyclerView.ViewHolder { return Color.HSVToColor(hsv); } - private void setNumAppsInCategory(int numAppsInCategory, Consumer onNoApps) { - if (numAppsInCategory == 0) { - onNoApps.accept(currentCategory); - } else { - viewAll.setVisibility(View.VISIBLE); - Resources r = activity.getResources(); - viewAll.setText(r.getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory, - numAppsInCategory)); - viewAll.setContentDescription(r.getQuantityString(R.plurals.tts_view_all_in_category, numAppsInCategory, - numAppsInCategory, currentCategory)); - } - } - @SuppressWarnings("FieldCanBeLocal") private final View.OnClickListener onViewAll = new View.OnClickListener() { @Override diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryItem.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryItem.java new file mode 100644 index 000000000..f858b9623 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryItem.java @@ -0,0 +1,14 @@ +package org.fdroid.fdroid.views.categories; + +import org.fdroid.database.Category; + +public class CategoryItem { + + public final Category category; + final int numApps; + + public CategoryItem(Category category, int numApps) { + this.category = category; + this.numApps = numApps; + } +} 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 8790f5a4f..c492627f8 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 @@ -1,10 +1,14 @@ package org.fdroid.fdroid.views.main; import android.content.Intent; +import android.util.ArraySet; +import android.util.Log; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import androidx.appcompat.app.AppCompatActivity; import androidx.core.os.LocaleListCompat; import androidx.lifecycle.Observer; @@ -25,11 +29,16 @@ import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.categories.CategoryAdapter; +import org.fdroid.fdroid.views.categories.CategoryItem; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; /** * Responsible for ensuring that the categories view is inflated and then populated correctly. @@ -39,15 +48,18 @@ import java.util.Map; class CategoriesViewBinder implements Observer> { public static final String TAG = "CategoriesViewBinder"; + private final FDroidDatabase db; + private final String[] defaultCategories; private final CategoryAdapter categoryAdapter; - private final AppCompatActivity activity; private final TextView emptyState; private final RecyclerView categoriesList; + @Nullable + private Disposable disposable; CategoriesViewBinder(final AppCompatActivity activity, FrameLayout parent) { - this.activity = activity; - FDroidDatabase db = DBHelper.getDb(activity); + db = DBHelper.getDb(activity); Transformations.distinctUntilChanged(db.getRepositoryDao().getLiveCategories()).observe(activity, this); + defaultCategories = activity.getResources().getStringArray(R.array.defaultCategories); View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true); @@ -85,27 +97,65 @@ class CategoriesViewBinder implements Observer> { */ @Override public void onChanged(List categories) { - LocaleListCompat localeListCompat = LocaleListCompat.getDefault(); - Collections.sort(categories, (o1, o2) -> { - String name1 = o1.getName(localeListCompat); - if (name1 == null) name1 = o1.getId(); - String name2 = o2.getName(localeListCompat); - if (name2 == null) name2 = o2.getId(); - return name1.compareToIgnoreCase(name2); - }); - // TODO force-adding nightly category here can be removed once fdroidserver LTS supports defining categories - ArrayList c = new ArrayList<>(categories); - Map name = Collections.singletonMap("en-US", activity.getString(R.string.category_Nightly)); - c.add(new Category(42L, "nightly", Collections.emptyMap(), name, Collections.emptyMap())); - categoryAdapter.setCategories(c); + if (disposable != null) disposable.dispose(); + disposable = Single.fromCallable(() -> loadCategoryItems(categories)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onItemsLoaded); + } - if (categoryAdapter.getItemCount() == 0) { + private void onItemsLoaded(List items) { + if (items.size() == 0) { emptyState.setVisibility(View.VISIBLE); categoriesList.setVisibility(View.GONE); } else { + categoryAdapter.setCategories(items); emptyState.setVisibility(View.GONE); categoriesList.setVisibility(View.VISIBLE); } } + @WorkerThread + private List loadCategoryItems(List categories) { + // get items + ArrayList items = new ArrayList<>(); + ArraySet ids = new ArraySet<>(categories.size()); + for (Category c : categories) { + int numApps = db.getAppDao().getNumberOfAppsInCategory(c.getId()); + if (numApps > 0) { + ids.add(c.getId()); + CategoryItem item = new CategoryItem(c, numApps); + items.add(item); + } else { + Log.d(TAG, "Not adding " + c.getId() + " because it has no apps."); + } + } + // add default categories, if they are not in already + for (String id : defaultCategories) { + if (!ids.contains(id)) { + int numApps = db.getAppDao().getNumberOfAppsInCategory(id); + if (numApps > 0) { + // name and icon gets set in CategoryController, if not given here + Category c = new Category(2L, id, Collections.emptyMap(), Collections.emptyMap(), + Collections.emptyMap()); + CategoryItem item = new CategoryItem(c, numApps); + items.add(item); + } else { + Log.d(TAG, "Not adding default " + id + " because it has no apps."); + } + } + } + // sort items + LocaleListCompat localeListCompat = LocaleListCompat.getDefault(); + Collections.sort(items, (o1, o2) -> { + ids.add(o2.category.getId()); + String name1 = o1.category.getName(localeListCompat); + if (name1 == null) name1 = o1.category.getId(); + String name2 = o2.category.getName(localeListCompat); + if (name2 == null) name2 = o2.category.getId(); + return name1.compareToIgnoreCase(name2); + }); + return items; + } + } diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index dbaa50fa4..571ed8b70 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -57,4 +57,25 @@ @string/antinsfw @string/antiothers + + + @string/category_Connectivity + @string/category_Development + @string/category_Games + @string/category_Graphics + @string/category_Internet + @string/category_Money + @string/category_Multimedia + @string/category_Navigation + @string/category_Phone_SMS + @string/category_Reading + @string/category_Science_Education + @string/category_Security + @string/category_Sports_Health + @string/category_System + @string/category_Theming + @string/category_Time + @string/category_Writing + @string/category_Nightly + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ecca8676..dbaed97b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -480,7 +480,7 @@ This often occurs with apps installed via Google Play or other sources, if they Theming Time Writing - Nightly Builds + nightly View %d