mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-14 05:45:37 +00:00
Bug 1012462 - Part 11: Support image loading for distribution files (r=rnewman)
This commit is contained in:
parent
92588f6998
commit
0b216e16ae
154
mobile/android/base/home/ImageLoader.java
Normal file
154
mobile/android/base/home/ImageLoader.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -56,9 +56,9 @@ class PanelAuthLayout extends LinearLayout {
|
||||
// Use a default image if an image URL isn't specified.
|
||||
imageView.setImageResource(R.drawable.icon_home_empty_firefox);
|
||||
} else {
|
||||
Picasso.with(getContext())
|
||||
.load(imageUrl)
|
||||
.into(imageView);
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.into(imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,10 +33,10 @@ class PanelBackItemView extends LinearLayout {
|
||||
if (TextUtils.isEmpty(backImageUrl)) {
|
||||
image.setImageResource(R.drawable.folder_up);
|
||||
} else {
|
||||
Picasso.with(getContext())
|
||||
.load(backImageUrl)
|
||||
.placeholder(R.drawable.folder_up)
|
||||
.into(image);
|
||||
ImageLoader.with(getContext())
|
||||
.load(backImageUrl)
|
||||
.placeholder(R.drawable.folder_up)
|
||||
.into(image);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,9 +68,9 @@ class PanelItemView extends LinearLayout {
|
||||
image.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (hasImageUrl) {
|
||||
Picasso.with(getContext())
|
||||
.load(imageUrl)
|
||||
.into(image);
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.into(image);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -460,10 +460,10 @@ abstract class PanelLayout extends FrameLayout {
|
||||
if (TextUtils.isEmpty(imageUrl)) {
|
||||
imageView.setImageResource(R.drawable.icon_home_empty_firefox);
|
||||
} else {
|
||||
Picasso.with(getContext())
|
||||
.load(imageUrl)
|
||||
.error(R.drawable.icon_home_empty_firefox)
|
||||
.into(imageView);
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.error(R.drawable.icon_home_empty_firefox)
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
viewState.setEmptyView(view);
|
||||
|
@ -149,7 +149,7 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||
updateType(TopSites.TYPE_BLANK);
|
||||
updateTitleView();
|
||||
setLoadId(Favicons.NOT_LOADING);
|
||||
Picasso.with(getContext()).cancelRequest(mThumbnailView);
|
||||
ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
|
||||
displayThumbnail(R.drawable.top_site_add);
|
||||
|
||||
}
|
||||
@ -192,7 +192,7 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||
if (changed) {
|
||||
updateTitleView();
|
||||
setLoadId(Favicons.NOT_LOADING);
|
||||
Picasso.with(getContext()).cancelRequest(mThumbnailView);
|
||||
ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
|
||||
}
|
||||
|
||||
if (updateType(type)) {
|
||||
@ -233,7 +233,7 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||
}
|
||||
mThumbnailSet = true;
|
||||
Favicons.cancelFaviconLoad(mLoadId);
|
||||
Picasso.with(getContext()).cancelRequest(mThumbnailView);
|
||||
ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
|
||||
|
||||
mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL);
|
||||
mThumbnailView.setImageBitmap(thumbnail);
|
||||
@ -251,11 +251,11 @@ public class TopSitesGridItemView extends RelativeLayout {
|
||||
mThumbnailView.setBackgroundColor(bgColor);
|
||||
mThumbnailSet = true;
|
||||
|
||||
Picasso.with(getContext())
|
||||
.load(imageUrl)
|
||||
.noFade()
|
||||
.error(R.drawable.favicon)
|
||||
.into(mThumbnailView);
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.noFade()
|
||||
.error(R.drawable.favicon)
|
||||
.into(mThumbnailView);
|
||||
}
|
||||
|
||||
public void displayFavicon(Bitmap favicon, String faviconURL, int expectedLoadId) {
|
||||
|
@ -278,6 +278,7 @@ gbjar.sources += [
|
||||
'home/HomePagerTabStrip.java',
|
||||
'home/HomePanelPicker.java',
|
||||
'home/HomePanelsManager.java',
|
||||
'home/ImageLoader.java',
|
||||
'home/MultiTypeCursorAdapter.java',
|
||||
'home/PanelAuthCache.java',
|
||||
'home/PanelAuthLayout.java',
|
||||
|
@ -13,6 +13,7 @@ jar.sources += [
|
||||
'src/tests/BrowserTestCase.java',
|
||||
'src/tests/TestDistribution.java',
|
||||
'src/tests/TestGeckoSharedPrefs.java',
|
||||
'src/tests/TestImageDownloader.java',
|
||||
'src/tests/TestJarReader.java',
|
||||
'src/tests/TestRawResource.java',
|
||||
'src/tests/TestSuggestedSites.java',
|
||||
|
@ -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"
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user