Bug 1012462 - Part 11: Support image loading for distribution files (r=rnewman)

This commit is contained in:
Lucas Rocha 2014-07-15 20:56:48 +01:00
parent 92588f6998
commit 0b216e16ae
9 changed files with 383 additions and 22 deletions

View File

@ -0,0 +1,154 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import android.content.Context;
import android.net.Uri;
import android.util.DisplayMetrics;
import android.util.Log;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Downloader.Response;
import com.squareup.picasso.UrlConnectionDownloader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;
import org.mozilla.gecko.distribution.Distribution;
public class ImageLoader {
private static final String LOGTAG = "GeckoImageLoader";
private static final String DISTRIBUTION_SCHEME = "gecko.distribution";
private static final String SUGGESTED_SITES_AUTHORITY = "suggestedsites";
// The order of density factors to try when looking for an image resource
// in the distribution directory. It looks for an exact match first (1.0) then
// tries to find images with higher density (2.0 and 1.5). If no image is found,
// try a lower density (0.5). See loadDistributionImage().
private static final float[] densityFactors = new float[] { 1.0f, 2.0f, 1.5f, 0.5f };
private static enum Density {
MDPI,
HDPI,
XHDPI,
XXHDPI;
@Override
public String toString() {
return super.toString().toLowerCase();
}
}
private static Picasso instance;
public static synchronized Picasso with(Context context) {
if (instance == null) {
Picasso.Builder builder = new Picasso.Builder(context);
final Distribution distribution = Distribution.getInstance(context);
builder.downloader(new ImageDownloader(context, distribution));
instance = builder.build();
}
return instance;
}
/**
* Custom Downloader built on top of Picasso's UrlConnectionDownloader
* that supports loading images from custom URIs.
*/
public static class ImageDownloader extends UrlConnectionDownloader {
private final Context context;
private final Distribution distribution;
public ImageDownloader(Context context, Distribution distribution) {
super(context);
this.context = context;
this.distribution = distribution;
}
private Density getDensity(float factor) {
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
final float densityDpi = dm.densityDpi * factor;
if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) {
return Density.XXHDPI;
} else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) {
return Density.XHDPI;
} else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) {
return Density.HDPI;
}
// Fallback to mdpi, no need to handle ldpi.
return Density.MDPI;
}
@Override
public Response load(Uri uri, boolean localCacheOnly) throws IOException {
final String scheme = uri.getScheme();
if (DISTRIBUTION_SCHEME.equals(scheme)) {
return loadDistributionImage(uri);
}
return super.load(uri, localCacheOnly);
}
private static String getPathForDensity(String basePath, Density density,
String filename) {
final File dir = new File(basePath, density.toString());
return String.format("%s/%s.png", dir.toString(), filename);
}
/**
* Handle distribution URIs in Picasso. The expected format is:
*
* gecko.distribution://<basepath>/<imagename>
*
* Which will look for the following file in the distribution:
*
* <distribution-root-dir>/<basepath>/<device-density>/<imagename>.png
*/
private Response loadDistributionImage(Uri uri) throws IOException {
// Eliminate the leading '//'
final String ssp = uri.getSchemeSpecificPart().substring(2);
final String filename;
final String basePath;
final int slashIndex = ssp.lastIndexOf('/');
if (slashIndex == -1) {
filename = ssp;
basePath = "";
} else {
filename = ssp.substring(slashIndex + 1);
basePath = ssp.substring(0, slashIndex);
}
Set<Density> triedDensities = EnumSet.noneOf(Density.class);
for (int i = 0; i < densityFactors.length; i++) {
final Density density = getDensity(densityFactors[i]);
if (!triedDensities.add(density)) {
continue;
}
final String path = getPathForDensity(basePath, density, filename);
Log.d(LOGTAG, "Trying to load image from distribution " + path);
final File f = distribution.getDistributionFile(path);
if (f != null) {
return new Response(new FileInputStream(f), true);
}
}
throw new ResponseException("Couldn't find suggested site image in distribution");
}
}
}

View File

@ -56,7 +56,7 @@ class PanelAuthLayout extends LinearLayout {
// Use a default image if an image URL isn't specified. // Use a default image if an image URL isn't specified.
imageView.setImageResource(R.drawable.icon_home_empty_firefox); imageView.setImageResource(R.drawable.icon_home_empty_firefox);
} else { } else {
Picasso.with(getContext()) ImageLoader.with(getContext())
.load(imageUrl) .load(imageUrl)
.into(imageView); .into(imageView);
} }

View File

