summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml1
-rw-r--r--res/values/strings.xml3
-rw-r--r--res/xml/preferences.xml141
-rw-r--r--src/org/fox/ttrss/FeedsFragment.java5
-rw-r--r--src/org/fox/ttrss/ImageCacheService.java181
-rw-r--r--src/org/fox/ttrss/MainActivity.java5
-rw-r--r--src/org/fox/ttrss/OfflineArticleFragment.java28
-rw-r--r--src/org/fox/ttrss/OfflineDownloadService.java76
8 files changed, 390 insertions, 50 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 20282d97..ef0bcc01 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -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"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5a37df58..25871fcd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -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> \ No newline at end of file
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index ec3c5d16..1afeac97 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -1,40 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <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>
-<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/debugging">
- <CheckBoxPreference android:defaultValue="false" android:title="@string/transport_debugging" android:key="transport_debugging" />
- </PreferenceCategory>
-
</PreferenceScreen> \ No newline at end of file
diff --git a/src/org/fox/ttrss/FeedsFragment.java b/src/org/fox/ttrss/FeedsFragment.java
index 0ead6de9..bb823dd0 100644
--- a/src/org/fox/ttrss/FeedsFragment.java
+++ b/src/org/fox/ttrss/FeedsFragment.java
@@ -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;
}
diff --git a/src/org/fox/ttrss/ImageCacheService.java b/src/org/fox/ttrss/ImageCacheService.java
new file mode 100644
index 00000000..999b7437
--- /dev/null
+++ b/src/org/fox/ttrss/ImageCacheService.java
@@ -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);
+ }
+ }
+
+}
diff --git a/src/org/fox/ttrss/MainActivity.java b/src/org/fox/ttrss/MainActivity.java
index 53075466..3310dcd2 100644
--- a/src/org/fox/ttrss/MainActivity.java
+++ b/src/org/fox/ttrss/MainActivity.java
@@ -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);
diff --git a/src/org/fox/ttrss/OfflineArticleFragment.java b/src/org/fox/ttrss/OfflineArticleFragment.java
index 77fae9a0..2469ca51 100644
--- a/src/org/fox/ttrss/OfflineArticleFragment.java
+++ b/src/org/fox/ttrss/OfflineArticleFragment.java
@@ -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);
diff --git a/src/org/fox/ttrss/OfflineDownloadService.java b/src/org/fox/ttrss/OfflineDownloadService.java
index 7a44d48b..933cc75e 100644
--- a/src/org/fox/ttrss/OfflineDownloadService.java
+++ b/src/org/fox/ttrss/OfflineDownloadService.java
@@ -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;