diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index ffb259ce8..24c9701e1 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -10,6 +10,7 @@ import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; +import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.support.design.widget.CoordinatorLayout; @@ -108,7 +109,7 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { - featureImage.setPalette(palette); + featureImage.setColour(palette.getDominantColor(Color.LTGRAY)); } }); } diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 67d773a63..41bad692f 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -58,7 +58,7 @@ import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.InstalledAppProviderService; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.installer.InstallHistoryService; -import org.fdroid.fdroid.net.IconDownloader; +import org.fdroid.fdroid.net.ImageLoaderForUIL; import org.fdroid.fdroid.net.WifiStateChangeService; import java.net.URL; @@ -280,7 +280,7 @@ public class FDroidApp extends Application { bluetoothAdapter = getBluetoothAdapter(); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()) - .imageDownloader(new IconDownloader(getApplicationContext())) + .imageDownloader(new ImageLoaderForUIL(getApplicationContext())) .diskCache(new LimitedAgeDiskCache( Utils.getIconsCacheDir(this), null, diff --git a/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java b/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java index 74e3209be..59836d8b2 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java @@ -11,6 +11,8 @@ import android.support.annotation.NonNull; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.Schema.CatJoinTable; import org.fdroid.fdroid.data.Schema.CategoryTable; +import org.fdroid.fdroid.data.Schema.AppMetadataTable; +import org.fdroid.fdroid.data.Schema.PackageTable; import org.fdroid.fdroid.data.Schema.CategoryTable.Cols; import java.util.ArrayList; @@ -96,13 +98,9 @@ public class CategoryProvider extends FDroidProvider { private class Query extends QueryBuilder { - private boolean onlyCategoriesWithApps; - @Override protected String getRequiredTables() { - String joinType = onlyCategoriesWithApps ? " JOIN " : " LEFT JOIN "; - - return CategoryTable.NAME + joinType + CatJoinTable.NAME + " ON (" + + return CategoryTable.NAME + " LEFT JOIN " + CatJoinTable.NAME + " ON (" + CatJoinTable.Cols.CATEGORY_ID + " = " + CategoryTable.NAME + "." + Cols.ROW_ID + ") "; } @@ -116,8 +114,11 @@ public class CategoryProvider extends FDroidProvider { return CategoryTable.NAME + "." + Cols.ROW_ID; } - public void setOnlyCategoriesWithApps(boolean onlyCategoriesWithApps) { - this.onlyCategoriesWithApps = onlyCategoriesWithApps; + public void setOnlyCategoriesWithApps() { + // Make sure that metadata from the preferred repository is used to determine if + // there is an app present or not. + join(AppMetadataTable.NAME, "app", "app." + AppMetadataTable.Cols.ROW_ID + " = " + CatJoinTable.NAME + "." + CatJoinTable.Cols.APP_METADATA_ID); + join(PackageTable.NAME, "pkg", "pkg." + PackageTable.Cols.PREFERRED_METADATA + " = " + "app." + AppMetadataTable.Cols.ROW_ID); } } @@ -218,7 +219,10 @@ public class CategoryProvider extends FDroidProvider { query.addSelection(selection); query.addFields(projection); query.addOrderBy(sortOrder); - query.setOnlyCategoriesWithApps(onlyCategoriesWithApps); + + if (onlyCategoriesWithApps) { + query.setOnlyCategoriesWithApps(); + } Cursor cursor = LoggingQuery.query(db(), query.toString(), query.getArgs()); cursor.setNotificationUri(getContext().getContentResolver(), uri); diff --git a/app/src/main/java/org/fdroid/fdroid/net/IconDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/ImageLoaderForUIL.java similarity index 51% rename from app/src/main/java/org/fdroid/fdroid/net/IconDownloader.java rename to app/src/main/java/org/fdroid/fdroid/net/ImageLoaderForUIL.java index 1ba751f99..508d9df06 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/IconDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/ImageLoaderForUIL.java @@ -2,16 +2,20 @@ package org.fdroid.fdroid.net; import android.content.Context; -import com.nostra13.universalimageloader.core.download.ImageDownloader; +import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import java.io.IOException; import java.io.InputStream; -public class IconDownloader implements ImageDownloader { +/** + * Class used by the Universal Image Loader library (UIL) to fetch images for displaying in F-Droid. + * See {@link org.fdroid.fdroid.FDroidApp} for where this gets configured. + */ +public class ImageLoaderForUIL implements com.nostra13.universalimageloader.core.download.ImageDownloader { private final Context context; - public IconDownloader(Context context) { + public ImageLoaderForUIL(Context context) { this.context = context; } @@ -20,8 +24,13 @@ public class IconDownloader implements ImageDownloader { switch (Scheme.ofUri(imageUri)) { case ASSETS: return context.getAssets().open(Scheme.ASSETS.crop(imageUri)); + + case DRAWABLE: + return new BaseImageDownloader(context).getStream(imageUri, extra); + default: return DownloaderFactory.create(context, imageUri).getInputStream(); } } + } 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 bbfbfe9a7..d2c145f58 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 @@ -107,7 +107,7 @@ public class CategorySpan extends ReplacementSpan { // The background which goes behind the text. Paint backgroundPaint = new Paint(); - backgroundPaint.setColor(CategoryController.getBackgroundColour(categoryName.toString())); + backgroundPaint.setColor(CategoryController.getBackgroundColour(context, categoryName.toString())); backgroundPaint.setAntiAlias(true); canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint); diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java b/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java index b43dc2bb9..08dd3cdcd 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java @@ -8,7 +8,9 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; +import android.graphics.PorterDuff; import android.os.Build; +import android.support.annotation.ColorInt; import android.support.annotation.Nullable; import android.support.v7.graphics.Palette; import android.support.v7.widget.AppCompatImageView; @@ -71,8 +73,8 @@ public class FeatureImage extends AppCompatImageView { * then creates a second variation that is slightly dimmer still. These two colours are then * randomly allocated to each triangle which is expected to be rendered. */ - public void setPalette(@Nullable Palette palette) { - if (palette == null) { + public void setColour(@ColorInt int colour) { + if (colour == 0) { trianglePaints = null; return; } @@ -80,7 +82,7 @@ public class FeatureImage extends AppCompatImageView { // It is easier to dull al colour in the HSV space, so convert to that then adjust the // saturation down and the colour value down. float[] hsv = new float[3]; - Color.colorToHSV(palette.getDominantColor(Color.LTGRAY), hsv); + Color.colorToHSV(colour, hsv); hsv[1] *= 0.5f; hsv[2] *= 0.7f; int colourOne = Color.HSVToColor(hsv); @@ -187,9 +189,8 @@ public class FeatureImage extends AppCompatImageView { /** * First try to draw whatever image was given to this view. If that doesn't exist, try to draw - * a geometric pattern based on the palette that was given to us. If we haven't had a palette - * assigned to us (using {@link FeatureImage#setPalette(Palette)}) then clear the - * view by filling it with white. + * a geometric pattern based on the palette that was given to us. If we haven't had a colour + * assigned to us (using {@link #setColour(int)}) then clear the view by filling it with white. */ @Override protected void onDraw(Canvas canvas) { @@ -200,12 +201,11 @@ public class FeatureImage extends AppCompatImageView { paint.setAlpha(currentAlpha); } - canvas.drawRect(0, 0, getWidth(), getHeight(), WHITE_PAINT); for (int i = 0; i < triangles.length; i++) { canvas.drawPath(triangles[i], trianglePaints[i]); } } else { - canvas.drawRect(0, 0, getWidth(), getHeight(), WHITE_PAINT); + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java index cefe85d6f..bf80ab41a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java @@ -3,6 +3,7 @@ package org.fdroid.fdroid.views.categories; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.support.annotation.IdRes; @@ -111,7 +112,7 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL } if (featuredImage != null) { - featuredImage.setPalette(null); + featuredImage.setColour(0); featuredImage.setImageDrawable(null); } @@ -154,7 +155,7 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { - featuredImage.setPalette(palette); + featuredImage.setColour(palette.getDominantColor(Color.LTGRAY)); } }); } 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 822236a23..aeb65eedd 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 @@ -4,11 +4,14 @@ import android.app.Activity; import android.content.Intent; import android.content.Context; import android.database.Cursor; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.v4.app.LoaderManager; +import android.support.v4.content.ContextCompat; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.ViewCompat; @@ -18,21 +21,29 @@ import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; +import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; + import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.views.apps.AppListActivity; +import org.fdroid.fdroid.views.apps.FeatureImage; import java.util.Random; public class CategoryController extends RecyclerView.ViewHolder implements LoaderManager.LoaderCallbacks { private final Button viewAll; private final TextView heading; + private final FeatureImage image; private final AppPreviewAdapter appCardsAdapter; private final FrameLayout background; private final Activity activity; private final LoaderManager loaderManager; + private final DisplayImageOptions displayImageOptions; private String currentCategory; @@ -48,25 +59,66 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade viewAll.setOnClickListener(onViewAll); heading = (TextView) itemView.findViewById(R.id.name); - + image = (FeatureImage) itemView.findViewById(R.id.category_image); background = (FrameLayout) itemView.findViewById(R.id.category_background); RecyclerView appCards = (RecyclerView) itemView.findViewById(R.id.app_cards); appCards.setAdapter(appCardsAdapter); appCards.addItemDecoration(new ItemDecorator(activity)); + + displayImageOptions = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .imageScaleType(ImageScaleType.NONE) + .displayer(new FadeInBitmapDisplayer(100, true, true, false)) + .bitmapConfig(Bitmap.Config.RGB_565) + .build(); } void bindModel(@NonNull String categoryName) { currentCategory = categoryName; - heading.setText(categoryName); + + int categoryNameId = getCategoryResource(activity, categoryName, "string", false); + String translatedName = categoryNameId == 0 ? categoryName : activity.getString(categoryNameId); + heading.setText(translatedName); + viewAll.setVisibility(View.INVISIBLE); + loaderManager.initLoader(currentCategory.hashCode(), null, this); loaderManager.initLoader(currentCategory.hashCode() + 1, null, this); - background.setBackgroundColor(getBackgroundColour(categoryName)); + @ColorInt int backgroundColour = getBackgroundColour(activity, categoryName); + background.setBackgroundColor(backgroundColour); + + int categoryImageId = getCategoryResource(activity, categoryName, "drawable", true); + if (categoryImageId == 0) { + image.setColour(backgroundColour); + image.setImageDrawable(null); + } else { + image.setColour(0); + ImageLoader.getInstance().displayImage("drawable://" + categoryImageId, image, displayImageOptions); + } } - public static int getBackgroundColour(@NonNull String categoryName) { + /** + * @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 + * "category_reading". Note how drawables must have only lower case letters, whereas + * we already have upper case letters in strings.xml. Hence this flag. + */ + private static int getCategoryResource(Context context, @NonNull String categoryName, String resourceType, boolean requiresLowerCaseId) { + String suffix = categoryName.replace(" & ", "_").replace(" ", "_").replace("'", ""); + if (requiresLowerCaseId) { + suffix = suffix.toLowerCase(); + } + return context.getResources().getIdentifier("category_" + suffix, resourceType, context.getPackageName()); + } + + public static int getBackgroundColour(Context context, @NonNull String categoryName) { + int colourId = getCategoryResource(context, categoryName, "color", false); + if (colourId > 0) { + return ContextCompat.getColor(context, colourId); + } + // Seed based on the categoryName, so that each time we try to choose a colour for the same // category it will look the same for each different user, and each different session. Random random = new Random(categoryName.toLowerCase().hashCode()); @@ -130,6 +182,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade appCardsAdapter.setAppCursor(null); } + @SuppressWarnings("FieldCanBeLocal") private final View.OnClickListener onViewAll = new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/res/drawable/category_connectivity.png b/app/src/main/res/drawable/category_connectivity.png new file mode 100644 index 000000000..716b40123 Binary files /dev/null and b/app/src/main/res/drawable/category_connectivity.png differ diff --git a/app/src/main/res/drawable/category_development.png b/app/src/main/res/drawable/category_development.png new file mode 100644 index 000000000..1090030ad Binary files /dev/null and b/app/src/main/res/drawable/category_development.png differ diff --git a/app/src/main/res/drawable/category_graphics.png b/app/src/main/res/drawable/category_graphics.png new file mode 100644 index 000000000..f434775d0 Binary files /dev/null and b/app/src/main/res/drawable/category_graphics.png differ diff --git a/app/src/main/res/drawable/category_internet.png b/app/src/main/res/drawable/category_internet.png new file mode 100644 index 000000000..716b40123 Binary files /dev/null and b/app/src/main/res/drawable/category_internet.png differ diff --git a/app/src/main/res/drawable/category_money.png b/app/src/main/res/drawable/category_money.png new file mode 100644 index 000000000..c28c26ddf Binary files /dev/null and b/app/src/main/res/drawable/category_money.png differ diff --git a/app/src/main/res/drawable/category_navigation.png b/app/src/main/res/drawable/category_navigation.png new file mode 100644 index 000000000..0aabe7b29 Binary files /dev/null and b/app/src/main/res/drawable/category_navigation.png differ diff --git a/app/src/main/res/drawable/category_reading.png b/app/src/main/res/drawable/category_reading.png new file mode 100644 index 000000000..6b34b55e2 Binary files /dev/null and b/app/src/main/res/drawable/category_reading.png differ diff --git a/app/src/main/res/drawable/category_science_education.png b/app/src/main/res/drawable/category_science_education.png new file mode 100644 index 000000000..0895f1b5a Binary files /dev/null and b/app/src/main/res/drawable/category_science_education.png differ diff --git a/app/src/main/res/drawable/category_security.png b/app/src/main/res/drawable/category_security.png new file mode 100644 index 000000000..e9b3941d3 Binary files /dev/null and b/app/src/main/res/drawable/category_security.png differ diff --git a/app/src/main/res/drawable/category_system.png b/app/src/main/res/drawable/category_system.png new file mode 100644 index 000000000..6eba35151 Binary files /dev/null and b/app/src/main/res/drawable/category_system.png differ diff --git a/app/src/main/res/drawable/category_theming.png b/app/src/main/res/drawable/category_theming.png new file mode 100644 index 000000000..afa70277f Binary files /dev/null and b/app/src/main/res/drawable/category_theming.png differ diff --git a/app/src/main/res/drawable/category_writing.png b/app/src/main/res/drawable/category_writing.png new file mode 100644 index 000000000..7f143cb69 Binary files /dev/null and b/app/src/main/res/drawable/category_writing.png differ diff --git a/app/src/main/res/layout/category_item.xml b/app/src/main/res/layout/category_item.xml index d2e516f80..100142f88 100644 --- a/app/src/main/res/layout/category_item.xml +++ b/app/src/main/res/layout/category_item.xml @@ -29,7 +29,7 @@ android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" - android:background="@android:color/transparent" + android:background="?attr/selectableItemBackground" android:paddingLeft="18dp" android:paddingStart="18dp" android:paddingRight="18dp" @@ -50,14 +50,19 @@ app:layout_constraintBottom_toBottomOf="@+id/app_cards" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - tools:background="#ffffbbbb"/> + tools:background="#ffffbbbb" /> - + android:layout_marginRight="8dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" /> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d3e57d942..b7025b7d9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,6 +5,24 @@ #ff999999 #ffdd2c00 + #CBEFEC + #E2D6BC + #6D6862 + #F25050 + #DBDDC9 + #DDDDD0 + #FF7F66 + #94D6FD + #F3CFC0 + #D6A07A + #F4F4EC + #6D6862 + #72C7EA + #D3DB77 + #DEEFE9 + #FF7043 + #F2E9CE + #ff1976d2 #ff0d47a1 #ff042570 diff --git a/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java index 5903f335b..72b5dfbe0 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java @@ -30,12 +30,49 @@ public class CategoryProviderTest extends FDroidProviderTest { TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class); } - // ======================================================================== - // "Categories" - // (at this point) not an additional table, but we treat them sort of - // like they are. That means that if we change the implementation to - // use a separate table in the future, these should still pass. - // ======================================================================== + /** + * Different repositories can specify a different set of categories for the same package. + * In this case, only the repository with the highest priority should get to choose which + * category the app goes in. + */ + @Test + public void onlyHighestPriorityMetadataDefinesCategories() { + long mainRepo = 1; + long gpRepo = 3; + + insertAppWithCategory("info.guardianproject.notepadbot", "NoteCipher", "Writing,Security", mainRepo); + insertAppWithCategory("com.dog.rock.apple", "Dog-Rock-Apple", "Animal,Mineral,Vegetable", mainRepo); + insertAppWithCategory("com.banana.apple", "Banana", "Vegetable,Vegetable", mainRepo); + + List categories = CategoryProvider.Helper.categories(context); + String[] expected = new String[] { + context.getResources().getString(R.string.category_Whats_New), + context.getResources().getString(R.string.category_Recently_Updated), + context.getResources().getString(R.string.category_All), + + "Animal", + "Mineral", + "Security", + "Vegetable", + "Writing", + }; + assertContainsOnly(categories, expected); + + insertAppWithCategory("info.guardianproject.notepadbot", "NoteCipher", "Office,GuardianProject", gpRepo); + assertContainsOnly(CategoryProvider.Helper.categories(context), expected); + + RepoProvider.Helper.purgeApps(context, new MockRepo(mainRepo)); + String[] expectedGp = new String[] { + context.getResources().getString(R.string.category_Whats_New), + context.getResources().getString(R.string.category_Recently_Updated), + context.getResources().getString(R.string.category_All), + + "GuardianProject", + "Office", + }; + List categoriesAfterPurge = CategoryProvider.Helper.categories(context); + assertContainsOnly(categoriesAfterPurge, expectedGp); + } @Test public void queryFreeTextAndCategories() {