@ -33,7 +33,7 @@ class PanelBackItemView extends LinearLayout {
if (TextUtils.isEmpty(backImageUrl)) { if (TextUtils.isEmpty(backImageUrl)) {
image.setImageResource(R.drawable.folder_up); image.setImageResource(R.drawable.folder_up);
} else { } else {
Picasso.with(getContext()) ImageLoader.with(getContext())
.load(backImageUrl) .load(backImageUrl)
.placeholder(R.drawable.folder_up) .placeholder(R.drawable.folder_up)
.into(image); .into(image);

View File

@ -68,7 +68,7 @@ class PanelItemView extends LinearLayout {
image.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE); image.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
if (hasImageUrl) { if (hasImageUrl) {
Picasso.with(getContext()) ImageLoader.with(getContext())
.load(imageUrl) .load(imageUrl)
.into(image); .into(image);
} }

View File

@ -460,7 +460,7 @@ abstract class PanelLayout extends FrameLayout {
if (TextUtils.isEmpty(imageUrl)) { if (TextUtils.isEmpty(imageUrl)) {
imageView.setImageResource(R.drawable.icon_home_empty_firefox); imageView.setImageResource(R.drawable.icon_home_empty_firefox);
} else { } else {
Picasso.with(getContext()) ImageLoader.with(getContext())
.load(imageUrl) .load(imageUrl)
.error(R.drawable.icon_home_empty_firefox) .error(R.drawable.icon_home_empty_firefox)
.into(imageView); .into(imageView);

View File

@ -149,7 +149,7 @@ public class TopSitesGridItemView extends RelativeLayout {
updateType(TopSites.TYPE_BLANK); updateType(TopSites.TYPE_BLANK);
updateTitleView(); updateTitleView();
setLoadId(Favicons.NOT_LOADING); setLoadId(Favicons.NOT_LOADING);
Picasso.with(getContext()).cancelRequest(mThumbnailView); ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
displayThumbnail(R.drawable.top_site_add); displayThumbnail(R.drawable.top_site_add);
} }
@ -192,7 +192,7 @@ public class TopSitesGridItemView extends RelativeLayout {
if (changed) { if (changed) {
updateTitleView(); updateTitleView();
setLoadId(Favicons.NOT_LOADING); setLoadId(Favicons.NOT_LOADING);
Picasso.with(getContext()).cancelRequest(mThumbnailView); ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
} }
if (updateType(type)) { if (updateType(type)) {
@ -233,7 +233,7 @@ public class TopSitesGridItemView extends RelativeLayout {
} }
mThumbnailSet = true; mThumbnailSet = true;
Favicons.cancelFaviconLoad(mLoadId); Favicons.cancelFaviconLoad(mLoadId);
Picasso.with(getContext()).cancelRequest(mThumbnailView); ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL); mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL);
mThumbnailView.setImageBitmap(thumbnail); mThumbnailView.setImageBitmap(thumbnail);
@ -251,7 +251,7 @@ public class TopSitesGridItemView extends RelativeLayout {
mThumbnailView.setBackgroundColor(bgColor); mThumbnailView.setBackgroundColor(bgColor);
mThumbnailSet = true; mThumbnailSet = true;
Picasso.with(getContext()) ImageLoader.with(getContext())
.load(imageUrl) .load(imageUrl)
.noFade() .noFade()
.error(R.drawable.favicon) .error(R.drawable.favicon)

View File

@ -278,6 +278,7 @@ gbjar.sources += [
'home/HomePagerTabStrip.java', 'home/HomePagerTabStrip.java',
'home/HomePanelPicker.java', 'home/HomePanelPicker.java',
'home/HomePanelsManager.java', 'home/HomePanelsManager.java',
'home/ImageLoader.java',
'home/MultiTypeCursorAdapter.java', 'home/MultiTypeCursorAdapter.java',
'home/PanelAuthCache.java', 'home/PanelAuthCache.java',
'home/PanelAuthLayout.java', 'home/PanelAuthLayout.java',

View File

@ -13,6 +13,7 @@ jar.sources += [
'src/tests/BrowserTestCase.java', 'src/tests/BrowserTestCase.java',
'src/tests/TestDistribution.java', 'src/tests/TestDistribution.java',
'src/tests/TestGeckoSharedPrefs.java', 'src/tests/TestGeckoSharedPrefs.java',
'src/tests/TestImageDownloader.java',
'src/tests/TestJarReader.java', 'src/tests/TestJarReader.java',
'src/tests/TestRawResource.java', 'src/tests/TestRawResource.java',
'src/tests/TestSuggestedSites.java', 'src/tests/TestSuggestedSites.java',

View File

@ -0,0 +1,205 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.browser.tests;
import android.content.Context;
import android.content.res.Resources;
import android.content.SharedPreferences;
import android.net.Uri;
import android.test.mock.MockResources;
import android.test.RenamingDelegatingContext;
import android.util.DisplayMetrics;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.home.ImageLoader.ImageDownloader;
public class TestImageDownloader extends BrowserTestCase {
private static class TestContext extends RenamingDelegatingContext {
private static final String PREFIX = "TestImageDownloader-";
private final Resources resources;
private final Set<String> usedPrefs;
public TestContext(Context context) {
super(context, PREFIX);
resources = new TestResources();
usedPrefs = Collections.synchronizedSet(new HashSet<String>());
}
@Override
public Resources getResources() {
return resources;
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
usedPrefs.add(name);
return super.getSharedPreferences(PREFIX + name, mode);
}
public void clearUsedPrefs() {
for (String prefsName : usedPrefs) {
getSharedPreferences(prefsName, 0).edit().clear().commit();
}
usedPrefs.clear();
}
}
private static class TestResources extends MockResources {
private final DisplayMetrics metrics;
public TestResources() {
metrics = new DisplayMetrics();
}
@Override
public DisplayMetrics getDisplayMetrics() {
return metrics;
}
public void setDensityDpi(int densityDpi) {
metrics.densityDpi = densityDpi;
}
}
private static class TestDistribution extends Distribution {
final List<String> accessedFiles;
public TestDistribution(Context context) {
super(context);
accessedFiles = new ArrayList<String>();
}
@Override
public File getDistributionFile(String name) {
accessedFiles.add(name);
// Return null to ensure the ImageDownloader will go
// through a complete density lookup for each filename.
return null;
}
public List<String> getAccessedFiles() {
return Collections.unmodifiableList(accessedFiles);
}
public void resetAccessedFiles() {
accessedFiles.clear();
}
}
private TestContext context;
private TestResources resources;
private TestDistribution distribution;
private ImageDownloader downloader;
protected void setUp() {
context = new TestContext(getApplicationContext());
resources = (TestResources) context.getResources();
distribution = new TestDistribution(context);
downloader = new ImageDownloader(context, distribution);
}
protected void tearDown() {
context.clearUsedPrefs();
}
private void triggerLoad(Uri uri) {
try {
downloader.load(uri, false);
} catch (IOException e) {
// Ignore any IO exceptions.
}
}
private void checkAccessedFiles(String[] filenames) {
List<String> accessedFiles = distribution.getAccessedFiles();
for (int i = 0; i < filenames.length; i++) {
assertEquals(filenames[i], accessedFiles.get(i));
}
}
private void checkAccessedFilesForUri(Uri uri, int densityDpi, String[] filenames) {
resources.setDensityDpi(densityDpi);
triggerLoad(uri);
checkAccessedFiles(filenames);
distribution.resetAccessedFiles();
}
public void testAccessedFiles() {
// Filename only.
checkAccessedFilesForUri(Uri.parse("gecko.distribution://file"),
DisplayMetrics.DENSITY_MEDIUM,
new String[] {
"mdpi/file.png",
"xhdpi/file.png",
"hdpi/file.png"
});
// Directory and filename.
checkAccessedFilesForUri(Uri.parse("gecko.distribution://dir/file"),
DisplayMetrics.DENSITY_MEDIUM,
new String[] {
"dir/mdpi/file.png",
"dir/xhdpi/file.png",
"dir/hdpi/file.png"
});
// Sub-directories and filename.
checkAccessedFilesForUri(Uri.parse("gecko.distribution://dir/subdir/file"),
DisplayMetrics.DENSITY_MEDIUM,
new String[] {
"dir/subdir/mdpi/file.png",
"dir/subdir/xhdpi/file.png",
"dir/subdir/hdpi/file.png"
});
}
public void testDensityLookup() {
Uri uri = Uri.parse("gecko.distribution://file");
// Medium density
checkAccessedFilesForUri(uri,
DisplayMetrics.DENSITY_MEDIUM,
new String[] {
"mdpi/file.png",
"xhdpi/file.png",
"hdpi/file.png"
});
checkAccessedFilesForUri(uri,
DisplayMetrics.DENSITY_HIGH,
new String[] {
"hdpi/file.png",
"xxhdpi/file.png",
"xhdpi/file.png"
});
checkAccessedFilesForUri(uri,
DisplayMetrics.DENSITY_XHIGH,
new String[] {
"xhdpi/file.png",
"xxhdpi/file.png",
"mdpi/file.png"
});
checkAccessedFilesForUri(uri,
DisplayMetrics.DENSITY_XXHIGH,
new String[] {
"xxhdpi/file.png",
"hdpi/file.png"
});
}
}