implement offline image caching

This commit is contained in:
Andrew Dolgov 2011-12-07 14:59:30 +03:00
parent 14461fa146
commit 42516faa2e
8 changed files with 389 additions and 49 deletions

View File

@ -38,6 +38,7 @@
<service android:enabled="true" android:name=".OfflineDownloadService" />
<service android:enabled="true" android:name=".OfflineUploadService" />
<service android:enabled="true" android:name=".ImageCacheService" />
<activity
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"

View File

@ -110,4 +110,7 @@
<string name="notify_downloading_title">Preparing offline mode</string>
<string name="notify_uploading_title">Synchronizing offline data</string>
<string name="offline_sync_success">Finished synchronizing your offline data</string>
<string name="offline_mode">Offline mode</string>
<string name="offline_image_cache_enabled">Cache images</string>
<string name="offline_image_cache_enabled_summary">Download images to sdcard. This might significantly increase time it takes to go offline.</string>
</resources>

View File

@ -1,40 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/connection">
<EditTextPreference android:summary="@string/login_summary" android:title="@string/login" android:key="login" android:singleLine="true"></EditTextPreference>
<EditTextPreference android:title="@string/password" android:key="password" android:singleLine="true" android:password="true"></EditTextPreference>
<EditTextPreference android:summary="@string/ttrss_url_summary" android:key="ttrss_url" android:title="@string/ttrss_url" android:singleLine="true" textUri="true" android:hint="@string/default_url"></EditTextPreference>
<CheckBoxPreference android:defaultValue="false" android:title="@string/ssl_trust_any" android:key="ssl_trust_any" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/http_authentication">
<EditTextPreference android:title="@string/login" android:summary="@string/http_login_summary" android:key="http_login" android:singleLine="true"></EditTextPreference>
<EditTextPreference android:title="@string/password" android:key="http_password" android:singleLine="true" android:password="true"></EditTextPreference>
</PreferenceCategory>
<PreferenceCategory android:title="@string/look_and_feel">
<ListPreference
android:title="@string/pref_theme"
android:key="theme"
android:defaultValue="THEME_DARK"
android:entries="@array/pref_theme_names"
android:entryValues="@array/pref_theme_values" android:summary="@string/pref_theme_long"/>
<CheckBoxPreference android:defaultValue="false" android:title="@string/sort_feeds_by_unread" android:key="sort_feeds_by_unread"/>
<CheckBoxPreference android:defaultValue="false" android:title="@string/download_feed_icons" android:key="download_feed_icons"/>
<CheckBoxPreference android:defaultValue="false" android:title="@string/enable_cats" android:key="enable_cats" />
<CheckBoxPreference android:defaultValue="false" android:title="@string/browse_cats_like_feeds" android:key="browse_cats_like_feeds"
android:summary="@string/browse_cats_like_feeds_summary" />
<CheckBoxPreference android:defaultValue="false" android:summary="@string/combined_mode_summary" android:title="@string/combined_mode" android:key="combined_mode" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/connection" >
<EditTextPreference
android:key="login"
android:singleLine="true"
android:summary="@string/login_summary"
android:title="@string/login" >
</EditTextPreference>
<EditTextPreference
android:key="password"
android:password="true"
android:singleLine="true"
android:title="@string/password" >
</EditTextPreference>
<EditTextPreference
android:hint="@string/default_url"
android:key="ttrss_url"
android:singleLine="true"
android:summary="@string/ttrss_url_summary"
textUri="true"
android:title="@string/ttrss_url" >
</EditTextPreference>
<CheckBoxPreference
android:defaultValue="false"
android:key="ssl_trust_any"
android:title="@string/ssl_trust_any" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/http_authentication" >
<EditTextPreference
android:key="http_login"
android:singleLine="true"
android:summary="@string/http_login_summary"
android:title="@string/login" >
</EditTextPreference>
<EditTextPreference
android:key="http_password"
android:password="true"
android:singleLine="true"
android:title="@string/password" >
</EditTextPreference>
</PreferenceCategory>
<PreferenceCategory android:title="@string/look_and_feel" >
<ListPreference
android:defaultValue="THEME_DARK"
android:entries="@array/pref_theme_names"
android:entryValues="@array/pref_theme_values"
android:key="theme"
android:summary="@string/pref_theme_long"
android:title="@string/pref_theme" />
<CheckBoxPreference
android:defaultValue="false"
android:key="sort_feeds_by_unread"
android:title="@string/sort_feeds_by_unread" />
<CheckBoxPreference
android:defaultValue="false"
android:key="download_feed_icons"
android:title="@string/download_feed_icons" />
<CheckBoxPreference
android:defaultValue="false"
android:key="enable_cats"
android:title="@string/enable_cats" />
<CheckBoxPreference
android:defaultValue="false"
android:key="browse_cats_like_feeds"
android:summary="@string/browse_cats_like_feeds_summary"
android:title="@string/browse_cats_like_feeds" />
<CheckBoxPreference
android:defaultValue="false"
android:key="combined_mode"
android:summary="@string/combined_mode_summary"
android:title="@string/combined_mode" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/offline_mode" >
<CheckBoxPreference
android:defaultValue="false"
android:key="offline_image_cache_enabled"
android:summary="@string/offline_image_cache_enabled_summary"
android:title="@string/offline_image_cache_enabled" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/debugging" >
<CheckBoxPreference
android:defaultValue="false"
android:key="transport_debugging"
android:title="@string/transport_debugging" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/debugging">
<CheckBoxPreference android:defaultValue="false" android:title="@string/transport_debugging" android:key="transport_debugging" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -7,6 +7,7 @@ import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -308,7 +309,9 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
else
setLoadingStatus(R.string.blank, false);
if (m_enableFeedIcons && !m_feedIconsChecked) getFeedIcons();
if (m_enableFeedIcons && !m_feedIconsChecked &&
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
getFeedIcons();
return;
}

