mirror of
https://github.com/viq/NewsBlur.git
synced 2025-09-18 21:43:31 +00:00
commit
45154edbbe
35 changed files with 467 additions and 346 deletions
|
@ -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"
|
||||
|
|
|
@ -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.
BIN
clients/android/NewsBlur/libs/okhttp-3.4.1.jar
Normal file
BIN
clients/android/NewsBlur/libs/okhttp-3.4.1.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
clients/android/NewsBlur/libs/okio-1.9.0.jar
Normal file
BIN
clients/android/NewsBlur/libs/okio-1.9.0.jar
Normal file
Binary file not shown.
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue