Merge branch 'new-ui--categories-imagery' into 'master'

Categories artwork

Closes #851

See merge request !448
This commit is contained in:
Peter Serwylo 2017-03-21 21:53:49 +00:00
commit c1cf153852
23 changed files with 173 additions and 43 deletions

View File

@ -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));
}
});
}

View File

@ -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,

View File

@ -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);

View File

@ -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();
}
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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));
}
});
}

View File

@ -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<Cursor> {
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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -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" />
<ImageView
<org.fdroid.fdroid.views.apps.FeatureImage
android:id="@+id/category_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintStart_toStartOf="@+id/category_background"
app:layout_constraintEnd_toEndOf="@+id/category_background"
app:layout_constraintTop_toTopOf="@+id/category_background"
app:layout_constraintBottom_toBottomOf="@+id/category_background"
android:layout_width="0dp"
android:layout_height="0dp"
tools:src="@drawable/category_graphics"
android:scaleType="fitStart"
android:importantForAccessibility="no"
tools:ignore="ContentDescription" />
<android.support.v7.widget.RecyclerView
@ -76,6 +81,8 @@
android:paddingBottom="@dimen/category_preview__app_list__padding__vertical"
android:clipToPadding="false"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"/>
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
</android.support.constraint.ConstraintLayout>

View File

@ -5,6 +5,24 @@
<color name="unverified">#ff999999</color>
<color name="red">#ffdd2c00</color>
<color name="category_connectivity">#CBEFEC</color>
<color name="category_development">#E2D6BC</color>
<color name="category_games">#6D6862</color>
<color name="category_graphics">#F25050</color>
<color name="category_internet">#DBDDC9</color>
<color name="category_money">#DDDDD0</color>
<color name="category_multimedia">#FF7F66</color>
<color name="category_navigation">#94D6FD</color>
<color name="category_phone_sms">#F3CFC0</color>
<color name="category_reading">#D6A07A</color>
<color name="category_science_education">#F4F4EC</color>
<color name="category_security">#6D6862</color>
<color name="category_sports_health">#72C7EA</color>
<color name="category_system">#D3DB77</color>
<color name="category_theming">#DEEFE9</color>
<color name="category_time">#FF7043</color>
<color name="category_writing">#F2E9CE</color>
<color name="fdroid_blue">#ff1976d2</color>
<color name="fdroid_blue_dark">#ff0d47a1</color>
<color name="fdroid_blue_darkest">#ff042570</color>

View File

@ -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<String> 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<String> categoriesAfterPurge = CategoryProvider.Helper.categories(context);
assertContainsOnly(categoriesAfterPurge, expectedGp);
}
@Test
public void queryFreeTextAndCategories() {