View File

@ -0,0 +1,181 @@
package org.fox.ttrss;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import android.app.ActivityManager;
import android.app.IntentService;
import android.app.NotificationManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.Intent;
import android.os.Environment;
import android.util.Log;
public class ImageCacheService extends IntentService {
private final String TAG = this.getClass().getSimpleName();
public static final int NOTIFY_DOWNLOADING = 1;
private static final String CACHE_PATH = "/org.fox.ttrss/image-cache/";
private int m_imagesDownloaded = 0;
private NotificationManager m_nmgr;
public ImageCacheService() {
super("ImageCacheService");
}
private boolean isDownloadServiceRunning() {
ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if ("org.fox.ttrss.OfflineDownloadService".equals(service.service.getClassName())) {
return true;
}
}
return false;
}
@Override
public void onCreate() {
super.onCreate();
m_nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
}
protected static boolean isUrlCached(String url) {
String hashedUrl = md5(url);
File storage = Environment.getExternalStorageDirectory();
File file = new File(storage.getAbsolutePath() + CACHE_PATH + "/" + hashedUrl + ".png");
return file.exists();
}
protected static String getCacheFileName(String url) {
String hashedUrl = md5(url);
File storage = Environment.getExternalStorageDirectory();
File file = new File(storage.getAbsolutePath() + CACHE_PATH + "/" + hashedUrl + ".png");
return file.getAbsolutePath();
}
protected static void cleanupCache(boolean deleteAll) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File storage = Environment.getExternalStorageDirectory();
File cachePath = new File(storage.getAbsolutePath() + CACHE_PATH);
long now = new Date().getTime();
if (cachePath.isDirectory()) {
for (File file : cachePath.listFiles()) {
if (deleteAll || now - file.lastModified() > 1000*60*60*24*7) {
file.delete();
}
}
}
}
}
protected static String md5(String s) {
try {
MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
digest.update(s.getBytes());
byte messageDigest[] = digest.digest();
StringBuffer hexString = new StringBuffer();
for (int i=0; i<messageDigest.length; i++)
hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
private InputStream getStream(String urlString) {
try {
URL url = new URL(urlString);
URLConnection urlConnection = url.openConnection();
urlConnection.setConnectTimeout(250);
return urlConnection.getInputStream();
} catch (Exception ex) {
return null;
}
}
@Override
protected void onHandleIntent(Intent intent) {
String url = intent.getStringExtra("url");
//Log.d(TAG, "got request to download URL=" + url);
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
return;
String hashedUrl = md5(url);
File storage = Environment.getExternalStorageDirectory();
File cachePath = new File(storage.getAbsolutePath() + CACHE_PATH);
if (!cachePath.exists()) cachePath.mkdirs();
if (cachePath.isDirectory() && hashedUrl != null) {
File outputFile = new File(cachePath.getAbsolutePath() + "/" + hashedUrl + ".png");
if (!outputFile.exists()) {
//Log.d(TAG, "downloading to " + outputFile.getAbsolutePath());
InputStream is = getStream(url);
if (is != null) {
try {
FileOutputStream fos = new FileOutputStream(outputFile);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
is.close();
m_imagesDownloaded++;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (!isDownloadServiceRunning()) {
m_nmgr.cancel(NOTIFY_DOWNLOADING);
Intent success = new Intent();
success.setAction(OfflineDownloadService.INTENT_ACTION_SUCCESS);
success.addCategory(Intent.CATEGORY_DEFAULT);
sendBroadcast(success);
}
}
}

View File

@ -551,6 +551,11 @@ public class MainActivity extends FragmentActivity implements OnlineServices {
new Dialog.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
SharedPreferences.Editor editor = m_prefs.edit();
editor.putBoolean("offline_mode_active", true);
editor.commit();
Intent refresh = new Intent(
MainActivity.this,
OfflineActivity.class);

View File

@ -4,8 +4,13 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import org.fox.ttrss.OnlineServices.RelativeArticle;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
@ -14,6 +19,7 @@ import android.provider.BaseColumns;
import android.support.v4.app.Fragment;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.LayoutInflater;
@ -123,6 +129,26 @@ public class OfflineArticleFragment extends Fragment implements OnClickListener
cssOverride = "";
}
String articleContent = m_cursor.getString(m_cursor.getColumnIndex("content"));
if (m_prefs.getBoolean("offline_image_cache_enabled", false)) {
Document doc = Jsoup.parse(articleContent);
if (doc != null) {
Elements images = doc.select("img");
for (Element img : images) {
String url = img.attr("src");
if (ImageCacheService.isUrlCached(url)) {
img.attr("src", "file://" + ImageCacheService.getCacheFileName(url));
}
}
articleContent = doc.toString();
}
}
content =
"<html>" +
"<head>" +
@ -134,7 +160,7 @@ public class OfflineArticleFragment extends Fragment implements OnClickListener
"body { text-align : justify; }" +
"</style>" +
"</head>" +
"<body>" + m_cursor.getString(m_cursor.getColumnIndex("content")) + "</body></html>";
"<body>" + articleContent + "</body></html>";
web.loadDataWithBaseURL(null, content, "text/html", "utf-8", null);

View File

@ -4,15 +4,25 @@ import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.util.Log;
@ -37,6 +47,9 @@ public class OfflineDownloadService extends IntentService {
private NotificationManager m_nmgr;
private boolean m_downloadInProgress = false;
private boolean m_downloadImages = false;
private int m_syncMax;
private SharedPreferences m_prefs;
public OfflineDownloadService() {
super("OfflineDownloadService");
@ -46,13 +59,15 @@ public class OfflineDownloadService extends IntentService {
public void onCreate() {
super.onCreate();
m_nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
m_prefs = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
m_downloadImages = m_prefs.getBoolean("offline_image_cache_enabled", false);
m_syncMax = m_prefs.getInt("offline_sync_max", OFFLINE_SYNC_MAX);
initDatabase();
}
/* public boolean getDownloadInProgress() {
return m_downloadInProgress;
} */
private void updateNotification(String msg) {
Notification notification = new Notification(R.drawable.icon,
getString(R.string.notify_downloading_title), System.currentTimeMillis());
@ -78,15 +93,30 @@ public class OfflineDownloadService extends IntentService {
m_downloadInProgress = false;
}
private boolean isCacheServiceRunning() {
ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if ("org.fox.ttrss.ImageCacheService".equals(service.service.getClassName())) {
return true;
}
}
return false;
}
public void downloadComplete() {
m_downloadInProgress = false;
m_nmgr.cancel(NOTIFY_DOWNLOADING);
Intent intent = new Intent();
intent.setAction(INTENT_ACTION_SUCCESS);
intent.addCategory(Intent.CATEGORY_DEFAULT);
sendBroadcast(intent);
// if cache service is running, it will send a finished intent on its own
if (!isCacheServiceRunning()) {
m_nmgr.cancel(NOTIFY_DOWNLOADING);
Intent intent = new Intent();
intent.setAction(INTENT_ACTION_SUCCESS);
intent.addCategory(Intent.CATEGORY_DEFAULT);
sendBroadcast(intent);
} else {
updateNotification("Downloading images...");
}
m_readableDb.close();
m_writableDb.close();
@ -242,6 +272,28 @@ public class OfflineDownloadService extends IntentService {
stmtInsert.bindString(10, tagsString); // comma-separated tags
stmtInsert.bindString(11, article.content);
if (m_downloadImages) {
Document doc = Jsoup.parse(article.content);
if (doc != null) {
Elements images = doc.select("img");
for (Element img : images) {
String url = img.attr("src");
if (url.indexOf("://") != -1) {
if (!ImageCacheService.isUrlCached(url)) {
Intent intent = new Intent(OfflineDownloadService.this,
ImageCacheService.class);
intent.putExtra("url", url);
startService(intent);
}
}
}
}
}
try {
stmtInsert.execute();
} catch (Exception e) {
@ -257,7 +309,7 @@ public class OfflineDownloadService extends IntentService {
Log.d(TAG, "offline: received " + articles.size() + " articles");
if (articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < OFFLINE_SYNC_MAX) {
if (articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < m_syncMax) {
downloadArticles();
} else {
downloadComplete();
@ -285,6 +337,8 @@ public class OfflineDownloadService extends IntentService {
m_sessionId = intent.getStringExtra("sessionId");
if (!m_downloadInProgress) {
if (m_downloadImages) ImageCacheService.cleanupCache(false);
updateNotification(R.string.notify_downloading_init);
m_downloadInProgress = true;