From ced80be1aea975b59db5a0e6dd14acb054e7910e Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 16 Sep 2012 13:25:28 +0400 Subject: initial --- src/org/fox/ttrss/util/ApiRequest.java | 257 ++++++++++++++++++++++++++ src/org/fox/ttrss/util/ImageCacheService.java | 208 --------------------- 2 files changed, 257 insertions(+), 208 deletions(-) create mode 100644 src/org/fox/ttrss/util/ApiRequest.java delete mode 100644 src/org/fox/ttrss/util/ImageCacheService.java (limited to 'src/org/fox/ttrss/util') diff --git a/src/org/fox/ttrss/util/ApiRequest.java b/src/org/fox/ttrss/util/ApiRequest.java new file mode 100644 index 00000000..56901393 --- /dev/null +++ b/src/org/fox/ttrss/util/ApiRequest.java @@ -0,0 +1,257 @@ +package org.fox.ttrss.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; + +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; +import org.fox.ttrss.R; +import org.fox.ttrss.R.string; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.http.AndroidHttpClient; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class ApiRequest extends AsyncTask, Integer, JsonElement> { + private final String TAG = this.getClass().getSimpleName(); + + public enum ApiError { NO_ERROR, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, + HTTP_SERVER_ERROR, HTTP_OTHER_ERROR, SSL_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED, API_UNKNOWN, LOGIN_FAILED, INVALID_URL, INCORRECT_USAGE }; + + public static final int API_STATUS_OK = 0; + public static final int API_STATUS_ERR = 1; + + private String m_api; + private boolean m_trustAny = false; + private boolean m_transportDebugging = false; + protected int m_httpStatusCode = 0; + protected int m_apiStatusCode = 0; + protected Context m_context; + private SharedPreferences m_prefs; + + protected ApiError m_lastError; + + public ApiRequest(Context context) { + m_context = context; + + m_prefs = PreferenceManager.getDefaultSharedPreferences(m_context); + + m_api = m_prefs.getString("ttrss_url", null).trim(); + m_trustAny = m_prefs.getBoolean("ssl_trust_any", false); + m_transportDebugging = m_prefs.getBoolean("transport_debugging", false); + m_lastError = ApiError.NO_ERROR; + + } + + protected int getErrorMessage() { + switch (m_lastError) { + case NO_ERROR: + return R.string.error_unknown; + case HTTP_UNAUTHORIZED: + return R.string.error_http_unauthorized; + case HTTP_FORBIDDEN: + return R.string.error_http_forbidden; + case HTTP_NOT_FOUND: + return R.string.error_http_not_found; + case HTTP_SERVER_ERROR: + return R.string.error_http_server_error; + case HTTP_OTHER_ERROR: + return R.string.error_http_other_error; + case SSL_REJECTED: + return R.string.error_ssl_rejected; + case PARSE_ERROR: + return R.string.error_parse_error; + case IO_ERROR: + return R.string.error_io_error; + case OTHER_ERROR: + return R.string.error_other_error; + case API_DISABLED: + return R.string.error_api_disabled; + case API_UNKNOWN: + return R.string.error_api_unknown; + case LOGIN_FAILED: + return R.string.error_login_failed; + case INVALID_URL: + return R.string.error_invalid_api_url; + case INCORRECT_USAGE: + return R.string.error_api_incorrect_usage; + default: + Log.d(TAG, "getErrorMessage: unknown error code=" + m_lastError); + return R.string.error_unknown; + } + } + + @Override + protected JsonElement doInBackground(HashMap... params) { + + Gson gson = new Gson(); + + String requestStr = gson.toJson(new HashMap(params[0])); + + if (m_transportDebugging) Log.d(TAG, ">>> (" + requestStr + ") " + m_api); + + AndroidHttpClient client = AndroidHttpClient.newInstance("Tiny Tiny RSS"); + + if (m_trustAny) { + client.getConnectionManager().getSchemeRegistry().register(new Scheme("https", new EasySSLSocketFactory(), 443)); + } + + try { + + HttpPost httpPost; + + try { + httpPost = new HttpPost(m_api + "/api/"); + } catch (IllegalArgumentException e) { + m_lastError = ApiError.INVALID_URL; + e.printStackTrace(); + client.close(); + return null; + } catch (Exception e) { + m_lastError = ApiError.OTHER_ERROR; + e.printStackTrace(); + client.close(); + return null; + } + + HttpContext context = null; + + String httpLogin = m_prefs.getString("http_login", "").trim(); + String httpPassword = m_prefs.getString("http_password", "").trim(); + + if (httpLogin.length() > 0) { + if (m_transportDebugging) Log.d(TAG, "Using HTTP Basic authentication."); + + URL targetUrl; + try { + targetUrl = new URL(m_api); + } catch (MalformedURLException e) { + m_lastError = ApiError.INVALID_URL; + e.printStackTrace(); + client.close(); + return null; + } + + HttpHost targetHost = new HttpHost(targetUrl.getHost(), targetUrl.getPort(), targetUrl.getProtocol()); + CredentialsProvider cp = new BasicCredentialsProvider(); + context = new BasicHttpContext(); + + cp.setCredentials( + new AuthScope(targetHost.getHostName(), targetHost.getPort()), + new UsernamePasswordCredentials(httpLogin, httpPassword)); + + context.setAttribute(ClientContext.CREDS_PROVIDER, cp); + } + + httpPost.setEntity(new StringEntity(requestStr, "utf-8")); + HttpResponse execute = client.execute(httpPost, context); + + m_httpStatusCode = execute.getStatusLine().getStatusCode(); + + switch (m_httpStatusCode) { + case 200: + InputStream content = execute.getEntity().getContent(); + + BufferedReader buffer = new BufferedReader( + new InputStreamReader(content), 8192); + + String s = ""; + String response = ""; + + while ((s = buffer.readLine()) != null) { + response += s; + } + + if (m_transportDebugging) Log.d(TAG, "<<< " + response); + + JsonParser parser = new JsonParser(); + + JsonElement result = parser.parse(response); + JsonObject resultObj = result.getAsJsonObject(); + + m_apiStatusCode = resultObj.get("status").getAsInt(); + + client.close(); + + switch (m_apiStatusCode) { + case API_STATUS_OK: + return result.getAsJsonObject().get("content"); + case API_STATUS_ERR: + JsonObject contentObj = resultObj.get("content").getAsJsonObject(); + String error = contentObj.get("error").getAsString(); + + if (error.equals("LOGIN_ERROR")) { + m_lastError = ApiError.LOGIN_FAILED; + } else if (error.equals("API_DISABLED")) { + m_lastError = ApiError.API_DISABLED; + } else if (error.equals("NOT_LOGGED_IN")) { + m_lastError = ApiError.LOGIN_FAILED; + } else if (error.equals("INCORRECT_USAGE")) { + m_lastError = ApiError.INCORRECT_USAGE; + } else { + Log.d(TAG, "Unknown API error: " + error); + m_lastError = ApiError.API_UNKNOWN; + } + } + + return null; + case 401: + m_lastError = ApiError.HTTP_UNAUTHORIZED; + break; + case 403: + m_lastError = ApiError.HTTP_FORBIDDEN; + break; + case 404: + m_lastError = ApiError.HTTP_NOT_FOUND; + break; + case 500: + m_lastError = ApiError.HTTP_SERVER_ERROR; + break; + default: + m_lastError = ApiError.HTTP_OTHER_ERROR; + break; + } + + client.close(); + return null; + } catch (javax.net.ssl.SSLPeerUnverifiedException e) { + m_lastError = ApiError.SSL_REJECTED; + e.printStackTrace(); + } catch (IOException e) { + m_lastError = ApiError.IO_ERROR; + e.printStackTrace(); + } catch (com.google.gson.JsonSyntaxException e) { + m_lastError = ApiError.PARSE_ERROR; + e.printStackTrace(); + } catch (Exception e) { + m_lastError = ApiError.OTHER_ERROR; + e.printStackTrace(); + } + + client.close(); + return null; + } +} diff --git a/src/org/fox/ttrss/util/ImageCacheService.java b/src/org/fox/ttrss/util/ImageCacheService.java deleted file mode 100644 index 25c4aff0..00000000 --- a/src/org/fox/ttrss/util/ImageCacheService.java +++ /dev/null @@ -1,208 +0,0 @@ -package org.fox.ttrss.util; - -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 org.fox.ttrss.MainActivity; -import org.fox.ttrss.R; -import org.fox.ttrss.offline.OfflineDownloadService; - -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.Intent; -import android.os.Environment; - -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 = "/data/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); - } - - public 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(); - } - - public 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(); - } - - public 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 Date: Mon, 17 Sep 2012 13:27:27 +0400 Subject: implement background loading in ArticlePager various loading and state related fixes --- src/org/fox/ttrss/ArticlePager.java | 73 +++++++- src/org/fox/ttrss/CommonActivity.java | 2 +- src/org/fox/ttrss/FeedsActivity.java | 81 +++++---- src/org/fox/ttrss/HeadlinesActivity.java | 4 + src/org/fox/ttrss/HeadlinesFragment.java | 26 ++- src/org/fox/ttrss/OnlineActivity.java | 2 +- src/org/fox/ttrss/util/ApiRequest.java | 257 --------------------------- src/org/fox/ttrss/util/HeadlinesRequest.java | 85 +++++++++ 8 files changed, 221 insertions(+), 309 deletions(-) delete mode 100644 src/org/fox/ttrss/util/ApiRequest.java create mode 100644 src/org/fox/ttrss/util/HeadlinesRequest.java (limited to 'src/org/fox/ttrss/util') diff --git a/src/org/fox/ttrss/ArticlePager.java b/src/org/fox/ttrss/ArticlePager.java index f9f7b1bd..a7ecaaa5 100644 --- a/src/org/fox/ttrss/ArticlePager.java +++ b/src/org/fox/ttrss/ArticlePager.java @@ -1,7 +1,13 @@ package org.fox.ttrss; +import java.util.HashMap; + import org.fox.ttrss.types.Article; import org.fox.ttrss.types.ArticleList; +import org.fox.ttrss.types.Feed; +import org.fox.ttrss.util.HeadlinesRequest; + +import com.google.gson.JsonElement; import android.app.Activity; import android.os.Bundle; @@ -9,6 +15,7 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -20,6 +27,8 @@ public class ArticlePager extends Fragment { private HeadlinesEventListener m_onlineServices; private Article m_article; private ArticleList m_articles; + private OnlineActivity m_activity; + private String m_searchQuery = ""; private class PagerAdapter extends FragmentStatePagerAdapter { @@ -54,6 +63,10 @@ public class ArticlePager extends Fragment { m_article = article; } + + public void setSearchQuery(String searchQuery) { + m_searchQuery = searchQuery; + } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -98,9 +111,8 @@ public class ArticlePager extends Fragment { //Log.d(TAG, "Page #" + position + "/" + m_adapter.getCount()); if (position == m_adapter.getCount() - 5) { - // FIXME load more articles somehow - //m_hf.refresh(true); - m_adapter.notifyDataSetChanged(); + Log.d(TAG, "loading more articles..."); + loadMoreArticles(); } } } @@ -109,6 +121,58 @@ public class ArticlePager extends Fragment { return view; } + @SuppressWarnings({ "unchecked", "serial" }) + private void loadMoreArticles() { + m_activity.setLoadingStatus(R.string.blank, true); + + HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity) { + protected void onPostExecute(JsonElement result) { + super.onPostExecute(result); + m_adapter.notifyDataSetChanged(); + } + }; + + final Feed feed = TinyApplication.getInstance().m_activeFeed; + + final String sessionId = m_activity.getSessionId(); + final boolean showUnread = m_activity.getUnreadArticlesOnly(); + int skip = 0; + + for (Article a : m_articles) { + if (a.unread) ++skip; + } + + if (skip == 0) skip = m_articles.size(); + + final int fskip = skip; + + req.setOffset(skip); + + HashMap map = new HashMap() { + { + put("op", "getHeadlines"); + put("sid", sessionId); + put("feed_id", String.valueOf(feed.id)); + put("show_content", "true"); + put("include_attachments", "true"); + put("limit", String.valueOf(HeadlinesFragment.HEADLINES_REQUEST_SIZE)); + put("offset", String.valueOf(0)); + put("view_mode", showUnread ? "adaptive" : "all_articles"); + put("skip", String.valueOf(fskip)); + + if (feed.is_cat) put("is_cat", "true"); + + if (m_searchQuery != null && m_searchQuery.length() != 0) { + put("search", m_searchQuery); + put("search_mode", ""); + put("match_on", "both"); + } + } + }; + + req.execute(map); + } + @Override public void onSaveInstanceState(Bundle out) { super.onSaveInstanceState(out); @@ -122,6 +186,7 @@ public class ArticlePager extends Fragment { super.onAttach(activity); m_onlineServices = (HeadlinesEventListener)activity; + m_activity = (OnlineActivity)activity; m_articles = TinyApplication.getInstance().m_loadedArticles; } @@ -130,7 +195,7 @@ public class ArticlePager extends Fragment { public void onResume() { super.onResume(); - ((OnlineActivity)getActivity()).initMenu(); + m_activity.initMenu(); } public Article getSelectedArticle() { diff --git a/src/org/fox/ttrss/CommonActivity.java b/src/org/fox/ttrss/CommonActivity.java index 9f67e49d..d9359b24 100644 --- a/src/org/fox/ttrss/CommonActivity.java +++ b/src/org/fox/ttrss/CommonActivity.java @@ -32,7 +32,7 @@ public class CommonActivity extends FragmentActivity { m_smallScreenMode = smallScreen; } - protected void setLoadingStatus(int status, boolean showProgress) { + public void setLoadingStatus(int status, boolean showProgress) { TextView tv = (TextView) findViewById(R.id.loading_message); if (tv != null) { diff --git a/src/org/fox/ttrss/FeedsActivity.java b/src/org/fox/ttrss/FeedsActivity.java index 291ff59e..72ca5bff 100644 --- a/src/org/fox/ttrss/FeedsActivity.java +++ b/src/org/fox/ttrss/FeedsActivity.java @@ -45,50 +45,55 @@ public class FeedsActivity extends OnlineActivity implements HeadlinesEventListe Intent intent = getIntent(); - if (intent.getParcelableExtra("feed") != null || intent.getParcelableExtra("category") != null || - intent.getParcelableExtra("article") != null) { - - Feed feed = (Feed) intent.getParcelableExtra("feed"); - FeedCategory cat = (FeedCategory) intent.getParcelableExtra("category"); - Article article = (Article) intent.getParcelableExtra("article"); - - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - - if (feed != null) { - HeadlinesFragment hf = new HeadlinesFragment(feed); - ft.replace(R.id.feeds_fragment, hf, FRAG_HEADLINES); - - setTitle(feed.title); - } + if (savedInstanceState == null) { - if (cat != null) { - FeedsFragment ff = new FeedsFragment(cat); - ft.replace(R.id.feeds_fragment, ff, FRAG_FEEDS); - - setTitle(cat.title); - } + if (intent.getParcelableExtra("feed") != null || intent.getParcelableExtra("category") != null || + intent.getParcelableExtra("article") != null) { - if (article != null) { - Article original = TinyApplication.getInstance().m_loadedArticles.findById(article.id); + Feed feed = (Feed) intent.getParcelableExtra("feed"); + FeedCategory cat = (FeedCategory) intent.getParcelableExtra("category"); + Article article = (Article) intent.getParcelableExtra("article"); + + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + + if (feed != null) { + HeadlinesFragment hf = new HeadlinesFragment(feed); + ft.replace(R.id.feeds_fragment, hf, FRAG_HEADLINES); + + setTitle(feed.title); + } - ArticlePager ap = new ArticlePager(original != null ? original : article); - ft.replace(R.id.feeds_fragment, ap, FRAG_ARTICLE); + if (cat != null) { + FeedsFragment ff = new FeedsFragment(cat); + ft.replace(R.id.feeds_fragment, ff, FRAG_FEEDS); + + setTitle(cat.title); + } - setTitle(intent.getStringExtra("feedTitle")); - } - - ft.commit(); + if (article != null) { + Article original = TinyApplication.getInstance().m_loadedArticles.findById(article.id); + + ArticlePager ap = new ArticlePager(original != null ? original : article); + ft.replace(R.id.feeds_fragment, ap, FRAG_ARTICLE); + + ap.setSearchQuery(intent.getStringExtra("searchQuery")); + + setTitle(intent.getStringExtra("feedTitle")); + } + + ft.commit(); - } else if (savedInstanceState == null) { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + } else { + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - if (m_prefs.getBoolean("enable_cats", false)) { - ft.replace(R.id.feeds_fragment, new FeedCategoriesFragment(), FRAG_CATS); - } else { - ft.replace(R.id.feeds_fragment, new FeedsFragment(), FRAG_FEEDS); + if (m_prefs.getBoolean("enable_cats", false)) { + ft.replace(R.id.feeds_fragment, new FeedCategoriesFragment(), FRAG_CATS); + } else { + ft.replace(R.id.feeds_fragment, new FeedsFragment(), FRAG_FEEDS); + } + + ft.commit(); } - - ft.commit(); } } @@ -257,6 +262,7 @@ public class FeedsActivity extends OnlineActivity implements HeadlinesEventListe intent.putExtra("feedTitle", hf.getFeed().title); intent.putExtra("article", article); + intent.putExtra("searchQuery", hf.getSearchQuery()); startActivityForResult(intent, 0); @@ -268,6 +274,7 @@ public class FeedsActivity extends OnlineActivity implements HeadlinesEventListe intent.putExtra("feed", hf.getFeed()); intent.putExtra("article", article); + intent.putExtra("searchQuery", hf.getSearchQuery()); startActivityForResult(intent, 0); } diff --git a/src/org/fox/ttrss/HeadlinesActivity.java b/src/org/fox/ttrss/HeadlinesActivity.java index 0cd18cc5..2d7a6a3a 100644 --- a/src/org/fox/ttrss/HeadlinesActivity.java +++ b/src/org/fox/ttrss/HeadlinesActivity.java @@ -50,10 +50,14 @@ public class HeadlinesActivity extends OnlineActivity implements HeadlinesEventL if (i.getExtras() != null) { Feed feed = i.getParcelableExtra("feed"); Article article = i.getParcelableExtra("article"); + String searchQuery = i.getStringExtra("searchQuery"); HeadlinesFragment hf = new HeadlinesFragment(feed, article); ArticlePager af = new ArticlePager(hf.getArticleById(article.id)); + hf.setSearchQuery(searchQuery); + af.setSearchQuery(searchQuery); + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES); diff --git a/src/org/fox/ttrss/HeadlinesFragment.java b/src/org/fox/ttrss/HeadlinesFragment.java index 986581fb..031321eb 100644 --- a/src/org/fox/ttrss/HeadlinesFragment.java +++ b/src/org/fox/ttrss/HeadlinesFragment.java @@ -15,6 +15,7 @@ import org.fox.ttrss.types.Article; import org.fox.ttrss.types.ArticleList; import org.fox.ttrss.types.Attachment; import org.fox.ttrss.types.Feed; +import org.fox.ttrss.util.HeadlinesRequest; import org.jsoup.Jsoup; import android.app.Activity; @@ -67,10 +68,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, private Feed m_feed; private Article m_activeArticle; - private boolean m_refreshInProgress = false; - private boolean m_canLoadMore = false; private boolean m_combinedMode = true; private String m_searchQuery = ""; + private boolean m_refreshInProgress = false; private SharedPreferences m_prefs; @@ -268,7 +268,6 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, //m_articles = savedInstanceState.getParcelable("articles"); m_activeArticle = savedInstanceState.getParcelable("activeArticle"); m_selectedArticles = savedInstanceState.getParcelable("selectedArticles"); - m_canLoadMore = savedInstanceState.getBoolean("canLoadMore"); m_combinedMode = savedInstanceState.getBoolean("combinedMode"); m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery"); } @@ -355,7 +354,13 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, public void refresh(boolean append) { m_refreshInProgress = true; - HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext()); + HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity) { + protected void onPostExecute(JsonElement result) { + super.onPostExecute(result); + m_refreshInProgress = false; + m_adapter.notifyDataSetChanged(); + } + }; final String sessionId = m_activity.getSessionId(); final boolean showUnread = m_listener.getUnreadArticlesOnly(); @@ -394,7 +399,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, if (isCat) put("is_cat", "true"); - if (m_searchQuery.length() != 0) { + if (m_searchQuery != null && m_searchQuery.length() != 0) { put("search", m_searchQuery); put("search_mode", ""); put("match_on", "both"); @@ -413,7 +418,6 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, //out.putParcelable("articles", m_articles); out.putParcelable("activeArticle", m_activeArticle); out.putParcelable("selectedArticles", m_selectedArticles); - out.putBoolean("canLoadMore", m_canLoadMore); out.putBoolean("combinedMode", m_combinedMode); out.putCharSequence("searchQuery", m_searchQuery); } @@ -431,7 +435,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, getActivity().setProgressBarIndeterminateVisibility(showProgress); } - private class HeadlinesRequest extends ApiRequest { + /* private class HeadlinesRequest extends ApiRequest { int m_offset = 0; public HeadlinesRequest(Context context) { @@ -494,7 +498,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, public void setOffset(int skip) { m_offset = skip; } - } + } */ private class ArticleListAdapter extends ArrayAdapter
{ private ArrayList
items; @@ -842,7 +846,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - if (!m_refreshInProgress && m_canLoadMore && firstVisibleItem + visibleItemCount == m_articles.size()) { + if (!m_refreshInProgress && m_articles.findById(-1) != null && firstVisibleItem + visibleItemCount == m_articles.size()) { refresh(true); } } @@ -864,6 +868,10 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener, } } + public String getSearchQuery() { + return m_searchQuery; + } + public void setSearchQuery(String query) { if (!m_searchQuery.equals(query)) { m_searchQuery = query; diff --git a/src/org/fox/ttrss/OnlineActivity.java b/src/org/fox/ttrss/OnlineActivity.java index d47e11dc..b91a398a 100644 --- a/src/org/fox/ttrss/OnlineActivity.java +++ b/src/org/fox/ttrss/OnlineActivity.java @@ -131,7 +131,7 @@ public class OnlineActivity extends CommonActivity { setContentView(R.layout.online); } - protected void login() { + public void login() { if (m_prefs.getString("ttrss_url", "").trim().length() == 0) { setLoadingStatus(R.string.login_need_configure, false); diff --git a/src/org/fox/ttrss/util/ApiRequest.java b/src/org/fox/ttrss/util/ApiRequest.java deleted file mode 100644 index 56901393..00000000 --- a/src/org/fox/ttrss/util/ApiRequest.java +++ /dev/null @@ -1,257 +0,0 @@ -package org.fox.ttrss.util; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; - -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.protocol.ClientContext; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.HttpContext; -import org.fox.ttrss.R; -import org.fox.ttrss.R.string; - -import android.content.Context; -import android.content.SharedPreferences; -import android.net.http.AndroidHttpClient; -import android.os.AsyncTask; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -public class ApiRequest extends AsyncTask, Integer, JsonElement> { - private final String TAG = this.getClass().getSimpleName(); - - public enum ApiError { NO_ERROR, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, - HTTP_SERVER_ERROR, HTTP_OTHER_ERROR, SSL_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED, API_UNKNOWN, LOGIN_FAILED, INVALID_URL, INCORRECT_USAGE }; - - public static final int API_STATUS_OK = 0; - public static final int API_STATUS_ERR = 1; - - private String m_api; - private boolean m_trustAny = false; - private boolean m_transportDebugging = false; - protected int m_httpStatusCode = 0; - protected int m_apiStatusCode = 0; - protected Context m_context; - private SharedPreferences m_prefs; - - protected ApiError m_lastError; - - public ApiRequest(Context context) { - m_context = context; - - m_prefs = PreferenceManager.getDefaultSharedPreferences(m_context); - - m_api = m_prefs.getString("ttrss_url", null).trim(); - m_trustAny = m_prefs.getBoolean("ssl_trust_any", false); - m_transportDebugging = m_prefs.getBoolean("transport_debugging", false); - m_lastError = ApiError.NO_ERROR; - - } - - protected int getErrorMessage() { - switch (m_lastError) { - case NO_ERROR: - return R.string.error_unknown; - case HTTP_UNAUTHORIZED: - return R.string.error_http_unauthorized; - case HTTP_FORBIDDEN: - return R.string.error_http_forbidden; - case HTTP_NOT_FOUND: - return R.string.error_http_not_found; - case HTTP_SERVER_ERROR: - return R.string.error_http_server_error; - case HTTP_OTHER_ERROR: - return R.string.error_http_other_error; - case SSL_REJECTED: - return R.string.error_ssl_rejected; - case PARSE_ERROR: - return R.string.error_parse_error; - case IO_ERROR: - return R.string.error_io_error; - case OTHER_ERROR: - return R.string.error_other_error; - case API_DISABLED: - return R.string.error_api_disabled; - case API_UNKNOWN: - return R.string.error_api_unknown; - case LOGIN_FAILED: - return R.string.error_login_failed; - case INVALID_URL: - return R.string.error_invalid_api_url; - case INCORRECT_USAGE: - return R.string.error_api_incorrect_usage; - default: - Log.d(TAG, "getErrorMessage: unknown error code=" + m_lastError); - return R.string.error_unknown; - } - } - - @Override - protected JsonElement doInBackground(HashMap... params) { - - Gson gson = new Gson(); - - String requestStr = gson.toJson(new HashMap(params[0])); - - if (m_transportDebugging) Log.d(TAG, ">>> (" + requestStr + ") " + m_api); - - AndroidHttpClient client = AndroidHttpClient.newInstance("Tiny Tiny RSS"); - - if (m_trustAny) { - client.getConnectionManager().getSchemeRegistry().register(new Scheme("https", new EasySSLSocketFactory(), 443)); - } - - try { - - HttpPost httpPost; - - try { - httpPost = new HttpPost(m_api + "/api/"); - } catch (IllegalArgumentException e) { - m_lastError = ApiError.INVALID_URL; - e.printStackTrace(); - client.close(); - return null; - } catch (Exception e) { - m_lastError = ApiError.OTHER_ERROR; - e.printStackTrace(); - client.close(); - return null; - } - - HttpContext context = null; - - String httpLogin = m_prefs.getString("http_login", "").trim(); - String httpPassword = m_prefs.getString("http_password", "").trim(); - - if (httpLogin.length() > 0) { - if (m_transportDebugging) Log.d(TAG, "Using HTTP Basic authentication."); - - URL targetUrl; - try { - targetUrl = new URL(m_api); - } catch (MalformedURLException e) { - m_lastError = ApiError.INVALID_URL; - e.printStackTrace(); - client.close(); - return null; - } - - HttpHost targetHost = new HttpHost(targetUrl.getHost(), targetUrl.getPort(), targetUrl.getProtocol()); - CredentialsProvider cp = new BasicCredentialsProvider(); - context = new BasicHttpContext(); - - cp.setCredentials( - new AuthScope(targetHost.getHostName(), targetHost.getPort()), - new UsernamePasswordCredentials(httpLogin, httpPassword)); - - context.setAttribute(ClientContext.CREDS_PROVIDER, cp); - } - - httpPost.setEntity(new StringEntity(requestStr, "utf-8")); - HttpResponse execute = client.execute(httpPost, context); - - m_httpStatusCode = execute.getStatusLine().getStatusCode(); - - switch (m_httpStatusCode) { - case 200: - InputStream content = execute.getEntity().getContent(); - - BufferedReader buffer = new BufferedReader( - new InputStreamReader(content), 8192); - - String s = ""; - String response = ""; - - while ((s = buffer.readLine()) != null) { - response += s; - } - - if (m_transportDebugging) Log.d(TAG, "<<< " + response); - - JsonParser parser = new JsonParser(); - - JsonElement result = parser.parse(response); - JsonObject resultObj = result.getAsJsonObject(); - - m_apiStatusCode = resultObj.get("status").getAsInt(); - - client.close(); - - switch (m_apiStatusCode) { - case API_STATUS_OK: - return result.getAsJsonObject().get("content"); - case API_STATUS_ERR: - JsonObject contentObj = resultObj.get("content").getAsJsonObject(); - String error = contentObj.get("error").getAsString(); - - if (error.equals("LOGIN_ERROR")) { - m_lastError = ApiError.LOGIN_FAILED; - } else if (error.equals("API_DISABLED")) { - m_lastError = ApiError.API_DISABLED; - } else if (error.equals("NOT_LOGGED_IN")) { - m_lastError = ApiError.LOGIN_FAILED; - } else if (error.equals("INCORRECT_USAGE")) { - m_lastError = ApiError.INCORRECT_USAGE; - } else { - Log.d(TAG, "Unknown API error: " + error); - m_lastError = ApiError.API_UNKNOWN; - } - } - - return null; - case 401: - m_lastError = ApiError.HTTP_UNAUTHORIZED; - break; - case 403: - m_lastError = ApiError.HTTP_FORBIDDEN; - break; - case 404: - m_lastError = ApiError.HTTP_NOT_FOUND; - break; - case 500: - m_lastError = ApiError.HTTP_SERVER_ERROR; - break; - default: - m_lastError = ApiError.HTTP_OTHER_ERROR; - break; - } - - client.close(); - return null; - } catch (javax.net.ssl.SSLPeerUnverifiedException e) { - m_lastError = ApiError.SSL_REJECTED; - e.printStackTrace(); - } catch (IOException e) { - m_lastError = ApiError.IO_ERROR; - e.printStackTrace(); - } catch (com.google.gson.JsonSyntaxException e) { - m_lastError = ApiError.PARSE_ERROR; - e.printStackTrace(); - } catch (Exception e) { - m_lastError = ApiError.OTHER_ERROR; - e.printStackTrace(); - } - - client.close(); - return null; - } -} diff --git a/src/org/fox/ttrss/util/HeadlinesRequest.java b/src/org/fox/ttrss/util/HeadlinesRequest.java new file mode 100644 index 00000000..49b8ec7c --- /dev/null +++ b/src/org/fox/ttrss/util/HeadlinesRequest.java @@ -0,0 +1,85 @@ +package org.fox.ttrss.util; + +import java.lang.reflect.Type; +import java.util.List; + +import org.fox.ttrss.ApiRequest; +import org.fox.ttrss.OnlineActivity; +import org.fox.ttrss.R; +import org.fox.ttrss.TinyApplication; +import org.fox.ttrss.ApiRequest.ApiError; +import org.fox.ttrss.types.Article; +import org.fox.ttrss.types.ArticleList; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +import android.content.Context; +import android.widget.Adapter; +import android.widget.ArrayAdapter; + +public class HeadlinesRequest extends ApiRequest { + public static final int HEADLINES_REQUEST_SIZE = 30; + public static final int HEADLINES_BUFFER_MAX = 500; + + private int m_offset = 0; + private OnlineActivity m_activity; + private ArticleList m_articles = TinyApplication.getInstance().m_loadedArticles; + + public HeadlinesRequest(Context context, OnlineActivity activity) { + super(context); + + m_activity = activity; + } + + protected void onPostExecute(JsonElement result) { + if (result != null) { + try { + JsonArray content = result.getAsJsonArray(); + if (content != null) { + Type listType = new TypeToken>() {}.getType(); + final List
articles = new Gson().fromJson(content, listType); + + while (m_articles.size() > HEADLINES_BUFFER_MAX) + m_articles.remove(0); + + if (m_offset == 0) + m_articles.clear(); + else + if (m_articles.get(m_articles.size()-1).id == -1) + m_articles.remove(m_articles.size()-1); // remove previous placeholder + + for (Article f : articles) + m_articles.add(f); + + if (articles.size() == HEADLINES_REQUEST_SIZE) { + Article placeholder = new Article(-1); + m_articles.add(placeholder); + } + + if (m_articles.size() == 0) + m_activity.setLoadingStatus(R.string.no_headlines_to_display, false); + else + m_activity.setLoadingStatus(R.string.blank, false); + + return; + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (m_lastError == ApiError.LOGIN_FAILED) { + m_activity.login(); + } else { + m_activity.setLoadingStatus(getErrorMessage(), false); + } + } + + public void setOffset(int skip) { + m_offset = skip; + } +} -- cgit v1.2.3-54-g00ecf From 4eac5b73bb4e3d91794a688d7e760e1f6e629c88 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Mon, 17 Sep 2012 16:28:32 +0400 Subject: implement core offline functions --- AndroidManifest.xml | 15 + src/org/fox/ttrss/ArticleEventListener.java | 9 - src/org/fox/ttrss/ArticleFragment.java | 8 +- src/org/fox/ttrss/FeedsActivity.java | 2 +- src/org/fox/ttrss/FeedsEventListener.java | 5 - src/org/fox/ttrss/HeadlinesActivity.java | 2 +- src/org/fox/ttrss/OnlineActivity.java | 309 ++++++++++- src/org/fox/ttrss/offline/OfflineActivity.java | 192 +++++++ .../ttrss/offline/OfflineArticleEventListener.java | 5 + .../fox/ttrss/offline/OfflineArticleFragment.java | 269 ++++++++++ src/org/fox/ttrss/offline/OfflineArticlePager.java | 150 ++++++ .../fox/ttrss/offline/OfflineDownloadService.java | 452 ++++++++++++++++ .../offline/OfflineFeedCategoriesFragment.java | 307 +++++++++++ .../fox/ttrss/offline/OfflineFeedsActivity.java | 261 +++++++++ .../fox/ttrss/offline/OfflineFeedsFragment.java | 331 ++++++++++++ .../offline/OfflineHeadlinesEventListener.java | 17 + .../ttrss/offline/OfflineHeadlinesFragment.java | 585 +++++++++++++++++++++ .../fox/ttrss/offline/OfflineUploadService.java | 264 ++++++++++ src/org/fox/ttrss/util/ImageCacheService.java | 208 ++++++++ 19 files changed, 3354 insertions(+), 37 deletions(-) delete mode 100644 src/org/fox/ttrss/ArticleEventListener.java delete mode 100644 src/org/fox/ttrss/FeedsEventListener.java create mode 100644 src/org/fox/ttrss/offline/OfflineActivity.java create mode 100644 src/org/fox/ttrss/offline/OfflineArticleEventListener.java create mode 100644 src/org/fox/ttrss/offline/OfflineArticleFragment.java create mode 100644 src/org/fox/ttrss/offline/OfflineArticlePager.java create mode 100644 src/org/fox/ttrss/offline/OfflineDownloadService.java create mode 100644 src/org/fox/ttrss/offline/OfflineFeedCategoriesFragment.java create mode 100644 src/org/fox/ttrss/offline/OfflineFeedsActivity.java create mode 100644 src/org/fox/ttrss/offline/OfflineFeedsFragment.java create mode 100644 src/org/fox/ttrss/offline/OfflineHeadlinesEventListener.java create mode 100644 src/org/fox/ttrss/offline/OfflineHeadlinesFragment.java create mode 100644 src/org/fox/ttrss/offline/OfflineUploadService.java create mode 100644 src/org/fox/ttrss/util/ImageCacheService.java (limited to 'src/org/fox/ttrss/util') diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1d48a3be..5b17ba4f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -47,6 +47,21 @@ android:name=".ArticleActivity" android:label="@string/app_name" > + + + + + + + + + 0; + } + } catch (IllegalStateException e) { + // db is closed? ugh } + + return false; + } + + private boolean hasOfflineData() { + try { + Cursor c = getReadableDb().query("articles", + new String[] { "COUNT(*)" }, null, null, null, null, null); + if (c.moveToFirst()) { + int modified = c.getInt(0); + c.close(); + + return modified > 0; + } + } catch (IllegalStateException e) { + // db is closed? + } + + return false; + } + + @Override + public void onPause() { + super.onPause(); + + unregisterReceiver(m_broadcastReceiver); + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + private void syncOfflineData() { + Log.d(TAG, "offlineSync: starting"); - Log.d(TAG, "m_sessionId=" + m_sessionId); - Log.d(TAG, "m_apiLevel=" + m_apiLevel); + Intent intent = new Intent( + OnlineActivity.this, + OfflineUploadService.class); - setContentView(R.layout.online); + intent.putExtra("sessionId", m_sessionId); + + startService(intent); + } + + private void cancelOfflineSync() { + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setMessage(R.string.dialog_offline_sync_in_progress) + .setNegativeButton(R.string.dialog_offline_sync_stop, + new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + + if (m_sessionId != null) { + Log.d(TAG, "offline: stopping"); + + m_offlineModeStatus = 0; + + Intent intent = new Intent( + OnlineActivity.this, + OfflineDownloadService.class); + + stopService(intent); + + dialog.dismiss(); + + restart(); + } + } + }) + .setPositiveButton(R.string.dialog_offline_sync_continue, + new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + + dialog.dismiss(); + + restart(); + } + }); + + AlertDialog dlg = builder.create(); + dlg.show(); } + + public void restart() { + Intent refresh = new Intent(OnlineActivity.this, OnlineActivity.class); + refresh.putExtra("sessionId", m_sessionId); + refresh.putExtra("apiLevel", m_apiLevel); + startActivity(refresh); + finish(); + } + + private void switchOfflineSuccess() { + logout(); + // setLoadingStatus(R.string.blank, false); + + SharedPreferences.Editor editor = m_prefs.edit(); + editor.putBoolean("offline_mode_active", true); + editor.commit(); + + Intent offline = new Intent(OnlineActivity.this, OfflineActivity.class); + offline.putExtra("initial", true); + offline.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + + startActivityForResult(offline, 0); + finish(); + + } + public void login() { if (m_prefs.getString("ttrss_url", "").trim().length() == 0) { @@ -186,6 +426,9 @@ public class OnlineActivity extends CommonActivity { intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); startActivityForResult(intent, 0); + + if (hasPendingOfflineData()) + syncOfflineData(); finish(); } @@ -203,7 +446,7 @@ public class OnlineActivity extends CommonActivity { login(); return true; case R.id.go_offline: - // FIXME go offline + switchOffline(); return true; case R.id.article_set_note: if (ap != null && ap.getSelectedArticle() != null) { @@ -538,6 +781,30 @@ public class OnlineActivity extends CommonActivity { protected void loginFailure() { m_sessionId = null; initMenu(); + + if (hasOfflineData()) { + + AlertDialog.Builder builder = new AlertDialog.Builder( + OnlineActivity.this) + .setMessage(R.string.dialog_offline_prompt) + .setPositiveButton(R.string.dialog_offline_go, + new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + switchOfflineSuccess(); + } + }) + .setNegativeButton(R.string.dialog_cancel, + new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + // + } + }); + + AlertDialog dlg = builder.create(); + dlg.show(); + } } public boolean getUnreadArticlesOnly() { @@ -556,12 +823,20 @@ public class OnlineActivity extends CommonActivity { out.putInt("apiLevel", m_apiLevel); out.putBoolean("unreadOnly", m_unreadOnly); out.putBoolean("unreadArticlesOnly", m_unreadArticlesOnly); + out.putInt("offlineModeStatus", m_offlineModeStatus); } @Override public void onResume() { super.onResume(); + IntentFilter filter = new IntentFilter(); + filter.addAction(OfflineDownloadService.INTENT_ACTION_SUCCESS); + filter.addAction(OfflineUploadService.INTENT_ACTION_SUCCESS); + filter.addCategory(Intent.CATEGORY_DEFAULT); + + registerReceiver(m_broadcastReceiver, filter); + if (m_sessionId == null) { login(); } else { diff --git a/src/org/fox/ttrss/offline/OfflineActivity.java b/src/org/fox/ttrss/offline/OfflineActivity.java new file mode 100644 index 00000000..7fb8c1a9 --- /dev/null +++ b/src/org/fox/ttrss/offline/OfflineActivity.java @@ -0,0 +1,192 @@ +package org.fox.ttrss.offline; + +import org.fox.ttrss.CommonActivity; +import org.fox.ttrss.R; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.BaseColumns; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; + +public class OfflineActivity extends CommonActivity { + private final String TAG = this.getClass().getSimpleName(); + + protected SharedPreferences m_prefs; + protected Menu m_menu; + protected boolean m_unreadOnly; + + @Override + public void onCreate(Bundle savedInstanceState) { + m_prefs = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + + if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) { + setTheme(R.style.DarkTheme); + } else { + setTheme(R.style.LightTheme); + } + + super.onCreate(savedInstanceState); + + setContentView(R.layout.online); + + setLoadingStatus(R.string.blank, false); + findViewById(R.id.loading_container).setVisibility(View.GONE); + + initMenu(); + + Intent intent = getIntent(); + + if (intent.getExtras() != null) { + if (intent.getBooleanExtra("initial", false)) { + intent = new Intent(OfflineActivity.this, OfflineFeedsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + + startActivityForResult(intent, 0); + finish(); + } + } + + if (savedInstanceState != null) { + m_unreadOnly = savedInstanceState.getBoolean("unreadOnly"); + } + } + + @Override + public void onSaveInstanceState(Bundle out) { + super.onSaveInstanceState(out); + + out.putBoolean("unreadOnly", m_unreadOnly); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.go_online: + switchOnline(); + return true; + default: + Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId()); + return super.onOptionsItemSelected(item); + } + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.offline_menu, menu); + + m_menu = menu; + + initMenu(); + + return true; + } + + public boolean getUnreadOnly() { + return m_unreadOnly; + } + + protected void initMenu() { + if (m_menu != null) { + m_menu.setGroupVisible(R.id.menu_group_headlines, false); + m_menu.setGroupVisible(R.id.menu_group_headlines_selection, false); + m_menu.setGroupVisible(R.id.menu_group_article, false); + m_menu.setGroupVisible(R.id.menu_group_feeds, false); + } + } + + private void switchOnline() { + SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = localPrefs.edit(); + editor.putBoolean("offline_mode_active", false); + editor.commit(); + + Intent refresh = new Intent(this, org.fox.ttrss.OnlineActivity.class); + startActivity(refresh); + finish(); + } + + protected Cursor getArticleById(int articleId) { + Cursor c = getReadableDb().query("articles", null, + BaseColumns._ID + "=?", + new String[] { String.valueOf(articleId) }, null, null, null); + + c.moveToFirst(); + + return c; + } + + protected Cursor getFeedById(int feedId) { + Cursor c = getReadableDb().query("feeds", null, + BaseColumns._ID + "=?", + new String[] { String.valueOf(feedId) }, null, null, null); + + c.moveToFirst(); + + return c; + } + + protected Cursor getCatById(int catId) { + Cursor c = getReadableDb().query("categories", null, + BaseColumns._ID + "=?", + new String[] { String.valueOf(catId) }, null, null, null); + + c.moveToFirst(); + + return c; + } + + protected Intent getShareIntent(Cursor article) { + String title = article.getString(article.getColumnIndex("title")); + String link = article.getString(article.getColumnIndex("link")); + + Intent intent = new Intent(Intent.ACTION_SEND); + + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT, title); + intent.putExtra(Intent.EXTRA_TEXT, link); + + return intent; + } + + protected void shareArticle(int articleId) { + + Cursor article = getArticleById(articleId); + + if (article != null) { + shareArticle(article); + article.close(); + } + } + + private void shareArticle(Cursor article) { + if (article != null) { + Intent intent = getShareIntent(article); + + startActivity(Intent.createChooser(intent, + getString(R.id.share_article))); + } + } + + protected int getSelectedArticleCount() { + Cursor c = getReadableDb().query("articles", + new String[] { "COUNT(*)" }, "selected = 1", null, null, null, + null); + c.moveToFirst(); + int selected = c.getInt(0); + c.close(); + + return selected; + } + +} diff --git a/src/org/fox/ttrss/offline/OfflineArticleEventListener.java b/src/org/fox/ttrss/offline/OfflineArticleEventListener.java new file mode 100644 index 00000000..35b9de28 --- /dev/null +++ b/src/org/fox/ttrss/offline/OfflineArticleEventListener.java @@ -0,0 +1,5 @@ +package org.fox.ttrss.offline; + +public interface OfflineArticleEventListener { + +} diff --git a/src/org/fox/ttrss/offline/OfflineArticleFragment.java b/src/org/fox/ttrss/offline/OfflineArticleFragment.java new file mode 100644 index 00000000..266c89b2 --- /dev/null +++ b/src/org/fox/ttrss/offline/OfflineArticleFragment.java @@ -0,0 +1,269 @@ +package org.fox.ttrss.offline; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.fox.ttrss.OnlineActivity; +import org.fox.ttrss.R; +import org.fox.ttrss.util.ImageCacheService; +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.SharedPreferences; +import android.database.Cursor; +import android.os.Bundle; +import android.preference.PreferenceManager; +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.util.TypedValue; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebSettings.LayoutAlgorithm; +import android.widget.TextView; +import android.widget.AdapterView.AdapterContextMenuInfo; + +public class OfflineArticleFragment extends Fragment { + @SuppressWarnings("unused") + private final String TAG = this.getClass().getSimpleName(); + + private SharedPreferences m_prefs; + private int m_articleId; + private boolean m_isCat = false; // FIXME use + private Cursor m_cursor; + private OfflineActivity m_activity; + + public OfflineArticleFragment() { + super(); + } + + public OfflineArticleFragment(int articleId) { + super(); + m_articleId = articleId; + } + + + @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) item + .getMenuInfo(); + + switch (item.getItemId()) { + case R.id.article_link_share: + m_activity.shareArticle(m_articleId); + return true; + case R.id.article_link_copy: + if (true) { + Cursor article = m_activity.getArticleById(m_articleId); + + if (article != null) { + m_activity.copyToClipboard(article.getString(article.getColumnIndex("link"))); + article.close(); + } + } + return true; + default: + Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId()); + return super.onContextItemSelected(item); + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + + getActivity().getMenuInflater().inflate(R.menu.article_link_context_menu, menu); + menu.setHeaderTitle(m_cursor.getString(m_cursor.getColumnIndex("title"))); + + super.onCreateContextMenu(menu, v, menuInfo); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + if (savedInstanceState != null) { + m_articleId = savedInstanceState.getInt("articleId"); + } + + View view = inflater.inflate(R.layout.article_fragment, container, false); + + m_cursor = m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")", + new String[] { "articles.*", "feeds.title AS feed_title" }, "articles." + BaseColumns._ID + "=?", + new String[] { String.valueOf(m_articleId) }, null, null, null); + + m_cursor.moveToFirst(); + + if (m_cursor.isFirst()) { + + TextView title = (TextView)view.findViewById(R.id.title); + + if (title != null) { + + String titleStr; + + if (m_cursor.getString(m_cursor.getColumnIndex("title")).length() > 200) + titleStr = m_cursor.getString(m_cursor.getColumnIndex("title")).substring(0, 200) + "..."; + else + titleStr = m_cursor.getString(m_cursor.getColumnIndex("title")); + + title.setMovementMethod(LinkMovementMethod.getInstance()); + title.setText(Html.fromHtml("" + titleStr + "")); + registerForContextMenu(title); + } + + WebView web = (WebView)view.findViewById(R.id.content); + + if (web != null) { + + String content; + String cssOverride = ""; + + WebSettings ws = web.getSettings(); + ws.setSupportZoom(true); + ws.setBuiltInZoomControls(true); + + web.getSettings().setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); + + TypedValue tv = new TypedValue(); + getActivity().getTheme().resolveAttribute(R.attr.linkColor, tv, true); + + // prevent flicker in ics + if (android.os.Build.VERSION.SDK_INT >= 11) { + web.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) { + cssOverride = "body { background : transparent; color : #e0e0e0}"; + //view.setBackgroundColor(android.R.color.black); + web.setBackgroundColor(getResources().getColor(android.R.color.transparent)); + } else { + cssOverride = ""; + } + + String hexColor = String.format("#%06X", (0xFFFFFF & tv.data)); + cssOverride += " a:link {color: "+hexColor+";} a:visited { color: "+hexColor+";}"; + + String articleContent = m_cursor.getString(m_cursor.getColumnIndex("content")); + Document doc = Jsoup.parse(articleContent); + + if (doc != null) { + if (m_prefs.getBoolean("offline_image_cache_enabled", false)) { + + 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)); + } + } + } + + // thanks webview for crashing on