Merge pull request #942 from dosiecki/master

Android: Story Thumbnails
This commit is contained in:
Samuel Clay 2016-08-22 13:17:37 -07:00 committed by GitHub
commit 45154edbbe
35 changed files with 467 additions and 346 deletions

View file

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur"
android:versionCode="129"
android:versionName="4.10.0b2" >
android:versionName="4.10.0b3" >
<uses-sdk
android:minSdkVersion="16"

View file

@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
@ -17,7 +17,7 @@ apply plugin: 'com.android.application'
dependencies {
compile 'com.android.support:support-v13:19.1.0'
compile 'com.jakewharton:butterknife:7.0.1'
compile 'com.squareup.okhttp3:okhttp:3.2.0'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.google.code.gson:gson:2.6.2'
}

Binary file not shown.

Binary file not shown.

View file

@ -53,13 +53,23 @@
android:singleLine="true"
style="?storyFeedTitleText" />
<ImageView
android:id="@+id/row_item_thumbnail"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginTop="1dp"
android:layout_marginBottom="1dp"
android:layout_alignParentRight="true"
android:visibility="gone" />
<ImageView
android:id="@+id/row_item_saved_icon"
android:src="@drawable/clock"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_below="@id/row_item_feedtitle"
android:layout_alignParentRight="true"
android:layout_toLeftOf="@id/row_item_thumbnail"
android:layout_alignWithParentIfMissing="true"
android:layout_marginTop="2dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="8dp"
@ -83,6 +93,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/row_item_inteldot"
android:layout_toLeftOf="@id/row_item_thumbnail"
android:layout_below="@id/row_item_title"
android:paddingBottom="4dp"
android:paddingRight="4dp"
@ -94,7 +105,8 @@
android:id="@+id/row_item_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_toLeftOf="@id/row_item_thumbnail"
android:layout_alignWithParentIfMissing="true"
android:layout_below="@id/row_item_content"
android:paddingBottom="4dp"
android:paddingRight="8dp"

View file

@ -240,6 +240,7 @@
<string name="settings_reading">Reading</string>
<string name="settings_immersive_enter_single_tap">Immersive Mode Via Single Tap</string>
<string name="settings_show_content_preview">Show Content Preview</string>
<string name="settings_show_thumbnails">Show Image Preview Thumbnails</string>
<string name="story">Story</string>
<string name="text">Text</string>

View file

@ -60,6 +60,10 @@
android:defaultValue="true"
android:key="pref_show_content_preview"
android:title="@string/settings_show_content_preview" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pref_show_thumbnails"
android:title="@string/settings_show_thumbnails" />
</PreferenceCategory>
<PreferenceCategory

View file

@ -64,7 +64,8 @@ public class InitActivity extends Activity {
boolean upgrade = PrefsUtils.checkForUpgrade(this);
if (upgrade) {
FeedUtils.dbHelper.dropAndRecreateTables();
PrefsUtils.updateVersion(this);
// don't actually unset the upgrade flag, the sync service will do this same check and
// update everything
}
}

View file

@ -93,7 +93,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
Bitmap userPicture = PrefsUtils.getUserImage(this);
if (userPicture != null) {
userPicture = UIUtils.roundCorners(userPicture, 5);
userPicture = UIUtils.clipAndRound(userPicture, 5, false);
userImage.setImageBitmap(userPicture);
}
userName.setText(PrefsUtils.getUserDetails(this).username);

View file

@ -288,6 +288,19 @@ public class BlurDatabaseHelper {
return urls;
}
public Set<String> getAllStoryThumbnails() {
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_THUMBNAIL_URL}, null, null, null, null, null);
Set<String> urls = new HashSet<String>(c.getCount());
while (c.moveToNext()) {
String url = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.STORY_THUMBNAIL_URL));
if (url != null) {
urls.add(url);
}
}
c.close();
return urls;
}
public void insertStories(StoriesResponse apiResponse, boolean forImmediateReading) {
StateFilter intelState = PrefsUtils.getStateFilter(context);
synchronized (RW_MUTEX) {
@ -322,8 +335,10 @@ public class BlurDatabaseHelper {
// handle story content
List<ContentValues> socialStoryValues = new ArrayList<ContentValues>();
for (Story story : apiResponse.stories) {
// pick a thumbnail for the story
story.thumbnailUrl = Story.guessStoryThumbnailURL(story);
// insert the story data
ContentValues values = story.getValues();
// immediate insert the story data
dbRW.insertWithOnConflict(DatabaseConstants.STORY_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
// if a story was shared by a user, also insert it into the social table under their userid, too
for (String sharedUserId : story.sharedUserIds) {

View file

@ -95,6 +95,7 @@ public class DatabaseConstants {
public static final String STORY_IMAGE_URLS = "image_urls";
public static final String STORY_LAST_READ_DATE = "last_read_date";
public static final String STORY_SEARCH_HIT = "search_hit";
public static final String STORY_THUMBNAIL_URL = "thumbnail_url";
public static final String READING_SESSION_TABLE = "reading_session";
public static final String READING_SESSION_STORY_HASH = "session_story_hash";
@ -240,7 +241,8 @@ public class DatabaseConstants {
STORY_TITLE + TEXT + ", " +
STORY_IMAGE_URLS + TEXT + ", " +
STORY_LAST_READ_DATE + INTEGER + ", " +
STORY_SEARCH_HIT + TEXT +
STORY_SEARCH_HIT + TEXT + ", " +
STORY_THUMBNAIL_URL + TEXT +
")";
static final String READING_SESSION_SQL = "CREATE TABLE " + READING_SESSION_TABLE + " (" +
@ -308,7 +310,7 @@ public class DatabaseConstants {
STORY_INTELLIGENCE_AUTHORS, STORY_INTELLIGENCE_FEED, STORY_INTELLIGENCE_TAGS, STORY_INTELLIGENCE_TOTAL,
STORY_INTELLIGENCE_TITLE, STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_STARRED_DATE, STORY_TAGS, STORY_USER_TAGS, STORY_TITLE,
STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_HASH,
STORY_LAST_READ_DATE,
STORY_LAST_READ_DATE, STORY_THUMBNAIL_URL,
};
private static final String STORY_COLUMNS =

View file

@ -207,7 +207,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
TextView nameView = ((TextView) v.findViewById(R.id.row_socialfeed_name));
nameView.setText(f.feedTitle);
nameView.setTextSize(textSize * defaultTextSize_childName);
FeedUtils.imageLoader.displayImage(f.photoUrl, ((ImageView) v.findViewById(R.id.row_socialfeed_icon)), false);
FeedUtils.iconLoader.displayImage(f.photoUrl, ((ImageView) v.findViewById(R.id.row_socialfeed_icon)), 0, false);
TextView neutCounter = ((TextView) v.findViewById(R.id.row_socialsumneu));
if (f.neutralCount > 0 && currentState != StateFilter.BEST) {
neutCounter.setVisibility(View.VISIBLE);
@ -239,7 +239,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
TextView nameView =((TextView) v.findViewById(R.id.row_feedname));
nameView.setText(f.title);
nameView.setTextSize(textSize * defaultTextSize_childName);
FeedUtils.imageLoader.displayImage(f.faviconUrl, ((ImageView) v.findViewById(R.id.row_feedfavicon)), false);
FeedUtils.iconLoader.displayImage(f.faviconUrl, ((ImageView) v.findViewById(R.id.row_feedfavicon)), 0, false);
TextView neutCounter = ((TextView) v.findViewById(R.id.row_feedneutral));
TextView posCounter = ((TextView) v.findViewById(R.id.row_feedpositive));
TextView savedCounter = ((TextView) v.findViewById(R.id.row_feedsaved));

View file

@ -107,7 +107,7 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
// lists with mixed feeds get added info, but single feeds do not
if (!singleFeed) {
String faviconUrl = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_URL));
FeedUtils.imageLoader.displayImage(faviconUrl, ((ImageView) v.findViewById(R.id.row_item_feedicon)), false);
FeedUtils.iconLoader.displayImage(faviconUrl, ((ImageView) v.findViewById(R.id.row_item_feedicon)), 0, false);
((TextView) v.findViewById(R.id.row_item_feedtitle)).setText(cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE)));
} else {
v.findViewById(R.id.row_item_feedicon).setVisibility(View.GONE);
@ -147,6 +147,7 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
((ImageView) v.findViewById(R.id.row_item_feedicon)).setImageAlpha(255);
((ImageView) v.findViewById(R.id.row_item_inteldot)).setImageAlpha(255);
((ImageView) v.findViewById(R.id.row_item_thumbnail)).setImageAlpha(255);
borderOne.getBackground().setAlpha(255);
borderTwo.getBackground().setAlpha(255);
} else {
@ -160,6 +161,7 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
((ImageView) v.findViewById(R.id.row_item_feedicon)).setImageAlpha(READ_STORY_ALPHA_B255);
((ImageView) v.findViewById(R.id.row_item_inteldot)).setImageAlpha(READ_STORY_ALPHA_B255);
((ImageView) v.findViewById(R.id.row_item_thumbnail)).setImageAlpha(READ_STORY_ALPHA_B255);
borderOne.getBackground().setAlpha(READ_STORY_ALPHA_B255);
borderTwo.getBackground().setAlpha(READ_STORY_ALPHA_B255);
}
@ -173,6 +175,19 @@ public class StoryItemsAdapter extends SimpleCursorAdapter {
if (!PrefsUtils.isShowContentPreviews(context)) {
itemContent.setVisibility(View.GONE);
}
ImageView thumbnailView = ((ImageView) v.findViewById(R.id.row_item_thumbnail));
if (PrefsUtils.isShowThumbnails(context)) {
if (story.thumbnailUrl != null ) {
thumbnailView.setVisibility(View.VISIBLE);
FeedUtils.thumbnailLoader.displayImage(story.thumbnailUrl, thumbnailView, 0, true);
} else {
thumbnailView.setVisibility(View.INVISIBLE);
}
} else {
thumbnailView.setVisibility(View.GONE);
}
}
class StoryItemViewBinder implements ViewBinder {

View file

@ -1,6 +1,8 @@
package com.newsblur.domain;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.ContentValues;
import android.database.Cursor;
@ -57,7 +59,7 @@ public class Story implements Serializable {
@SerializedName("story_content")
public String content;
// this isn't actually a serialized feed, but created client-size in StoryTypeAdapter
// this isn't actually a serialized field, but created client-size in StoryTypeAdapter
public String shortContent;
@SerializedName("story_authors")
@ -91,6 +93,9 @@ public class Story implements Serializable {
// non-API and only set once when story is pushed to DB so it can be selected upon
public String searchHit = "";
// non-API, though it probably could/should be. populated on first story ingest if thumbnails are turned on
public String thumbnailUrl;
public ContentValues getValues() {
final ContentValues values = new ContentValues();
@ -120,6 +125,7 @@ public class Story implements Serializable {
values.put(DatabaseConstants.STORY_IMAGE_URLS, TextUtils.join(",", imageUrls));
values.put(DatabaseConstants.STORY_LAST_READ_DATE, lastReadTimestamp);
values.put(DatabaseConstants.STORY_SEARCH_HIT, searchHit);
values.put(DatabaseConstants.STORY_THUMBNAIL_URL, thumbnailUrl);
return values;
}
@ -150,6 +156,7 @@ public class Story implements Serializable {
story.id = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_ID));
story.storyHash = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_HASH));
story.lastReadTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_LAST_READ_DATE));
story.thumbnailUrl = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_THUMBNAIL_URL));
return story;
}
@ -227,4 +234,42 @@ public class Story implements Serializable {
return result;
}
private static final Pattern ytSniff1 = Pattern.compile("youtube\\.com\\/embed\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff2 = Pattern.compile("youtube\\.com\\/v\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff3 = Pattern.compile("ytimg\\.com\\/vi\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff4 = Pattern.compile("youtube\\.com\\/watch\\?v=([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final String YT_THUMB_PRE = "http://img.youtube.com/vi/";
private static final String YT_THUMB_POST = "/0.jpg";
public static String guessStoryThumbnailURL(Story story) {
String content = story.content;
String ytUrl = null;
if (ytUrl == null) {
Matcher m = ytSniff1.matcher(content);
if (m.find()) ytUrl = m.group(1);
}
if (ytUrl == null) {
Matcher m = ytSniff2.matcher(content);
if (m.find()) ytUrl = m.group(1);
}
if (ytUrl == null) {
Matcher m = ytSniff3.matcher(content);
if (m.find()) ytUrl = m.group(1);
}
if (ytUrl == null) {
Matcher m = ytSniff4.matcher(content);
if (m.find()) ytUrl = m.group(1);
}
if (ytUrl != null) {
return YT_THUMB_PRE + ytUrl + YT_THUMB_POST;
}
if ((story.imageUrls != null) && (story.imageUrls.length > 0)) {
return story.imageUrls[0];
}
return null;
}
}

View file

@ -95,7 +95,7 @@ public class LoginProgressFragment extends Fragment {
updateStatus.startAnimation(a);
loginProfilePicture.setVisibility(View.VISIBLE);
loginProfilePicture.setImageBitmap(UIUtils.roundCorners(PrefsUtils.getUserImage(c), 10f));
loginProfilePicture.setImageBitmap(UIUtils.clipAndRound(PrefsUtils.getUserImage(c), 10f, false));
feedProgress.setVisibility(View.VISIBLE);
final Animation b = AnimationUtils.loadAnimation(c, R.anim.text_up);

View file

@ -99,7 +99,7 @@ public class ProfileDetailsFragment extends Fragment implements OnClickListener
followingCount.setText("" + user.followingCount);
if (!viewingSelf) {
FeedUtils.imageLoader.displayImage(user.photoUrl, imageView);
FeedUtils.iconLoader.displayImage(user.photoUrl, imageView, 5, false);
if (user.followedByYou) {
unfollowButton.setVisibility(View.VISIBLE);
followButton.setVisibility(View.GONE);
@ -113,7 +113,7 @@ public class ProfileDetailsFragment extends Fragment implements OnClickListener
// seems to sometimes be an error loading the picture so prevent
// force close if null returned
if (userPicture != null) {
userPicture = UIUtils.roundCorners(userPicture, 5);
userPicture = UIUtils.clipAndRound(userPicture, 5, false);
imageView.setImageBitmap(userPicture);
}
}

View file

@ -43,7 +43,7 @@ import com.newsblur.domain.UserDetails;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.ImageCache;
import com.newsblur.util.FileCache;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StoryUtils;
import com.newsblur.util.UIUtils;
@ -326,7 +326,7 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
itemFeed.setVisibility(View.GONE);
feedIcon.setVisibility(View.GONE);
} else {
FeedUtils.imageLoader.displayImage(feedIconUrl, feedIcon, false);
FeedUtils.iconLoader.displayImage(feedIconUrl, feedIcon, 0, false);
itemFeed.setText(feedTitle);
}
@ -597,12 +597,10 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
}
private String swapInOfflineImages(String html) {
ImageCache cache = new ImageCache(getActivity());
Matcher imageTagMatcher = Pattern.compile("<img[^>]*(src\\s*=\\s*)\"([^\"]*)\"[^>]*>", Pattern.CASE_INSENSITIVE).matcher(html);
while (imageTagMatcher.find()) {
String url = imageTagMatcher.group(2);
String localPath = cache.getCachedLocation(url);
String localPath = FeedUtils.storyImageCache.getCachedLocation(url);
if (localPath == null) continue;
html = html.replace(imageTagMatcher.group(1)+"\""+url+"\"", "src=\""+localPath+"\"");
imageUrlRemaps.put(localPath, url);

View file

@ -119,7 +119,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
ImageView favouriteImage = new ImageView(context);
UserProfile user = FeedUtils.dbHelper.getUserProfile(id);
if (user != null) {
FeedUtils.imageLoader.displayImage(user.photoUrl, favouriteImage, 10f);
FeedUtils.iconLoader.displayImage(user.photoUrl, favouriteImage, 10f, false);
favouriteContainer.addView(favouriteImage);
}
}
@ -163,7 +163,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
final UserProfile replyUser = FeedUtils.dbHelper.getUserProfile(reply.userId);
if (replyUser != null) {
FeedUtils.imageLoader.displayImage(replyUser.photoUrl, replyImage);
FeedUtils.iconLoader.displayImage(replyUser.photoUrl, replyImage, 5, false);
replyImage.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
@ -210,11 +210,11 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
UserProfile sourceUser = FeedUtils.dbHelper.getUserProfile(comment.sourceUserId);
if (sourceUser != null) {
FeedUtils.imageLoader.displayImage(sourceUser.photoUrl, sourceUserImage, 10f);
FeedUtils.imageLoader.displayImage(userPhoto, usershareImage, 10f);
FeedUtils.iconLoader.displayImage(sourceUser.photoUrl, sourceUserImage, 10f, false);
FeedUtils.iconLoader.displayImage(userPhoto, usershareImage, 10f, false);
}
} else {
FeedUtils.imageLoader.displayImage(userPhoto, commentImage, 10f);
FeedUtils.iconLoader.displayImage(userPhoto, commentImage, 10f, false);
}
commentImage.setOnClickListener(new OnClickListener() {

View file

@ -4,7 +4,6 @@ import android.util.Log;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FileCache;
import com.newsblur.util.ImageCache;
import com.newsblur.util.PrefsUtils;
public class CleanupService extends SubService {
@ -30,13 +29,17 @@ public class CleanupService extends SubService {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up old story texts");
parent.dbHelper.cleanupStoryText();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up image cache");
ImageCache imageCache = new ImageCache(parent);
imageCache.cleanup(parent.dbHelper.getAllStoryImages());
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up story image cache");
FileCache imageCache = FileCache.asStoryImageCache(parent);
imageCache.cleanupUnusedOrOld(parent.dbHelper.getAllStoryImages());
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up icon cache");
FileCache iconCache = new FileCache(parent);
iconCache.cleanup();
FileCache iconCache = FileCache.asIconCache(parent);
iconCache.cleanupOld();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up thumbnail cache");
FileCache thumbCache = FileCache.asThumbnailCache(parent);
thumbCache.cleanupUnusedOrOld(parent.dbHelper.getAllStoryThumbnails());
PrefsUtils.updateLastCleanupTime(parent);
}

View file

@ -3,7 +3,7 @@ package com.newsblur.service;
import android.util.Log;
import com.newsblur.util.AppConstants;
import com.newsblur.util.ImageCache;
import com.newsblur.util.FileCache;
import com.newsblur.util.PrefsUtils;
import java.util.Collections;
@ -14,26 +14,30 @@ public class ImagePrefetchService extends SubService {
private static volatile boolean Running = false;
ImageCache imageCache;
FileCache storyImageCache;
FileCache thumbnailCache;
/** URLs of images contained in recently fetched stories that are candidates for prefetch. */
static Set<String> ImageQueue;
static { ImageQueue = Collections.synchronizedSet(new HashSet<String>()); }
static Set<String> StoryImageQueue;
static { StoryImageQueue = Collections.synchronizedSet(new HashSet<String>()); }
/** URLs of thumbnails for recently fetched stories that are candidates for prefetch. */
static Set<String> ThumbnailQueue;
static { ThumbnailQueue = Collections.synchronizedSet(new HashSet<String>()); }
public ImagePrefetchService(NBSyncService parent) {
super(parent);
imageCache = new ImageCache(parent);
storyImageCache = FileCache.asStoryImageCache(parent);
thumbnailCache = FileCache.asThumbnailCache(parent);
}
@Override
protected void exec() {
if (!PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (ImageQueue.size() < 1) return;
if (!PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
gotWork();
while (ImageQueue.size() > 0) {
while (StoryImageQueue.size() > 0) {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
@ -43,7 +47,7 @@ public class ImagePrefetchService extends SubService {
Set<String> unreadImages = parent.dbHelper.getAllStoryImages();
Set<String> fetchedImages = new HashSet<String>();
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
batchloop: for (String url : ImageQueue) {
batchloop: for (String url : StoryImageQueue) {
batch.add(url);
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
}
@ -53,12 +57,43 @@ public class ImagePrefetchService extends SubService {
// dont fetch the image if the associated story was marked read before we got to it
if (unreadImages.contains(url)) {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "prefetching image: " + url);
imageCache.cacheImage(url);
storyImageCache.cacheFile(url);
}
fetchedImages.add(url);
}
} finally {
ImageQueue.removeAll(fetchedImages);
StoryImageQueue.removeAll(fetchedImages);
gotWork();
}
}
while (ThumbnailQueue.size() > 0) {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
if (! PrefsUtils.isShowThumbnails(parent)) return;
startExpensiveCycle();
// on each batch, re-query the DB for images associated with yet-unread stories
// this is a bit expensive, but we are running totally async at a really low priority
Set<String> unreadImages = parent.dbHelper.getAllStoryThumbnails();
Set<String> fetchedImages = new HashSet<String>();
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
batchloop: for (String url : ThumbnailQueue) {
batch.add(url);
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
}
try {
for (String url : batch) {
if (parent.stopSync()) return;
// dont fetch the image if the associated story was marked read before we got to it
if (unreadImages.contains(url)) {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "prefetching thumbnail: " + url);
thumbnailCache.cacheFile(url);
}
fetchedImages.add(url);
}
} finally {
ThumbnailQueue.removeAll(fetchedImages);
gotWork();
}
}
@ -66,15 +101,20 @@ public class ImagePrefetchService extends SubService {
}
public void addUrl(String url) {
ImageQueue.add(url);
StoryImageQueue.add(url);
}
public static int getPendingCount() {
return ImageQueue.size();
public void addThumbnailUrl(String url) {
ThumbnailQueue.add(url);
}
public static int getPendingCount() {
return (StoryImageQueue.size() + ThumbnailQueue.size());
}
public static void clear() {
ImageQueue.clear();
StoryImageQueue.clear();
ThumbnailQueue.clear();
}
public static boolean running() {

View file

@ -269,13 +269,16 @@ public class NBSyncService extends Service {
if (upgraded) {
HousekeepingRunning = true;
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS | NbActivity.UPDATE_REBUILD);
// wipe the local DB
dbHelper.dropAndRecreateTables();
NbActivity.updateAllActivities(NbActivity.UPDATE_METADATA);
// wipe the local DB if this is a first background run. if this is a first foreground
// run, InitActivity will have wiped for us
if (NbActivity.getActiveActivityCount() < 1) {
dbHelper.dropAndRecreateTables();
}
// in case this is the first time we have run since moving the cache to the new location,
// blow away the old version entirely. This line can be removed some time well after
// v61+ is widely deployed
FileCache.cleanUpOldCache(this);
FileCache.cleanUpOldCache1(this);
FileCache.cleanUpOldCache2(this);
PrefsUtils.updateVersion(this);
}

View file

@ -142,6 +142,9 @@ public class UnreadsService extends SubService {
parent.imagePrefetchService.addUrl(url);
}
}
if (story.thumbnailUrl != null) {
parent.imagePrefetchService.addThumbnailUrl(story.thumbnailUrl);
}
DefaultFeedView mode = PrefsUtils.getDefaultFeedViewForFeed(parent, story.feedId);
if (mode == DefaultFeedView.TEXT) {
parent.originalTextService.addHash(story.storyHash);

View file

@ -31,14 +31,22 @@ public class FeedUtils {
// cannot be created lazily or via static init, they have to be created when
// the main app context is created and it offers a reference
public static BlurDatabaseHelper dbHelper;
public static ImageLoader imageLoader;
public static ImageLoader iconLoader;
public static ImageLoader thumbnailLoader;
public static FileCache storyImageCache;
public static void offerInitContext(Context context) {
if (dbHelper == null) {
dbHelper = new BlurDatabaseHelper(context.getApplicationContext());
}
if (imageLoader == null) {
imageLoader = new ImageLoader(context.getApplicationContext());
if (iconLoader == null) {
iconLoader = ImageLoader.asIconLoader(context.getApplicationContext());
}
if (thumbnailLoader == null) {
thumbnailLoader = ImageLoader.asThumbnailLoader(context.getApplicationContext());
}
if (storyImageCache == null) {
storyImageCache = FileCache.asStoryImageCache(context.getApplicationContext());
}
}

View file

@ -1,53 +1,155 @@
package com.newsblur.util;
import java.net.URL;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.Context;
import android.util.Log;
public class FileCache {
private static final long MAX_FILE_AGE_MILLIS = 30L * 24L * 60L * 60L * 1000L;
private static final long MIN_FREE_SPACE_BYTES = 250L * 1024L * 1024L;
private static final Pattern POSTFIX_PATTERN = Pattern.compile("(\\.[a-zA-Z0-9]+)[^\\.]*$");
private File cacheDir;
private final long maxFileAgeMillis;
private final int minValidCacheBytes;
public FileCache(Context context) {
cacheDir = context.getCacheDir();
private final File cacheDir;
private FileCache(Context context, String subdir, long maxFileAgeMillis, int minValidCacheBytes) {
this.maxFileAgeMillis = maxFileAgeMillis;
this.minValidCacheBytes = minValidCacheBytes;
cacheDir = new File(context.getCacheDir(), subdir);
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
}
public File getFile(String url){
if (url == null ) return null;
final String filename = String.valueOf(url.hashCode());
final File f = new File(cacheDir, filename);
return f;
}
public static FileCache asStoryImageCache(Context context) {
FileCache fc = new FileCache(context, "olimages", 30L * 24L * 60L * 60L * 1000L, 512);
return fc;
}
/**
* Clean up any very old cached icons in the current cache dir. This should be
* done periodically so that new favicons are picked up and ones from removed
* feeds don't clog up the system.
*/
public void cleanup() {
public static FileCache asIconCache(Context context) {
FileCache fc = new FileCache(context, "icons", 45L * 24L * 60L * 60L * 1000L, 128);
return fc;
}
public static FileCache asThumbnailCache(Context context) {
FileCache fc = new FileCache(context, "thumbs", 15L * 24L * 60L * 60L * 1000L, 256);
return fc;
}
public void cacheFile(String url) {
try {
// don't be evil and download if the user is low on storage
if (cacheDir.getFreeSpace() < MIN_FREE_SPACE_BYTES) {
Log.w(this.getClass().getName(), "device low on storage, not caching");
return;
}
String fileName = getFileName(url);
if (fileName == null) {
return;
}
File f = new File(cacheDir, fileName);
if (f.exists()) return;
long size = NetworkUtils.loadURL(new URL(url), f);
// images that are super-small tend to be errors or invisible. don't waste file handles on them
if (size < minValidCacheBytes) {
f.delete();
}
} catch (Exception e) {
// a huge number of things could go wrong fetching and storing an image. don't spam logs with them
}
}
public File getCachedFile(String url) {
try {
String fileName = getFileName(url);
if (fileName == null) {
return null;
}
return new File(cacheDir, fileName);
} catch (Exception e) {
Log.e(this.getClass().getName(), "cache error", e);
return null;
}
}
public String getCachedLocation(String url) {
try {
String fileName = getFileName(url);
if (fileName == null) {
return null;
}
File f = new File(cacheDir, fileName);
if (f.exists()) {
return f.getAbsolutePath();
} else {
return null;
}
} catch (Exception e) {
Log.e(this.getClass().getName(), "cache error", e);
return null;
}
}
private String getFileName(String url) {
Matcher m = POSTFIX_PATTERN.matcher(url);
if (! m.find()) {
return null;
}
String fileName = Integer.toString(Math.abs(url.hashCode())) + m.group(1);
return fileName;
}
public void cleanupOld() {
try {
File[] files = cacheDir.listFiles();
if (files == null) return;
for (File f : files) {
long timestamp = f.lastModified();
if (System.currentTimeMillis() > (timestamp + MAX_FILE_AGE_MILLIS)) {
if (System.currentTimeMillis() > (timestamp + maxFileAgeMillis)) {
f.delete();
}
}
} catch (Exception e) {
android.util.Log.e(FileCache.class.getName(), "exception cleaning up icon cache", e);
android.util.Log.e(FileCache.class.getName(), "exception cleaning up cache", e);
}
}
public void cleanupUnusedOrOld(Set<String> currentUrls) {
// if there appear to be zero images in the system, a DB rebuild probably just
// occured, so don't trust that data for cleanup
if (currentUrls.size() == 0) return;
Set<String> currentFiles = new HashSet<String>(currentUrls.size());
for (String url : currentUrls) currentFiles.add(getFileName(url));
try {
File[] files = cacheDir.listFiles();
if (files == null) return;
for (File f : files) {
long timestamp = f.lastModified();
if ((System.currentTimeMillis() > (timestamp + maxFileAgeMillis)) ||
(!currentFiles.contains(f.getName()))) {
f.delete();
}
}
} catch (Exception e) {
android.util.Log.e(FileCache.class.getName(), "exception cleaning up cache", e);
}
}
/**
* Looks for and cleans up any remains of the old, mis-located legacy cache directory.
*/
public static void cleanUpOldCache(Context context) {
public static void cleanUpOldCache1(Context context) {
try {
File dir = new File(android.os.Environment.getExternalStorageDirectory(), "NewsblurCache");
if (!dir.exists()) return;
@ -61,4 +163,23 @@ public class FileCache {
android.util.Log.e(FileCache.class.getName(), "exception cleaning up legacy cache", e);
}
}
/**
* Looks for and cleans up any remains of the old caching system that used poor filenames.
*/
public static void cleanUpOldCache2(Context context) {
try {
File dir = context.getCacheDir();
File[] files = dir.listFiles();
if (files == null) return;
Pattern oldCachePattern = Pattern.compile("^[0-9-]+$");
for (File f : files) {
if ( (!f.isDirectory()) && (oldCachePattern.matcher(f.getName()).matches())) {
f.delete();
}
}
} catch (Exception e) {
android.util.Log.e(FileCache.class.getName(), "exception cleaning up legacy cache", e);
}
}
}

View file

@ -1,114 +0,0 @@
package com.newsblur.util;
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A utility to cache images for offline reading. Takes an image URL and turns it into
* a local file with a unique name that can be easily re-calculated in the future. Also
* supports automatic cleanup of old files.
*/
public class ImageCache {
private static final String CACHE_SUBDIR = "olimages";
private static final long MAX_FILE_AGE_MILLIS = 30L * 24L * 60L * 60L * 1000L;
private static final long MIN_FREE_SPACE_BYTES = 100L * 1024L * 1024L;
private static final int MIN_VALID_CACHE_BYTES = 64;
private File cacheDir;
private Pattern postfixPattern;
public ImageCache(Context context) {
cacheDir = new File(context.getCacheDir(), CACHE_SUBDIR);
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
postfixPattern = Pattern.compile("(\\.[a-zA-Z0-9]+)[^\\.]*$");
}
public void cacheImage(String url) {
try {
// don't be evil and download images if the user is low on storage
if (cacheDir.getFreeSpace() < MIN_FREE_SPACE_BYTES) {
Log.w(this.getClass().getName(), "device low on storage, not caching images");
return;
}
String fileName = getFileName(url);
if (fileName == null) {
Log.w(this.getClass().getName(), "failed to cache image: no file extension");
return;
}
File f = new File(cacheDir, fileName);
if (f.exists()) return;
long size = NetworkUtils.loadURL(new URL(url), f);
// images that are super-small tend to be errors or invisible. don't waste file handles on them
if (size < MIN_VALID_CACHE_BYTES) {
f.delete();
}
} catch (IOException e) {
// a huge number of things could go wrong fetching and storing an image. don't spam logs with them
}
}
/**
* Gets the cached location of the specified network image, if it has
* been cached. Fails fast and returns null if for any reason the image
* is not available.
*/
public String getCachedLocation(String url) {
try {
String fileName = getFileName(url);
if (fileName == null) {
return null;
}
File f = new File(cacheDir, fileName);
if (f.exists()) {
return f.getAbsolutePath();
} else {
return null;
}
} catch (Exception e) {
Log.e(this.getClass().getName(), "image cache error", e);
return null;
}
}
private String getFileName(String url) {
Matcher m = postfixPattern.matcher(url);
if (! m.find()) {
return null;
}
String fileName = Integer.toString(Math.abs(url.hashCode())) + m.group(1);
return fileName;
}
public void cleanup(Set<String> currentImages) {
// if there appear to be zero images in the system, a DB rebuild probably just
// occured, so don't trust that data for cleanup
if (currentImages.size() == 0) return;
Set<String> currentFiles = new HashSet<String>(currentImages.size());
for (String url : currentImages) currentFiles.add(getFileName(url));
File[] files = cacheDir.listFiles();
if (files == null) return;
for (File f : files) {
long timestamp = f.lastModified();
if ((System.currentTimeMillis() > (timestamp + MAX_FILE_AGE_MILLIS)) ||
(!currentFiles.contains(f.getName()))) {
f.delete();
}
}
}
}

View file

@ -14,6 +14,7 @@ import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Process;
import android.util.Log;
import android.widget.ImageView;
@ -22,108 +23,67 @@ import com.newsblur.network.APIConstants;
public class ImageLoader {
private final MemoryCache memoryCache = new MemoryCache();
private final MemoryCache memoryCache;
private final FileCache fileCache;
private final ExecutorService executorService;
private final Map<ImageView, String> imageViews = Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
private final int emptyRID;
private final int minImgHeight;
public ImageLoader(Context context) {
fileCache = new FileCache(context);
executorService = Executors.newFixedThreadPool(2);
}
public void displayImage(String url, ImageView imageView) {
displayImage(url, imageView, true);
// some image loads can happen after the imageview in question is already reused for some other image. keep
// track of what image each view wants so that when it comes time to load them, they aren't stale
private final Map<ImageView, String> imageViewMappings = Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
private ImageLoader(FileCache fileCache, int emptyRID, int minImgHeight, long memoryCacheSize) {
this.memoryCache = new MemoryCache(memoryCacheSize);
this.fileCache = fileCache;
executorService = Executors.newFixedThreadPool(3);
this.emptyRID = emptyRID;
this.minImgHeight = minImgHeight;
}
public Bitmap tryGetImage(String url) {
Bitmap bitmap = memoryCache.get(url);
if ((bitmap == null) && (url != null)) {
File f = fileCache.getFile(url);
bitmap = decodeBitmap(f);
}
return bitmap;
public static ImageLoader asIconLoader(Context context) {
return new ImageLoader(FileCache.asIconCache(context), R.drawable.world, 2, (Runtime.getRuntime().maxMemory()/20));
}
public static ImageLoader asThumbnailLoader(Context context) {
return new ImageLoader(FileCache.asThumbnailCache(context), android.R.color.transparent, 32, (Runtime.getRuntime().maxMemory()/5));
}
public void displayImage(String url, ImageView imageView, boolean doRound) {
imageViews.put(imageView, url);
Bitmap bitmap = tryGetImage(url);
public void displayImage(String url, ImageView imageView, float roundRadius, boolean cropSquare) {
if (url == null) {
imageView.setImageResource(emptyRID);
return;
}
if (url.startsWith("/")) {
url = APIConstants.NEWSBLUR_URL + url;
}
imageViewMappings.put(imageView, url);
PhotoToLoad photoToLoad = new PhotoToLoad(url, imageView, roundRadius, cropSquare);
// try from memory
Bitmap bitmap = memoryCache.get(url);
if (bitmap != null) {
if (doRound) {
bitmap = UIUtils.roundCorners(bitmap, 5);
}
imageView.setImageBitmap(bitmap);
setViewImage(bitmap, photoToLoad);
} else {
queuePhoto(url, imageView);
imageView.setImageResource(R.drawable.world);
// if not loaded, fetch and set in background
executorService.submit(new PhotosLoader(photoToLoad));
imageView.setImageResource(emptyRID);
}
}
public void displayImage(String url, ImageView imageView, float roundRadius) {
imageViews.put(imageView, url);
Bitmap bitmap = tryGetImage(url);
if (bitmap != null) {
if (roundRadius > 0) {
bitmap = UIUtils.roundCorners(bitmap, roundRadius);
}
imageView.setImageBitmap(bitmap);
} else {
queuePhoto(url, imageView);
imageView.setImageResource(R.drawable.world);
}
}
private void queuePhoto(String url, ImageView imageView) {
PhotoToLoad p = new PhotoToLoad(url, imageView);
executorService.submit(new PhotosLoader(p));
}
private Bitmap getBitmap(String url) {
if (url == null) return null;
File f = fileCache.getFile(url);
Bitmap bitmap = decodeBitmap(f);
if (bitmap != null) {
memoryCache.put(url, bitmap);
bitmap = UIUtils.roundCorners(bitmap, 5);
return bitmap;
}
FileInputStream fis = null;
try {
if (url.startsWith("/")) {
url = APIConstants.NEWSBLUR_URL + url;
}
long bytesRead = NetworkUtils.loadURL(new URL(url), f);
if (bytesRead == 0) return null;
fis = new FileInputStream(f);
bitmap = BitmapFactory.decodeStream(fis);
memoryCache.put(url, bitmap);
if (bitmap == null) return null;
bitmap = UIUtils.roundCorners(bitmap, 5);
return bitmap;
} catch (Exception e) {
Log.e(this.getClass().getName(), "Error loading image from network: " + url, e);
return null;
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// ignore
}
}
}
}
private class PhotoToLoad {
public String url;
public ImageView imageView;
public PhotoToLoad(final String u, final ImageView i) {
public float roundRadius;
public boolean cropSquare;
public PhotoToLoad(final String u, final ImageView i, float rr, boolean cs) {
url = u;
imageView = i;
roundRadius = rr;
cropSquare = cs;
}
}
@ -136,26 +96,30 @@ public class ImageLoader {
@Override
public void run() {
if (imageViewReused(photoToLoad)) {
return;
}
Bitmap bmp = getBitmap(photoToLoad.url);
memoryCache.put(photoToLoad.url, bmp);
if (imageViewReused(photoToLoad)) {
return;
}
BitmapDisplayer bitmapDisplayer = new BitmapDisplayer(bmp, photoToLoad);
Activity a = (Activity) photoToLoad.imageView.getContext();
a.runOnUiThread(bitmapDisplayer);
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE);
File f = fileCache.getCachedFile(photoToLoad.url);
Bitmap bitmap = decodeBitmap(f);
if (bitmap == null) {
fileCache.cacheFile(photoToLoad.url);
f = fileCache.getCachedFile(photoToLoad.url);
bitmap = decodeBitmap(f);
}
if (bitmap != null) {
memoryCache.put(photoToLoad.url, bitmap);
setViewImage(bitmap, photoToLoad);
}
}
}
private boolean imageViewReused(PhotoToLoad photoToLoad){
final String tag = imageViews.get(photoToLoad.imageView);
return (tag == null || !tag.equals(photoToLoad.url));
}
private void setViewImage(Bitmap bitmap, PhotoToLoad photoToLoad) {
if (bitmap == null) return;
if (bitmap.getHeight() < minImgHeight) return;
BitmapDisplayer bitmapDisplayer = new BitmapDisplayer(bitmap, photoToLoad);
Activity a = (Activity) photoToLoad.imageView.getContext();
a.runOnUiThread(bitmapDisplayer);
}
private class BitmapDisplayer implements Runnable {
Bitmap bitmap;
@ -166,12 +130,14 @@ public class ImageLoader {
photoToLoad = p;
}
public void run() {
if (imageViewReused(photoToLoad)) {
return;
} else if (bitmap != null) {
// ensure this imageview even still wants this image
String latestMappedUrl = imageViewMappings.get(photoToLoad.imageView);
if (latestMappedUrl == null || !latestMappedUrl.equals(photoToLoad.url)) return;
if (bitmap != null) {
bitmap = UIUtils.clipAndRound(bitmap, photoToLoad.roundRadius, photoToLoad.cropSquare);
photoToLoad.imageView.setImageBitmap(bitmap);
} else {
photoToLoad.imageView.setImageResource(R.drawable.world);
photoToLoad.imageView.setImageResource(emptyRID);
}
}
}

View file

@ -7,25 +7,18 @@ import java.util.Map;
import java.util.Map.Entry;
import android.graphics.Bitmap;
import android.util.Log;
public class MemoryCache {
private static final String TAG = "MemoryCache";
private Map<String, Bitmap> cache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));
private Map<String, Bitmap> cache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(20, 1.7f, true));
private long size = 0; //current allocated size
private long limit = 1000000; //max memory in bytes
private long limit; //max memory in bytes
public MemoryCache(){
setLimit(Runtime.getRuntime().maxMemory()/4);
public MemoryCache(long limitBytes) {
this.limit = limitBytes;
}
public void setLimit(final long new_limit){
limit = new_limit;
Log.i(TAG, "MemoryCache will use up to " + limit/1024./1024.+" MB");
}
public Bitmap get(final String id){
public Bitmap get(String id){
try {
if (cache == null || !cache.containsKey(id)) {
return null;
@ -38,7 +31,6 @@ public class MemoryCache {
}
public void put(String id, Bitmap bitmap) {
if (cache.containsKey(id)) {
size -= getSizeInBytes(cache.get(id));
}
@ -61,11 +53,7 @@ public class MemoryCache {
}
}
public void clear() {
cache.clear();
}
public long getSizeInBytes(Bitmap bitmap) {
private long getSizeInBytes(Bitmap bitmap) {
if (bitmap == null) {
return 0;
} else {

View file

@ -45,8 +45,12 @@ public class NetworkUtils {
Response response = ImageFetchHttpClient.newCall(requestBuilder.build()).execute();
if (response.isSuccessful()) {
BufferedSink sink = Okio.buffer(Okio.sink(file));
bytesRead = sink.writeAll(response.body().source());
sink.close();
try {
bytesRead = sink.writeAll(response.body().source());
} finally {
sink.close();
response.close();
}
}
} catch (Throwable t) {
// a huge number of things could go wrong fetching and storing an image. don't spam logs with them

View file

@ -52,6 +52,7 @@ public class PrefConstants {
public static final String STORIES_AUTO_OPEN_FIRST = "pref_auto_open_first_unread";
public static final String STORIES_SHOW_PREVIEWS = "pref_show_content_preview";
public static final String STORIES_SHOW_THUMBNAILS = "pref_show_thumbnails";
public static final String ENABLE_OFFLINE = "enable_offline";
public static final String ENABLE_IMAGE_PREFETCH = "enable_image_prefetch";

View file

@ -52,7 +52,7 @@ public class PrefsUtils {
String oldVersion = prefs.getString(AppConstants.LAST_APP_VERSION, null);
if ( (oldVersion == null) || (!oldVersion.equals(version)) ) {
Log.i(PrefsUtils.class.getName(), "detected new version of app");
Log.i(PrefsUtils.class.getName(), "detected new version of app:" + version);
return true;
}
return false;
@ -511,6 +511,11 @@ public class PrefsUtils {
return prefs.getBoolean(PrefConstants.STORIES_SHOW_PREVIEWS, true);
}
public static boolean isShowThumbnails(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return prefs.getBoolean(PrefConstants.STORIES_SHOW_THUMBNAILS, false);
}
public static boolean isAutoOpenFirstUnread(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return prefs.getBoolean(PrefConstants.STORIES_AUTO_OPEN_FIRST, false);

View file

@ -9,11 +9,14 @@ import android.app.ActionBar;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Shader;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
@ -32,33 +35,30 @@ public class UIUtils {
private UIUtils() {} // util class - no instances
/*
* Based on the RoundedCorners code from Square / Eric Burke's "Android UI" talk
* and the GitHub Android code.
* https://github.com/github/android
*/
public static Bitmap roundCorners(Bitmap source, final float radius) {
int width = source.getWidth();
int height = source.getHeight();
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(WHITE);
Bitmap clipped = Bitmap.createBitmap(width, height, ARGB_8888);
Canvas canvas = new Canvas(clipped);
canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(DST_IN));
Bitmap rounded = Bitmap.createBitmap(width, height, ARGB_8888);
canvas = new Canvas(rounded);
canvas.drawBitmap(source, 0, 0, null);
canvas.drawBitmap(clipped, 0, 0, paint);
clipped.recycle();
return rounded;
@SuppressWarnings("deprecation")
public static Bitmap clipAndRound(Bitmap source, float radius, boolean clipSquare) {
Bitmap result = source;
if (clipSquare) {
int width = result.getWidth();
int height = result.getHeight();
int newSize = Math.min(width, height);
int x = (width-newSize) / 2;
int y = (height-newSize) / 2;
result = Bitmap.createBitmap(result, x, y, newSize, newSize);
}
if ((radius > 0f) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
int width = result.getWidth();
int height = result.getHeight();
Bitmap canvasMap = Bitmap.createBitmap(width, height, ARGB_8888);
Canvas canvas = new Canvas(canvasMap);
BitmapShader shader = new BitmapShader(result, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
canvas.drawRoundRect(0, 0, width, height, radius, radius, paint);
result = canvasMap;
}
return result;
}
/*
@ -99,7 +99,7 @@ public class UIUtils {
*/
public static void setCustomActionBar(Activity activity, String imageUrl, String title) {
ImageView iconView = setupCustomActionbar(activity, title);
FeedUtils.imageLoader.displayImage(imageUrl, iconView, false);
FeedUtils.iconLoader.displayImage(imageUrl, iconView, 0, false);
}
public static void setCustomActionBar(Activity activity, int imageId, String title) {

View file

@ -30,7 +30,7 @@ public class ViewUtils {
image.setMaxWidth(imageLength);
image.setLayoutParams(imageParameters);
FeedUtils.imageLoader.displayImage(photoUrl, image, 10f);
FeedUtils.iconLoader.displayImage(photoUrl, image, 10f, false);
image.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {

View file

@ -59,13 +59,13 @@ public abstract class ActivityDetailsAdapter extends ArrayAdapter<ActivityDetail
activityTime.setText(activity.timeSince.toUpperCase() + " " + ago);
if (activity.category == Category.FEED_SUBSCRIPTION) {
FeedUtils.imageLoader.displayImage(APIConstants.S3_URL_FEED_ICONS + activity.feedId + ".png", imageView);
FeedUtils.iconLoader.displayImage(APIConstants.S3_URL_FEED_ICONS + activity.feedId + ".png", imageView, 5, false);
} else if (activity.category == Category.SHARED_STORY) {
FeedUtils.imageLoader.displayImage(currentUserDetails.photoUrl, imageView, 10f);
FeedUtils.iconLoader.displayImage(currentUserDetails.photoUrl, imageView, 10f, false);
} else if (activity.category == Category.STAR) {
imageView.setImageResource(R.drawable.clock);
} else if (activity.user != null) {
FeedUtils.imageLoader.displayImage(activity.user.photoUrl, imageView);
FeedUtils.iconLoader.displayImage(activity.user.photoUrl, imageView, 5, false);
} else {
imageView.setImageResource(R.drawable.logo);
}