summaryrefslogtreecommitdiff
path: root/orgfoxttrss/src/main/java/org
diff options
context:
space:
mode:
Diffstat (limited to 'orgfoxttrss/src/main/java/org')
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/ApiRequest.java351
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/ArticleFragment.java449
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/ArticlePager.java347
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/CommonActivity.java250
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/DashClock.java107
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/DummyFragment.java17
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java547
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/FeedsActivity.java509
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/FeedsFragment.java808
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/GlobalState.java63
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesActivity.java281
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java11
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java1179
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/LoadingFragment.java18
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/OnlineActivity.java1765
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/PreferencesActivity.java25
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineActivity.java881
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticleFragment.java439
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticlePager.java298
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineDownloadService.java500
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedCategoriesFragment.java337
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsActivity.java349
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsFragment.java372
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesActivity.java167
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesEventListener.java7
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesFragment.java774
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineUploadService.java286
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java57
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonShareActivity.java136
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java146
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java321
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerReceiver.java93
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerSettingsActivity.java96
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/Article.java115
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/ArticleList.java59
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/Attachment.java75
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/Feed.java90
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategory.java58
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategoryList.java43
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedList.java43
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/types/Label.java13
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/AppRater.java105
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/DatabaseHelper.java89
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/EnlargingImageView.java252
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/FontSizeDialogPreference.java224
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java101
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/ImageCacheService.java212
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/LessBrokenWebView.java37
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/NoChildFocusScrollView.java34
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/PrefsBackupAgent.java19
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/SimpleLoginManager.java105
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/TitleWebView.java91
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/util/TypefaceCache.java29
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/widget/SmallWidgetProvider.java65
-rw-r--r--orgfoxttrss/src/main/java/org/fox/ttrss/widget/WidgetUpdateService.java141
55 files changed, 13986 insertions, 0 deletions
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/ApiRequest.java b/orgfoxttrss/src/main/java/org/fox/ttrss/ApiRequest.java
new file mode 100644
index 00000000..65e97e8e
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/ApiRequest.java
@@ -0,0 +1,351 @@
+package org.fox.ttrss;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.util.Base64;
+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<HashMap<String,String>, 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, SSL_HOSTNAME_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED,
+ API_UNKNOWN, LOGIN_FAILED, INVALID_URL, API_INCORRECT_USAGE, NETWORK_UNAVAILABLE, API_UNKNOWN_METHOD };
+
+ public static final int API_STATUS_OK = 0;
+ public static final int API_STATUS_ERR = 1;
+
+ private String m_api;
+ private boolean m_transportDebugging = false;
+ protected int m_responseCode = 0;
+ protected String m_responseMessage;
+ protected int m_apiStatusCode = 0;
+ protected boolean m_canUseProgress = false;
+ 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", "").trim();
+ m_transportDebugging = m_prefs.getBoolean("transport_debugging", false);
+ m_lastError = ApiError.NO_ERROR;
+
+ }
+
+ @SuppressLint("NewApi")
+ @SuppressWarnings("unchecked")
+ public void execute(HashMap<String,String> map) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ super.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, map);
+ else
+ super.execute(map);
+ }
+
+ public 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 SSL_HOSTNAME_REJECTED:
+ return R.string.error_ssl_hostname_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 API_UNKNOWN_METHOD:
+ return R.string.error_api_unknown_method;
+ case LOGIN_FAILED:
+ return R.string.error_login_failed;
+ case INVALID_URL:
+ return R.string.error_invalid_api_url;
+ case API_INCORRECT_USAGE:
+ return R.string.error_api_incorrect_usage;
+ case NETWORK_UNAVAILABLE:
+ return R.string.error_network_unavailable;
+ default:
+ Log.d(TAG, "getErrorMessage: unknown error code=" + m_lastError);
+ return R.string.error_unknown;
+ }
+ }
+
+ @Override
+ protected JsonElement doInBackground(HashMap<String, String>... params) {
+
+ if (!isNetworkAvailable()) {
+ m_lastError = ApiError.NETWORK_UNAVAILABLE;
+ return null;
+ }
+
+ Gson gson = new Gson();
+
+ String requestStr = gson.toJson(new HashMap<String,String>(params[0]));
+ byte[] postData = null;
+
+ try {
+ postData = requestStr.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ m_lastError = ApiError.OTHER_ERROR;
+ e.printStackTrace();
+ return null;
+ }
+
+ /* disableConnectionReuseIfNecessary(); */
+
+ if (m_transportDebugging) Log.d(TAG, ">>> (" + requestStr + ") " + m_api);
+
+ /* ApiRequest.trustAllHosts(m_prefs.getBoolean("ssl_trust_any", false),
+ m_prefs.getBoolean("ssl_trust_any_host", false)); */
+
+ URL url;
+
+ try {
+ url = new URL(m_api + "/api/");
+ } catch (Exception e) {
+ m_lastError = ApiError.INVALID_URL;
+ e.printStackTrace();
+ return null;
+ }
+
+ try {
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+ 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.");
+
+ conn.setRequestProperty("Authorization", "Basic " +
+ Base64.encodeToString((httpLogin + ":" + httpPassword).getBytes("UTF-8"), Base64.NO_WRAP));
+ }
+
+ conn.setDoInput(true);
+ conn.setDoOutput(true);
+ conn.setUseCaches(false);
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Length", Integer.toString(postData.length));
+
+ OutputStream out = conn.getOutputStream();
+ out.write(postData);
+ out.close();
+
+ m_responseCode = conn.getResponseCode();
+ m_responseMessage = conn.getResponseMessage();
+
+ switch (m_responseCode) {
+ case HttpURLConnection.HTTP_OK:
+ StringBuffer response = new StringBuffer();
+ InputStreamReader in = new InputStreamReader(conn.getInputStream(), "UTF-8");
+ char[] buf = new char[256];
+ int read = 0;
+ int total = 0;
+
+ int contentLength = conn.getHeaderFieldInt("Api-Content-Length", -1);
+
+ m_canUseProgress = (contentLength != -1);
+
+ while ((read = in.read(buf)) >= 0) {
+ response.append(buf, 0, read);
+ total += read;
+ publishProgress(Integer.valueOf(total), Integer.valueOf(contentLength));
+ }
+
+ if (m_transportDebugging) Log.d(TAG, "<<< " + response);
+
+ JsonParser parser = new JsonParser();
+
+ JsonElement result = parser.parse(response.toString());
+ JsonObject resultObj = result.getAsJsonObject();
+
+ m_apiStatusCode = resultObj.get("status").getAsInt();
+
+ conn.disconnect();
+
+ 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.API_INCORRECT_USAGE;
+ } else if (error.equals("UNKNOWN_METHOD")) {
+ m_lastError = ApiError.API_UNKNOWN_METHOD;
+ } else {
+ Log.d(TAG, "Unknown API error: " + error);
+ m_lastError = ApiError.API_UNKNOWN;
+ }
+ }
+
+ return null;
+ case HttpURLConnection.HTTP_UNAUTHORIZED:
+ m_lastError = ApiError.HTTP_UNAUTHORIZED;
+ break;
+ case HttpURLConnection.HTTP_FORBIDDEN:
+ m_lastError = ApiError.HTTP_FORBIDDEN;
+ break;
+ case HttpURLConnection.HTTP_NOT_FOUND:
+ m_lastError = ApiError.HTTP_NOT_FOUND;
+ break;
+ case HttpURLConnection.HTTP_INTERNAL_ERROR:
+ m_lastError = ApiError.HTTP_SERVER_ERROR;
+ break;
+ default:
+ Log.d(TAG, "HTTP response code: " + m_responseCode + "(" + m_responseMessage + ")");
+ m_lastError = ApiError.HTTP_OTHER_ERROR;
+ break;
+ }
+
+ conn.disconnect();
+ return null;
+ } catch (javax.net.ssl.SSLPeerUnverifiedException e) {
+ m_lastError = ApiError.SSL_REJECTED;
+ e.printStackTrace();
+ } catch (IOException e) {
+ m_lastError = ApiError.IO_ERROR;
+
+ if (e.getMessage() != null) {
+ if (e.getMessage().matches("Hostname [^ ]+ was not verified")) {
+ m_lastError = ApiError.SSL_HOSTNAME_REJECTED;
+ }
+ }
+
+ 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();
+ }
+
+ return null;
+ }
+
+ protected static void trustAllHosts(boolean trustAnyCert, boolean trustAnyHost) {
+ try {
+ if (trustAnyCert) {
+ X509TrustManager easyTrustManager = new X509TrustManager() {
+
+ public void checkClientTrusted(
+ X509Certificate[] chain,
+ String authType) throws CertificateException {
+ // Oh, I am easy!
+ }
+
+ public void checkServerTrusted(
+ X509Certificate[] chain,
+ String authType) throws CertificateException {
+ // Oh, I am easy!
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ };
+
+ // Create a trust manager that does not validate certificate chains
+ TrustManager[] trustAllCerts = new TrustManager[] {easyTrustManager};
+
+ // Install the all-trusting trust manager
+
+ SSLContext sc = SSLContext.getInstance("TLS");
+
+ sc.init(null, trustAllCerts, new java.security.SecureRandom());
+
+ HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ }
+
+ if (trustAnyHost) {
+ HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ });
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ protected static void disableConnectionReuseIfNecessary() {
+ // HTTP connection reuse which was buggy pre-froyo
+ if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
+ System.setProperty("http.keepAlive", "false");
+ }
+ }
+
+ protected boolean isNetworkAvailable() {
+ ConnectivityManager cm = (ConnectivityManager)
+ m_context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+
+ // if no network is available networkInfo will be null
+ // otherwise check if we are connected
+ if (networkInfo != null && networkInfo.isConnected()) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/ArticleFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/ArticleFragment.java
new file mode 100644
index 00000000..4a568d3c
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/ArticleFragment.java
@@ -0,0 +1,449 @@
+package org.fox.ttrss;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.Attachment;
+import org.fox.ttrss.util.TypefaceCache;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.Fragment;
+import android.text.Html;
+import android.util.TypedValue;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebView.HitTestResult;
+import android.widget.TextView;
+
+public class ArticleFragment extends Fragment {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private SharedPreferences m_prefs;
+ private Article m_article;
+ private OnlineActivity m_activity;
+
+ public void initialize(Article article) {
+ m_article = article;
+ }
+
+ private View.OnTouchListener m_gestureListener;
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+
+ if (v.getId() == R.id.content) {
+ HitTestResult result = ((WebView)v).getHitTestResult();
+
+ if (result != null && (result.getType() == HitTestResult.IMAGE_TYPE || result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE)) {
+ menu.setHeaderTitle(result.getExtra());
+ getActivity().getMenuInflater().inflate(R.menu.article_content_img_context_menu, menu);
+
+ /* FIXME I have no idea how to do this correctly ;( */
+
+ m_activity.setLastContentImageHitTestUrl(result.getExtra());
+
+ } else {
+ menu.setHeaderTitle(m_article.title);
+ getActivity().getMenuInflater().inflate(R.menu.article_link_context_menu, menu);
+ }
+ } else {
+ menu.setHeaderTitle(m_article.title);
+ getActivity().getMenuInflater().inflate(R.menu.article_link_context_menu, menu);
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ m_activity.setProgressBarVisibility(true);
+
+ if (savedInstanceState != null) {
+ m_article = savedInstanceState.getParcelable("article");
+ }
+
+ boolean useTitleWebView = m_prefs.getBoolean("article_compat_view", false);
+
+ View view = inflater.inflate(useTitleWebView ? R.layout.article_fragment_compat : R.layout.article_fragment, container, false);
+
+ if (m_article != null) {
+
+ if (!useTitleWebView) {
+ View scroll = view.findViewById(R.id.article_scrollview);
+
+ if (scroll != null) {
+ final float scale = getResources().getDisplayMetrics().density;
+
+ if (m_activity.isSmallScreen()) {
+ scroll.setPadding((int)(8 * scale + 0.5f),
+ (int)(5 * scale + 0.5f),
+ (int)(8 * scale + 0.5f),
+ 0);
+ } else {
+ scroll.setPadding((int)(25 * scale + 0.5f),
+ (int)(10 * scale + 0.5f),
+ (int)(25 * scale + 0.5f),
+ 0);
+
+ }
+
+ }
+ }
+
+ int articleFontSize = Integer.parseInt(m_prefs.getString("article_font_size_sp", "16"));
+ int articleSmallFontSize = Math.max(10, Math.min(18, articleFontSize - 2));
+
+ TextView title = (TextView)view.findViewById(R.id.title);
+
+ if (title != null) {
+
+ if (m_prefs.getBoolean("enable_condensed_fonts", false)) {
+ Typeface tf = TypefaceCache.get(m_activity, "sans-serif-condensed", Typeface.NORMAL);
+
+ if (tf != null && !tf.equals(title.getTypeface())) {
+ title.setTypeface(tf);
+ }
+
+ title.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, articleFontSize + 5));
+ } else {
+ title.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, articleFontSize + 3));
+ }
+
+ String titleStr;
+
+ if (m_article.title.length() > 200)
+ titleStr = m_article.title.substring(0, 200) + "...";
+ else
+ titleStr = m_article.title;
+
+ title.setText(Html.fromHtml(titleStr));
+ //title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
+ title.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ URL url = new URL(m_article.link.trim());
+ String uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(),
+ url.getPort(), url.getPath(), url.getQuery(), url.getRef()).toString();
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ m_activity.toast(R.string.error_other_error);
+ }
+ }
+ });
+
+ registerForContextMenu(title);
+ }
+
+ TextView comments = (TextView)view.findViewById(R.id.comments);
+
+ if (comments != null) {
+ if (m_activity.getApiLevel() >= 4 && m_article.comments_count > 0) {
+ comments.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+
+ String commentsTitle = getString(R.string.article_comments, m_article.comments_count);
+ comments.setText(commentsTitle);
+ //comments.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
+ comments.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ URL url = new URL((m_article.comments_link != null && m_article.comments_link.length() > 0) ?
+ m_article.comments_link : m_article.link);
+ String uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(),
+ url.getPort(), url.getPath(), url.getQuery(), url.getRef()).toString();
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(uri));
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ m_activity.toast(R.string.error_other_error);
+ }
+ }
+ });
+
+ } else {
+ comments.setVisibility(View.GONE);
+ }
+ }
+
+ TextView note = (TextView)view.findViewById(R.id.note);
+
+ if (note != null) {
+ if (m_article.note != null && !"".equals(m_article.note)) {
+ note.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+ note.setText(m_article.note);
+ } else {
+ note.setVisibility(View.GONE);
+ }
+
+ }
+
+ final WebView web = (WebView)view.findViewById(R.id.content);
+
+ if (web != null) {
+
+ web.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ HitTestResult result = ((WebView)v).getHitTestResult();
+
+ if (result != null && (result.getType() == HitTestResult.IMAGE_TYPE || result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE)) {
+ registerForContextMenu(web);
+ m_activity.openContextMenu(web);
+ unregisterForContextMenu(web);
+ return true;
+ } else {
+ if (m_activity.isCompatMode()) {
+ KeyEvent shiftPressEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0);
+ shiftPressEvent.dispatch(web);
+ }
+
+ return false;
+ }
+ }
+ });
+
+ // prevent flicker in ics
+ if (!m_prefs.getBoolean("webview_hardware_accel", true) || useTitleWebView) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ web.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ }
+
+ web.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onProgressChanged(WebView view, int progress) {
+ m_activity.setProgress(Math.round(((float)progress / 100f) * 10000));
+ if (progress == 100) {
+ m_activity.setProgressBarVisibility(false);
+ }
+ }
+ });
+
+ String content;
+ String cssOverride = "";
+
+ WebSettings ws = web.getSettings();
+ ws.setSupportZoom(false);
+
+ TypedValue tv = new TypedValue();
+ getActivity().getTheme().resolveAttribute(R.attr.linkColor, tv, true);
+
+ String theme = m_prefs.getString("theme", CommonActivity.THEME_DEFAULT);
+
+ if (CommonActivity.THEME_HOLO.equals(theme)) {
+ cssOverride = "body { background : transparent; color : #e0e0e0}";
+ } else if (CommonActivity.THEME_DARK.equals(theme)) {
+ cssOverride = "body { background : transparent; color : #e0e0e0}";
+ } else {
+ cssOverride = "body { background : transparent; }";
+ }
+
+ if (useTitleWebView || android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
+ web.setBackgroundColor(Color.TRANSPARENT);
+ } else {
+ // seriously?
+ web.setBackgroundColor(Color.argb(1, 0, 0, 0));
+ }
+
+ String hexColor = String.format("#%06X", (0xFFFFFF & tv.data));
+ cssOverride += " a:link {color: "+hexColor+";} a:visited { color: "+hexColor+";}";
+
+ cssOverride += " table { width : 100%; }";
+
+ String articleContent = m_article.content != null ? m_article.content : "";
+
+ Document doc = Jsoup.parse(articleContent);
+
+ if (doc != null) {
+ // thanks webview for crashing on <video> tag
+ Elements videos = doc.select("video");
+
+ for (Element video : videos)
+ video.remove();
+
+ articleContent = doc.toString();
+ }
+
+ if (m_prefs.getBoolean("justify_article_text", true)) {
+ cssOverride += "body { text-align : justify; } ";
+ }
+
+ ws.setDefaultFontSize(articleFontSize);
+
+ content =
+ "<html>" +
+ "<head>" +
+ "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">" +
+ "<meta name=\"viewport\" content=\"width=device-width, user-scalable=no\" />" +
+ "<style type=\"text/css\">" +
+ "body { padding : 0px; margin : 0px; line-height : 130%; }" +
+ "img { max-width : 100%; width : auto; height : auto; }" +
+ cssOverride +
+ "</style>" +
+ "</head>" +
+ "<body>" + articleContent;
+
+ if (useTitleWebView) {
+ content += "<p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p>";
+ }
+
+ if (m_article.attachments != null && m_article.attachments.size() != 0) {
+ String flatContent = articleContent.replaceAll("[\r\n]", "");
+ boolean hasImages = flatContent.matches(".*?<img[^>+].*?");
+
+ for (Attachment a : m_article.attachments) {
+ if (a.content_type != null && a.content_url != null) {
+ try {
+ if (a.content_type.indexOf("image") != -1 &&
+ (!hasImages || m_article.always_display_attachments)) {
+
+ URL url = new URL(a.content_url.trim());
+ String strUrl = url.toString().trim();
+
+ content += "<p><img src=\"" + strUrl.replace("\"", "\\\"") + "\"></p>";
+ }
+
+ } catch (MalformedURLException e) {
+ //
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ content += "</body></html>";
+
+ try {
+ String baseUrl = null;
+
+ try {
+ URL url = new URL(m_article.link);
+ baseUrl = url.getProtocol() + "://" + url.getHost();
+ } catch (MalformedURLException e) {
+ //
+ }
+
+ web.loadDataWithBaseURL(baseUrl, content, "text/html", "utf-8", null);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ }
+
+ if (m_activity.isSmallScreen())
+ web.setOnTouchListener(m_gestureListener);
+
+ web.setVisibility(View.VISIBLE);
+ }
+
+ TextView dv = (TextView)view.findViewById(R.id.date);
+
+ if (dv != null) {
+ dv.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+
+ Date d = new Date(m_article.updated * 1000L);
+ DateFormat df = new SimpleDateFormat("MMM dd, HH:mm");
+ dv.setText(df.format(d));
+ }
+
+ TextView author = (TextView)view.findViewById(R.id.author);
+
+ boolean hasAuthor = false;
+
+ if (author != null) {
+ author.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+
+ if (m_article.author != null && m_article.author.length() > 0) {
+ author.setText(getString(R.string.author_formatted, m_article.author));
+ } else {
+ author.setVisibility(View.GONE);
+ }
+ hasAuthor = true;
+ }
+
+ TextView tagv = (TextView)view.findViewById(R.id.tags);
+
+ if (tagv != null) {
+ tagv.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+
+ if (m_article.feed_title != null) {
+ String fTitle = m_article.feed_title;
+
+ if (!hasAuthor && m_article.author != null && m_article.author.length() > 0) {
+ fTitle += " (" + getString(R.string.author_formatted, m_article.author) + ")";
+ }
+
+ tagv.setText(fTitle);
+ } else if (m_article.tags != null) {
+ String tagsStr = "";
+
+ for (String tag : m_article.tags)
+ tagsStr += tag + ", ";
+
+ tagsStr = tagsStr.replaceAll(", $", "");
+
+ tagv.setText(tagsStr);
+ } else {
+ tagv.setVisibility(View.GONE);
+ }
+ }
+
+ }
+
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.setClassLoader(getClass().getClassLoader());
+ out.putParcelable("article", m_article);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ m_activity = (OnlineActivity)activity;
+ //m_article = m_onlineServices.getSelectedArticle();
+
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/ArticlePager.java b/orgfoxttrss/src/main/java/org/fox/ttrss/ArticlePager.java
new file mode 100644
index 00000000..ee940e79
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/ArticlePager.java
@@ -0,0 +1,347 @@
+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 android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.os.BadParcelableException;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.ClassloaderWorkaroundFragmentStatePagerAdapter;
+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;
+import android.view.WindowManager;
+
+import com.google.gson.JsonElement;
+import com.viewpagerindicator.UnderlinePageIndicator;
+
+public class ArticlePager extends Fragment {
+
+ private final String TAG = "ArticlePager";
+ private PagerAdapter m_adapter;
+ private HeadlinesEventListener m_listener;
+ private Article m_article;
+ private ArticleList m_articles = GlobalState.getInstance().m_loadedArticles;
+ private OnlineActivity m_activity;
+ private String m_searchQuery = "";
+ private Feed m_feed;
+ private SharedPreferences m_prefs;
+
+ private class PagerAdapter extends ClassloaderWorkaroundFragmentStatePagerAdapter {
+
+ public PagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ Article article = m_articles.get(position);
+
+ if (article != null) {
+ ArticleFragment af = new ArticleFragment();
+ af.initialize(article);
+
+ if (m_prefs.getBoolean("dim_status_bar", false) && getView() != null && !m_activity.isCompatMode()) {
+ getView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ }
+
+ return af;
+ }
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return m_articles.size();
+ }
+
+ }
+
+ public void initialize(Article article, Feed feed) {
+ m_article = article;
+ m_feed = feed;
+ }
+
+ public void setSearchQuery(String searchQuery) {
+ m_searchQuery = searchQuery;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.article_pager, container, false);
+
+ if (savedInstanceState != null) {
+ m_article = savedInstanceState.getParcelable("article");
+ m_feed = savedInstanceState.getParcelable("feed");
+ }
+
+ m_adapter = new PagerAdapter(getActivity().getSupportFragmentManager());
+
+ ViewPager pager = (ViewPager) view.findViewById(R.id.article_pager);
+
+ int position = m_articles.indexOf(m_article);
+
+ m_listener.onArticleSelected(m_article, false);
+
+ m_activity.setProgressBarVisibility(true);
+
+ pager.setAdapter(m_adapter);
+
+ UnderlinePageIndicator indicator = (UnderlinePageIndicator)view.findViewById(R.id.article_titles);
+ indicator.setViewPager(pager);
+
+ pager.setCurrentItem(position);
+
+ indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+
+ @Override
+ public void onPageScrollStateChanged(int arg0) {
+ }
+
+ @Override
+ public void onPageScrolled(int arg0, float arg1, int arg2) {
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ Article article = m_articles.get(position);
+
+ if (article != null) {
+ m_article = article;
+
+ /* if (article.unread) {
+ article.unread = false;
+ m_activity.saveArticleUnread(article);
+ } */
+
+ m_listener.onArticleSelected(article, false);
+
+ //Log.d(TAG, "Page #" + position + "/" + m_adapter.getCount());
+
+ if ((m_activity.isSmallScreen() || m_activity.isPortrait()) && position == m_adapter.getCount() - 15) {
+ Log.d(TAG, "loading more articles...");
+ refresh(true);
+ }
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @SuppressWarnings({ "serial" })
+ protected void refresh(boolean append) {
+ m_activity.setLoadingStatus(R.string.blank, true);
+
+ m_activity.setProgressBarVisibility(true);
+ //m_activity.m_pullToRefreshAttacher.setRefreshing(true);
+
+ if (!m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
+ append = false;
+ }
+
+ HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity, m_feed) {
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ m_activity.setProgress(progress[0] / progress[1] * 10000);
+ }
+
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (isDetached()) return;
+
+ m_activity.setProgressBarVisibility(false);
+ //m_activity.m_pullToRefreshAttacher.setRefreshComplete();
+
+ super.onPostExecute(result);
+
+ if (result != null) {
+ try {
+ m_adapter.notifyDataSetChanged();
+ } catch (BadParcelableException e) {
+ if (getActivity() != null) {
+ getActivity().finish();
+ return;
+ }
+ }
+
+ if (m_article != null) {
+ if (m_article.id == 0 || m_articles.indexOf(m_article) == -1) {
+ if (m_articles.size() > 0) {
+ m_article = m_articles.get(0);
+ m_listener.onArticleSelected(m_article, false);
+ }
+ }
+ }
+
+ } else {
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ m_activity.login(true);
+ } else {
+ m_activity.toast(getErrorMessage());
+ //setLoadingStatus(getErrorMessage(), false);
+ }
+ }
+ }
+ };
+
+ final Feed feed = m_feed;
+
+ final String sessionId = m_activity.getSessionId();
+ int skip = 0;
+
+ if (append) {
+ // adaptive, all_articles, marked, published, unread
+ String viewMode = m_activity.getViewMode();
+ int numUnread = 0;
+ int numAll = m_articles.size();
+
+ for (Article a : m_articles) {
+ if (a.unread) ++numUnread;
+ }
+
+ if ("marked".equals(viewMode)) {
+ skip = numAll;
+ } else if ("published".equals(viewMode)) {
+ skip = numAll;
+ } else if ("unread".equals(viewMode)) {
+ skip = numUnread;
+ } else if (m_searchQuery != null && m_searchQuery.length() > 0) {
+ skip = numAll;
+ } else if ("adaptive".equals(viewMode)) {
+ skip = numUnread > 0 ? numUnread : numAll;
+ } else {
+ skip = numAll;
+ }
+ }
+
+ final int fskip = skip;
+
+ req.setOffset(skip);
+
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ 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", m_activity.getViewMode());
+ put("skip", String.valueOf(fskip));
+ put("include_nested", "true");
+ put("order_by", m_prefs.getBoolean("oldest_first", false) ? "date_reverse" : "");
+
+ 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);
+
+ out.setClassLoader(getClass().getClassLoader());
+ out.putParcelable("article", m_article);
+ out.putParcelable("feed", m_feed);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_listener = (HeadlinesEventListener)activity;
+ m_activity = (OnlineActivity)activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (m_articles.size() == 0 || !m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
+ refresh(false);
+ GlobalState.getInstance().m_activeFeed = m_feed;
+ }
+
+ m_activity.initMenu();
+
+ if (!m_activity.isCompatMode() && m_prefs.getBoolean("dim_status_bar", false)) {
+ getView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ }
+
+ if (m_prefs.getBoolean("full_screen_mode", false)) {
+ m_activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ /* if (!m_activity.isCompatMode()) {
+ m_activity.getSupportActionBar().hide();
+ } */
+ }
+ }
+
+ public Article getSelectedArticle() {
+ return m_article;
+ }
+
+ public void setActiveArticle(Article article) {
+ if (m_article != article) {
+ m_article = article;
+
+ int position = m_articles.indexOf(m_article);
+
+ ViewPager pager = (ViewPager) getView().findViewById(R.id.article_pager);
+
+ pager.setCurrentItem(position);
+ }
+ }
+
+ public void selectArticle(boolean next) {
+ if (m_article != null) {
+ int position = m_articles.indexOf(m_article);
+
+ if (next)
+ position++;
+ else
+ position--;
+
+ try {
+ Article tmp = m_articles.get(position);
+
+ if (tmp != null) {
+ setActiveArticle(tmp);
+ }
+
+ } catch (IndexOutOfBoundsException e) {
+ // do nothing
+ }
+ }
+ }
+
+ public void notifyUpdated() {
+ m_adapter.notifyDataSetChanged();
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/CommonActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/CommonActivity.java
new file mode 100644
index 00000000..5a64ae57
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/CommonActivity.java
@@ -0,0 +1,250 @@
+package org.fox.ttrss;
+
+
+import java.io.File;
+import java.io.IOException;
+
+import org.fox.ttrss.util.DatabaseHelper;
+
+import com.nostra13.universalimageloader.core.ImageLoader;
+import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
+import com.nostra13.universalimageloader.utils.StorageUtils;
+import com.readystatesoftware.systembartint.SystemBarTintManager;
+
+import android.annotation.SuppressLint;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.net.http.HttpResponseCache;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class CommonActivity extends ActionBarActivity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ public final static String FRAG_HEADLINES = "headlines";
+ public final static String FRAG_ARTICLE = "article";
+ public final static String FRAG_FEEDS = "feeds";
+ public final static String FRAG_CATS = "cats";
+
+ public final static String THEME_DARK = "THEME_DARK";
+ public final static String THEME_LIGHT = "THEME_LIGHT";
+ public final static String THEME_SEPIA = "THEME_SEPIA";
+ public final static String THEME_HOLO = "THEME_HOLO";
+ public final static String THEME_DEFAULT = CommonActivity.THEME_LIGHT;
+
+ public static final int EXCERPT_MAX_SIZE = 200;
+
+ private SQLiteDatabase m_readableDb;
+ private SQLiteDatabase m_writableDb;
+
+ private boolean m_smallScreenMode = true;
+ private boolean m_compatMode = false;
+ private String m_theme;
+
+ protected SharedPreferences m_prefs;
+
+ /* protected void enableHttpCaching() {
+ // enable resource caching
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ try {
+ File httpCacheDir = new File(getApplicationContext().getCacheDir(), "http");
+ long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
+ HttpResponseCache.install(httpCacheDir, httpCacheSize);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } */
+
+ protected void setSmallScreen(boolean smallScreen) {
+ Log.d(TAG, "m_smallScreenMode=" + smallScreen);
+ m_smallScreenMode = smallScreen;
+ }
+
+ public boolean getUnreadOnly() {
+ return m_prefs.getBoolean("show_unread_only", true);
+ }
+
+ public void setUnreadOnly(boolean unread) {
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putBoolean("show_unread_only", unread);
+ editor.commit();
+ }
+
+ public void setLoadingStatus(int status, boolean showProgress) {
+ TextView tv = (TextView) findViewById(R.id.loading_message);
+
+ if (tv != null) {
+ tv.setText(status);
+ }
+
+ findViewById(R.id.loading_container).setVisibility(status == R.string.blank ? View.GONE : View.VISIBLE);
+
+ setProgressBarIndeterminateVisibility(showProgress);
+ }
+
+ public void toast(int msgId) {
+ Toast toast = Toast.makeText(CommonActivity.this, msgId, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ public void toast(String msg) {
+ Toast toast = Toast.makeText(CommonActivity.this, msg, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ private void initDatabase() {
+ DatabaseHelper dh = new DatabaseHelper(getApplicationContext());
+
+ m_writableDb = dh.getWritableDatabase();
+ m_readableDb = dh.getReadableDatabase();
+ }
+
+ public synchronized SQLiteDatabase getReadableDb() {
+ return m_readableDb;
+ }
+
+ public synchronized SQLiteDatabase getWritableDb() {
+ return m_writableDb;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!m_theme.equals(m_prefs.getString("theme", CommonActivity.THEME_DEFAULT))) {
+ Log.d(TAG, "theme changed, restarting");
+
+ finish();
+ startActivity(getIntent());
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ m_readableDb.close();
+ m_writableDb.close();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ if (savedInstanceState != null) {
+ m_theme = savedInstanceState.getString("theme");
+ } else {
+ m_theme = m_prefs.getString("theme", CommonActivity.THEME_DEFAULT);
+ }
+
+ initDatabase();
+
+ m_compatMode = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB;
+
+ Log.d(TAG, "m_compatMode=" + m_compatMode);
+
+ super.onCreate(savedInstanceState);
+ }
+
+ public void setStatusBarTint() {
+ if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.KITKAT) {
+ SystemBarTintManager tintManager = new SystemBarTintManager(this);
+ // enable status bar tint
+ tintManager.setStatusBarTintEnabled(true);
+ // enable navigation bar tint
+ tintManager.setNavigationBarTintEnabled(true);
+
+ TypedValue tv = new TypedValue();
+ getTheme().resolveAttribute(R.attr.statusBarHintColor, tv, true);
+
+ tintManager.setStatusBarTintColor(tv.data);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putString("theme", m_theme);
+ }
+
+ public boolean isSmallScreen() {
+ return m_smallScreenMode;
+ }
+
+ public boolean isCompatMode() {
+ return m_compatMode;
+ }
+
+ @SuppressWarnings("deprecation")
+ public boolean isPortrait() {
+ Display display = getWindowManager().getDefaultDisplay();
+
+ int width = display.getWidth();
+ int height = display.getHeight();
+
+ return width < height;
+ }
+
+ @SuppressLint({ "NewApi", "ServiceCast" })
+ @SuppressWarnings("deprecation")
+ public void copyToClipboard(String str) {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
+ android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ clipboard.setText(str);
+ } else {
+ android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ clipboard.setText(str);
+ }
+
+ Toast toast = Toast.makeText(this, R.string.text_copied_to_clipboard, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ public boolean isDarkTheme() {
+ String theme = m_prefs.getString("theme", THEME_DEFAULT);
+
+ return theme.equals(THEME_DARK) || theme.equals(THEME_HOLO);
+ }
+
+ protected void setAppTheme(SharedPreferences prefs) {
+ String theme = prefs.getString("theme", CommonActivity.THEME_DEFAULT);
+
+ if (theme.equals(THEME_DARK)) {
+ setTheme(R.style.DarkTheme);
+ } else if (theme.equals(THEME_SEPIA)) {
+ setTheme(R.style.SepiaTheme);
+ } else if (theme.equals(THEME_HOLO)) {
+ setTheme(R.style.HoloTheme);
+ } else {
+ setTheme(R.style.LightTheme);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ protected int getScreenWidthInPixel() {
+ Display display = getWindowManager().getDefaultDisplay();
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) {
+ Point size = new Point();
+ display.getSize(size);
+ int width = size.x;
+ return width;
+ } else {
+ return display.getWidth();
+ }
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/DashClock.java b/orgfoxttrss/src/main/java/org/fox/ttrss/DashClock.java
new file mode 100644
index 00000000..b3491972
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/DashClock.java
@@ -0,0 +1,107 @@
+package org.fox.ttrss;
+
+import java.util.HashMap;
+
+import org.fox.ttrss.util.SimpleLoginManager;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.view.View;
+
+import com.google.android.apps.dashclock.api.DashClockExtension;
+import com.google.android.apps.dashclock.api.ExtensionData;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+public class DashClock extends DashClockExtension {
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ protected SharedPreferences m_prefs;
+
+ @Override
+ protected void onInitialize(boolean isReconnect) {
+ super.onInitialize(isReconnect);
+ setUpdateWhenScreenOn(true);
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ }
+
+ @Override
+ protected void onUpdateData(int reason) {
+
+ SimpleLoginManager loginManager = new SimpleLoginManager() {
+
+ @Override
+ protected void onLoginSuccess(int requestId, String sessionId, int apiLevel) {
+
+ ApiRequest aru = new ApiRequest(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ try {
+ JsonObject content = result.getAsJsonObject();
+
+ if (content != null) {
+ int unread = content.get("unread").getAsInt();
+
+ ExtensionData updatedData = null; // when null DashClock hides the widget
+
+ if (unread > 0) {
+ updatedData = new ExtensionData();
+ updatedData.visible(true);
+
+ updatedData.icon(R.drawable.dashclock);
+ updatedData.status(String.valueOf(unread));
+
+ updatedData.expandedTitle(getString(R.string.n_unread_articles, unread));
+ //updatedData.expandedBody(getString(R.string.app_name));
+
+ updatedData.clickIntent(new Intent().setClassName("org.fox.ttrss",
+ "org.fox.ttrss.OnlineActivity"));
+ }
+
+ publishUpdate(updatedData);
+
+ return;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+ };
+
+ final String fSessionId = sessionId;
+
+ HashMap<String, String> umap = new HashMap<String, String>() {
+ {
+ put("op", "getUnread");
+ put("sid", fSessionId);
+ }
+ };
+
+ aru.execute(umap);
+ }
+
+ @Override
+ protected void onLoginFailed(int requestId, ApiRequest ar) {
+
+ }
+
+ @Override
+ protected void onLoggingIn(int requestId) {
+
+
+ }
+ };
+
+ String login = m_prefs.getString("login", "").trim();
+ String password = m_prefs.getString("password", "").trim();
+
+ loginManager.logIn(getApplicationContext(), 1, login, password);
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/DummyFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/DummyFragment.java
new file mode 100644
index 00000000..7bf799a9
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/DummyFragment.java
@@ -0,0 +1,17 @@
+package org.fox.ttrss;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class DummyFragment extends Fragment {
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.dummy_fragment, container, false);
+
+ return view;
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java
new file mode 100644
index 00000000..4439a943
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java
@@ -0,0 +1,547 @@
+package org.fox.ttrss;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+import org.fox.ttrss.types.Feed;
+import org.fox.ttrss.types.FeedCategory;
+import org.fox.ttrss.types.FeedCategoryList;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+
+public class FeedCategoriesFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener {
+ private final String TAG = this.getClass().getSimpleName();
+ private SharedPreferences m_prefs;
+ private FeedCategoryListAdapter m_adapter;
+ private FeedCategoryList m_cats = new FeedCategoryList();
+ private FeedCategory m_selectedCat;
+ private FeedsActivity m_activity;
+ private SwipeRefreshLayout m_swipeLayout;
+
+ @SuppressLint("DefaultLocale")
+ class CatUnreadComparator implements Comparator<FeedCategory> {
+ @Override
+ public int compare(FeedCategory a, FeedCategory b) {
+ if (a.unread != b.unread)
+ return b.unread - a.unread;
+ else
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ }
+ }
+
+
+ @SuppressLint("DefaultLocale")
+ class CatTitleComparator implements Comparator<FeedCategory> {
+
+ @Override
+ public int compare(FeedCategory a, FeedCategory b) {
+ if (a.id >= 0 && b.id >= 0)
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ else
+ return a.id - b.id;
+ }
+
+ }
+
+ @SuppressLint("DefaultLocale")
+ class CatOrderComparator implements Comparator<FeedCategory> {
+
+ @Override
+ public int compare(FeedCategory a, FeedCategory b) {
+ if (a.id >= 0 && b.id >= 0)
+ if (a.order_id != 0 && b.order_id != 0)
+ return a.order_id - b.order_id;
+ else
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ else
+ return a.id - b.id;
+ }
+
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo();
+
+ switch (item.getItemId()) {
+ case R.id.browse_articles:
+ if (true) {
+ FeedCategory cat = getCategoryAtPosition(info.position);
+ if (cat != null) {
+ m_activity.openFeedArticles(new Feed(cat.id, cat.title, true));
+ //setSelectedCategory(cat);
+ }
+ }
+ return true;
+ case R.id.browse_headlines:
+ if (true) {
+ FeedCategory cat = getCategoryAtPosition(info.position);
+ if (cat != null) {
+ m_activity.onCatSelected(cat, true);
+ //setSelectedCategory(cat);
+ }
+ }
+ return true;
+ case R.id.browse_feeds:
+ if (true) {
+ FeedCategory cat = getCategoryAtPosition(info.position);
+ if (cat != null) {
+ m_activity.onCatSelected(cat, false);
+ //cf.setSelectedCategory(cat);
+ }
+ }
+ return true;
+ case R.id.create_shortcut:
+ if (true) {
+ FeedCategory cat = getCategoryAtPosition(info.position);
+ if (cat != null) {
+ m_activity.createCategoryShortcut(cat);
+ //cf.setSelectedCategory(cat);
+ }
+ }
+ return true;
+ case R.id.catchup_category:
+ if (true) {
+ final FeedCategory cat = getCategoryAtPosition(info.position);
+ if (cat != null) {
+
+ if (m_prefs.getBoolean("confirm_headlines_catchup", true)) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ m_activity)
+ .setMessage(getString(R.string.context_confirm_catchup, cat.title))
+ .setPositiveButton(R.string.catchup,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ m_activity.catchupFeed(new Feed(cat.id, cat.title, true));
+
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ }
+ });
+
+ AlertDialog dlg = builder.create();
+ dlg.show();
+ } else {
+ m_activity.catchupFeed(new Feed(cat.id, cat.title, true));
+ }
+
+ }
+ }
+ 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) {
+
+ m_activity.getMenuInflater().inflate(R.menu.category_menu, menu);
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+ FeedCategory cat = m_adapter.getItem(info.position);
+
+ if (cat != null)
+ menu.setHeaderTitle(cat.title);
+
+ if (!m_activity.isSmallScreen()) {
+ menu.findItem(R.id.browse_articles).setVisible(false);
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ public FeedCategory getCategoryAtPosition(int position) {
+ return m_adapter.getItem(position);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ m_selectedCat = savedInstanceState.getParcelable("selectedCat");
+ m_cats = savedInstanceState.getParcelable("cats");
+ }
+
+ View view = inflater.inflate(R.layout.cats_fragment, container, false);
+
+ m_swipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.feeds_swipe_container);
+
+ m_swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ refresh(false);
+ }
+ });
+
+ if (!m_activity.isCompatMode()) {
+ m_swipeLayout.setColorScheme(android.R.color.holo_green_dark,
+ android.R.color.holo_red_dark,
+ android.R.color.holo_blue_dark,
+ android.R.color.holo_orange_dark);
+ }
+
+
+ ListView list = (ListView)view.findViewById(R.id.feeds);
+ m_adapter = new FeedCategoryListAdapter(getActivity(), R.layout.feeds_row, (ArrayList<FeedCategory>)m_cats);
+ list.setAdapter(m_adapter);
+ list.setOnItemClickListener(this);
+ registerForContextMenu(list);
+
+ //m_activity.m_pullToRefreshAttacher.addRefreshableView(list, this);
+
+ return view;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_activity = (FeedsActivity)activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ m_prefs.registerOnSharedPreferenceChangeListener(this);
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ refresh(false);
+
+ m_activity.initMenu();
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.setClassLoader(getClass().getClassLoader());
+ out.putParcelable("selectedCat", m_selectedCat);
+ out.putParcelable("cats", m_cats);
+ }
+
+ /* private void setLoadingStatus(int status, boolean showProgress) {
+ if (getView() != null) {
+ TextView tv = (TextView)getView().findViewById(R.id.loading_message);
+
+ if (tv != null) {
+ tv.setText(status);
+ }
+ }
+
+ m_activity.setProgressBarIndeterminateVisibility(showProgress);
+ } */
+
+ public void refresh(boolean background) {
+ m_swipeLayout.setRefreshing(true);
+
+ CatsRequest req = new CatsRequest(getActivity().getApplicationContext());
+
+ final String sessionId = m_activity.getSessionId();
+ final boolean unreadOnly = m_activity.getUnreadOnly();
+
+ if (sessionId != null) {
+ //m_activity.setLoadingStatus(R.string.blank, true);
+ //m_activity.setProgressBarVisibility(true);
+
+ @SuppressWarnings("serial")
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ put("op", "getCategories");
+ put("sid", sessionId);
+ put("enable_nested", "true");
+ if (unreadOnly) {
+ put("unread_only", String.valueOf(unreadOnly));
+ }
+ }
+ };
+
+ req.execute(map);
+ }
+ }
+
+ private class CatsRequest extends ApiRequest {
+
+ public CatsRequest(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ m_activity.setProgress(Math.round((((float)progress[0] / (float)progress[1]) * 10000)));
+ }
+
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (isDetached()) return;
+
+ m_activity.setProgressBarVisibility(false);
+ m_swipeLayout.setRefreshing(false);
+
+ if (getView() != null) {
+ ListView list = (ListView)getView().findViewById(R.id.feeds);
+
+ if (list != null) {
+ list.setEmptyView(getView().findViewById(R.id.no_feeds));
+ }
+ }
+
+ if (result != null) {
+ try {
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
+ Type listType = new TypeToken<List<FeedCategory>>() {}.getType();
+ final List<FeedCategory> cats = new Gson().fromJson(content, listType);
+
+ m_cats.clear();
+
+ int apiLevel = m_activity.getApiLevel();
+
+ // virtual cats implemented in getCategories since api level 1
+ if (apiLevel == 0) {
+ m_cats.add(new FeedCategory(-1, "Special", 0));
+ m_cats.add(new FeedCategory(-2, "Labels", 0));
+ m_cats.add(new FeedCategory(0, "Uncategorized", 0));
+ }
+
+ for (FeedCategory c : cats)
+ m_cats.add(c);
+
+ sortCats();
+
+ /* if (m_cats.size() == 0)
+ setLoadingStatus(R.string.no_feeds_to_display, false);
+ else */
+
+ //m_adapter.notifyDataSetChanged(); (done by sortCats)
+ m_activity.setLoadingStatus(R.string.blank, false);
+
+ return;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ m_activity.login(true);
+ } else {
+ m_activity.setLoadingStatus(getErrorMessage(), false);
+ }
+ }
+
+ }
+
+ public void sortCats() {
+ Comparator<FeedCategory> cmp;
+
+ if (m_prefs.getBoolean("sort_feeds_by_unread", false)) {
+ cmp = new CatUnreadComparator();
+ } else {
+ if (m_activity.getApiLevel() >= 3) {
+ cmp = new CatOrderComparator();
+ } else {
+ cmp = new CatTitleComparator();
+ }
+ }
+
+ try {
+ Collections.sort(m_cats, cmp);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ }
+ try {
+ m_adapter.notifyDataSetChanged();
+ } catch (NullPointerException e) {
+ // adapter missing
+ }
+
+ }
+
+ private class FeedCategoryListAdapter extends ArrayAdapter<FeedCategory> {
+ private ArrayList<FeedCategory> items;
+
+ public static final int VIEW_NORMAL = 0;
+ public static final int VIEW_SELECTED = 1;
+
+ public static final int VIEW_COUNT = VIEW_SELECTED+1;
+
+ public FeedCategoryListAdapter(Context context, int textViewResourceId, ArrayList<FeedCategory> items) {
+ super(context, textViewResourceId, items);
+ this.items = items;
+ }
+
+ public int getViewTypeCount() {
+ return VIEW_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ FeedCategory cat = items.get(position);
+
+ if (!m_activity.isSmallScreen() && m_selectedCat != null && cat.id == m_selectedCat.id) {
+ return VIEW_SELECTED;
+ } else {
+ return VIEW_NORMAL;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+
+ FeedCategory cat = items.get(position);
+
+ if (v == null) {
+ int layoutId = R.layout.feeds_row;
+
+ switch (getItemViewType(position)) {
+ case VIEW_SELECTED:
+ layoutId = R.layout.feeds_row_selected;
+ break;
+ }
+
+ LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(layoutId, null);
+
+ }
+
+ TextView tt = (TextView) v.findViewById(R.id.title);
+
+ if (tt != null) {
+ tt.setText(cat.title);
+ }
+
+ TextView tu = (TextView) v.findViewById(R.id.unread_counter);
+
+ if (tu != null) {
+ tu.setText(String.valueOf(cat.unread));
+ tu.setVisibility((cat.unread > 0) ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ ImageView icon = (ImageView)v.findViewById(R.id.icon);
+
+ if (icon != null) {
+ icon.setImageResource(cat.unread > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+
+ ImageButton ib = (ImageButton) v.findViewById(R.id.feed_menu_button);
+
+ if (ib != null) {
+ ib.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().openContextMenu(v);
+ }
+ });
+ }
+
+
+ return v;
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+
+ sortCats();
+
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> av, View view, int position, long id) {
+ ListView list = (ListView)av;
+
+ Log.d(TAG, "onItemClick=" + position);
+
+ if (list != null) {
+ FeedCategory cat = (FeedCategory)list.getItemAtPosition(position);
+
+ if (cat.id < 0) {
+ m_activity.onCatSelected(cat, false);
+ } else {
+ if ("ARTICLES".equals(m_prefs.getString("default_view_mode", "HEADLINES")) &&
+ m_prefs.getBoolean("browse_cats_like_feeds", false)) {
+
+ m_activity.openFeedArticles(new Feed(cat.id, cat.title, true));
+
+ } else {
+ m_activity.onCatSelected(cat);
+ }
+ }
+
+ //if (!m_activity.isSmallScreen())
+ // m_selectedCat = cat;
+
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ public void setSelectedCategory(FeedCategory cat) {
+ m_selectedCat = cat;
+
+ if (m_adapter != null) {
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ public FeedCategory getSelectedCategory() {
+ return m_selectedCat;
+ }
+
+ /* @Override
+ public void onRefreshStarted(View view) {
+ refresh(false);
+ } */
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/FeedsActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/FeedsActivity.java
new file mode 100644
index 00000000..90e2c118
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/FeedsActivity.java
@@ -0,0 +1,509 @@
+package org.fox.ttrss;
+
+
+import java.util.Date;
+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.types.FeedCategory;
+import org.fox.ttrss.util.AppRater;
+
+import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+import com.google.gson.JsonElement;
+import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;
+import com.readystatesoftware.systembartint.SystemBarTintManager;
+
+public class FeedsActivity extends OnlineActivity implements HeadlinesEventListener {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private static final int HEADLINES_REQUEST = 1;
+
+ protected SharedPreferences m_prefs;
+ protected long m_lastRefresh = 0;
+
+ private boolean m_actionbarUpEnabled = false;
+ private int m_actionbarRevertDepth = 0;
+ private SlidingMenu m_slidingMenu;
+ private boolean m_feedIsSelected = false;
+ private boolean m_feedWasSelected = false;
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ setAppTheme(m_prefs);
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.headlines);
+
+ setStatusBarTint();
+
+ setSmallScreen(findViewById(R.id.sw600dp_anchor) == null &&
+ findViewById(R.id.sw600dp_port_anchor) == null);
+
+ GlobalState.getInstance().load(savedInstanceState);
+
+ if (isSmallScreen() || findViewById(R.id.sw600dp_port_anchor) != null) {
+ m_slidingMenu = new SlidingMenu(this);
+
+/* if (findViewById(R.id.sw600dp_port_anchor) != null) {
+ m_slidingMenu.setBehindWidth(getScreenWidthInPixel() * 2/3);
+ } */
+
+ m_slidingMenu.setMode(SlidingMenu.LEFT);
+ m_slidingMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
+ m_slidingMenu.attachToActivity(this, SlidingMenu.SLIDING_CONTENT);
+ m_slidingMenu.setMenu(R.layout.feeds);
+ m_slidingMenu.setSlidingEnabled(true);
+
+ m_slidingMenu.setOnClosedListener(new SlidingMenu.OnClosedListener() {
+
+ @Override
+ public void onClosed() {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ m_actionbarUpEnabled = true;
+ m_feedIsSelected = true;
+
+ initMenu();
+ }
+ });
+
+ m_slidingMenu.setOnOpenedListener(new SlidingMenu.OnOpenedListener() {
+
+ @Override
+ public void onOpened() {
+ if (m_actionbarRevertDepth == 0) {
+ m_actionbarUpEnabled = false;
+ getSupportActionBar().setDisplayHomeAsUpEnabled(false);
+ refresh(false);
+ }
+
+ m_feedIsSelected = false;
+ initMenu();
+ }
+ });
+ }
+
+ if (savedInstanceState == null) {
+ if (m_slidingMenu != null)
+ m_slidingMenu.showMenu();
+
+ final Intent i = getIntent();
+ boolean shortcutMode = i.getBooleanExtra("shortcut_mode", false);
+
+ Log.d(TAG, "is_shortcut_mode: " + shortcutMode);
+
+ if (shortcutMode) {
+ LoginRequest lr = new LoginRequest(this, false, new OnLoginFinishedListener() {
+
+ @Override
+ public void OnLoginSuccess() {
+ int feedId = i.getIntExtra("feed_id", 0);
+ boolean isCat = i.getBooleanExtra("feed_is_cat", false);
+ String feedTitle = i.getStringExtra("feed_title");
+
+ Feed tmpFeed = new Feed(feedId, feedTitle, isCat);
+
+ onFeedSelected(tmpFeed);
+ }
+
+ @Override
+ public void OnLoginFailed() {
+ login();
+ }
+ });
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("op", "login");
+ put("user", m_prefs.getString("login", "").trim());
+ put("password", m_prefs.getString("password", "").trim());
+ }
+ };
+
+ lr.execute(map);
+ }
+
+ //m_pullToRefreshAttacher.setRefreshing(true);
+
+ 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);
+ }
+
+ ft.commit();
+
+ AppRater.appLaunched(this);
+ checkTrial(true);
+
+ } else { // savedInstanceState != null
+ m_actionbarUpEnabled = savedInstanceState.getBoolean("actionbarUpEnabled");
+ m_actionbarRevertDepth = savedInstanceState.getInt("actionbarRevertDepth");
+ m_feedIsSelected = savedInstanceState.getBoolean("feedIsSelected");
+ m_feedWasSelected = savedInstanceState.getBoolean("feedWasSelected");
+
+ if (findViewById(R.id.sw600dp_port_anchor) != null && m_feedWasSelected && m_slidingMenu != null) {
+ m_slidingMenu.setBehindWidth(getScreenWidthInPixel() * 2/3);
+ }
+
+ if (m_slidingMenu != null && m_feedIsSelected == false) {
+ m_slidingMenu.showMenu();
+ } else if (m_slidingMenu != null) {
+ m_actionbarUpEnabled = true;
+ } else {
+ m_actionbarUpEnabled = m_actionbarRevertDepth > 0;
+ }
+
+ if (m_actionbarUpEnabled) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (!isSmallScreen()) {
+ // temporary hack because FeedsActivity doesn't track whether active feed is open
+ LinearLayout container = (LinearLayout) findViewById(R.id.fragment_container);
+
+ if (container != null)
+ container.setWeightSum(3f);
+ }
+
+ }
+
+ /* if (!isCompatMode() && !isSmallScreen()) {
+ ((ViewGroup)findViewById(R.id.headlines_fragment)).setLayoutTransition(new LayoutTransition());
+ ((ViewGroup)findViewById(R.id.feeds_fragment)).setLayoutTransition(new LayoutTransition());
+ } */
+
+ }
+
+ @Override
+ protected void initMenu() {
+ super.initMenu();
+
+ if (m_menu != null && getSessionId() != null) {
+ Fragment ff = getSupportFragmentManager().findFragmentByTag(FRAG_FEEDS);
+ Fragment cf = getSupportFragmentManager().findFragmentByTag(FRAG_CATS);
+ HeadlinesFragment hf = (HeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (m_slidingMenu != null) {
+ m_menu.setGroupVisible(R.id.menu_group_feeds, m_slidingMenu.isMenuShowing());
+ m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.isAdded() && !m_slidingMenu.isMenuShowing());
+ } else {
+ m_menu.setGroupVisible(R.id.menu_group_feeds, (ff != null && ff.isAdded()) || (cf != null && cf.isAdded()));
+ m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.isAdded());
+
+ m_menu.findItem(R.id.update_headlines).setVisible(false);
+ }
+
+ m_menu.findItem(R.id.headlines_toggle_sidebar).setVisible(false);
+
+ MenuItem item = m_menu.findItem(R.id.show_feeds);
+
+ if (getUnreadOnly()) {
+ item.setTitle(R.string.menu_all_feeds);
+ } else {
+ item.setTitle(R.string.menu_unread_feeds);
+ }
+ }
+ }
+
+ public void onFeedSelected(Feed feed) {
+ GlobalState.getInstance().m_loadedArticles.clear();
+ //m_pullToRefreshAttacher.setRefreshing(true);
+
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+
+ ft.replace(R.id.headlines_fragment, new LoadingFragment(), null);
+ ft.commit();
+
+ if (!isCompatMode() && !isSmallScreen()) {
+ LinearLayout container = (LinearLayout) findViewById(R.id.fragment_container);
+ if (container != null) {
+ float wSum = container.getWeightSum();
+ if (wSum <= 2.0f) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(container, "weightSum", wSum, 3.0f);
+ anim.setDuration(200);
+ anim.start();
+ }
+ }
+ }
+
+ final Feed fFeed = feed;
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+
+ HeadlinesFragment hf = new HeadlinesFragment();
+ hf.initialize(fFeed);
+ ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
+
+ ft.commit();
+
+ m_feedIsSelected = true;
+ m_feedWasSelected = true;
+
+ if (m_slidingMenu != null) {
+ if (findViewById(R.id.sw600dp_port_anchor) != null) {
+ m_slidingMenu.setBehindWidth(getScreenWidthInPixel() * 2/3);
+ }
+
+ m_slidingMenu.showContent();
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ m_actionbarUpEnabled = true;
+
+ }
+ }
+ }, 10);
+
+
+ Date date = new Date();
+
+ if (date.getTime() - m_lastRefresh > 10000) {
+ m_lastRefresh = date.getTime();
+ refresh(false);
+ }
+ }
+
+ public void onCatSelected(FeedCategory cat, boolean openAsFeed) {
+ FeedCategoriesFragment fc = (FeedCategoriesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_CATS);
+
+ //m_pullToRefreshAttacher.setRefreshing(true);
+
+ if (!openAsFeed) {
+
+ if (fc != null) {
+ fc.setSelectedCategory(null);
+ }
+
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+
+ FeedsFragment ff = new FeedsFragment();
+ ff.initialize(cat);
+ ft.replace(R.id.feeds_fragment, ff, FRAG_FEEDS);
+
+ ft.addToBackStack(null);
+ ft.commit();
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ m_actionbarUpEnabled = true;
+ m_actionbarRevertDepth = m_actionbarRevertDepth + 1;
+
+ } else {
+
+ if (fc != null) {
+ fc.setSelectedCategory(cat);
+ }
+
+ Feed feed = new Feed(cat.id, cat.title, true);
+ onFeedSelected(feed);
+ }
+ }
+
+ public void onCatSelected(FeedCategory cat) {
+ onCatSelected(cat, m_prefs.getBoolean("browse_cats_like_feeds", false));
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (m_actionbarRevertDepth > 0) {
+
+ if (m_feedIsSelected && m_slidingMenu != null && !m_slidingMenu.isMenuShowing()) {
+ m_slidingMenu.showMenu();
+ } else {
+ m_actionbarRevertDepth = m_actionbarRevertDepth - 1;
+ m_actionbarUpEnabled = m_actionbarRevertDepth > 0;
+ getSupportActionBar().setDisplayHomeAsUpEnabled(m_actionbarUpEnabled);
+
+ onBackPressed();
+ }
+ } else if (m_slidingMenu != null && !m_slidingMenu.isMenuShowing()) {
+ m_slidingMenu.showMenu();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (m_actionbarUpEnabled)
+ onBackPressed();
+ return true;
+ case R.id.show_feeds:
+ setUnreadOnly(!getUnreadOnly());
+ initMenu();
+ refresh();
+ return true;
+ case R.id.update_feeds:
+ //m_pullToRefreshAttacher.setRefreshing(true);
+ refresh();
+ return true;
+ default:
+ Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void loginSuccess(boolean refresh) {
+ setLoadingStatus(R.string.blank, false);
+ //findViewById(R.id.loading_container).setVisibility(View.GONE);
+ initMenu();
+
+ if (refresh) refresh();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putBoolean("actionbarUpEnabled", m_actionbarUpEnabled);
+ out.putInt("actionbarRevertDepth", m_actionbarRevertDepth);
+ out.putBoolean("feedIsSelected", m_feedIsSelected);
+ out.putBoolean("feedWasSelected", m_feedWasSelected);
+
+ //if (m_slidingMenu != null )
+ // out.putBoolean("slidingMenuVisible", m_slidingMenu.isMenuShowing());
+
+ GlobalState.getInstance().save(out);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ initMenu();
+ }
+
+ @Override
+ public void onArticleListSelectionChange(ArticleList m_selectedArticles) {
+ initMenu();
+ }
+
+ public void openFeedArticles(Feed feed) {
+ GlobalState.getInstance().m_loadedArticles.clear();
+
+ Intent intent = new Intent(FeedsActivity.this, HeadlinesActivity.class);
+ intent.putExtra("feed", feed);
+ intent.putExtra("article", (Article)null);
+ intent.putExtra("searchQuery", (String)null);
+
+ startActivityForResult(intent, HEADLINES_REQUEST);
+ overridePendingTransition(R.anim.right_slide_in, 0);
+ }
+
+ public void onArticleSelected(Article article, boolean open) {
+ if (article.unread) {
+ article.unread = false;
+ saveArticleUnread(article);
+ }
+
+ if (open) {
+ HeadlinesFragment hf = (HeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ Intent intent = new Intent(FeedsActivity.this, HeadlinesActivity.class);
+ intent.putExtra("feed", hf.getFeed());
+ intent.putExtra("article", article);
+ intent.putExtra("searchQuery", hf.getSearchQuery());
+
+ startActivityForResult(intent, HEADLINES_REQUEST);
+ overridePendingTransition(R.anim.right_slide_in, 0);
+
+ } else {
+ initMenu();
+ }
+ }
+
+ @Override
+ public void onArticleSelected(Article article) {
+ onArticleSelected(article, true);
+ }
+
+ public void catchupFeed(final Feed feed) {
+ super.catchupFeed(feed);
+ refresh();
+ }
+
+ @Override
+ public void onHeadlinesLoaded(boolean appended) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == HEADLINES_REQUEST) {
+ GlobalState.getInstance().m_activeArticle = null;
+ }
+ }
+
+ public void createFeedShortcut(Feed feed) {
+ final Intent shortcutIntent = new Intent(this, FeedsActivity.class);
+ shortcutIntent.putExtra("feed_id", feed.id);
+ shortcutIntent.putExtra("feed_is_cat", feed.is_cat);
+ shortcutIntent.putExtra("feed_title", feed.title);
+ shortcutIntent.putExtra("shortcut_mode", true);
+
+ Intent intent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
+
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, feed.title);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon));
+ intent.putExtra("duplicate", false);
+
+ sendBroadcast(intent);
+
+ toast(R.string.shortcut_has_been_placed_on_the_home_screen);
+ }
+
+ public void createCategoryShortcut(FeedCategory cat) {
+ createFeedShortcut(new Feed(cat.id, cat.title, true));
+ }
+
+ public void unsubscribeFeed(final Feed feed) {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ refresh();
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "unsubscribeFeed");
+ put("feed_id", String.valueOf(feed.id));
+ }
+ };
+
+ req.execute(map);
+
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/FeedsFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/FeedsFragment.java
new file mode 100644
index 00000000..7c974809
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/FeedsFragment.java
@@ -0,0 +1,808 @@
+package org.fox.ttrss;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.lang.reflect.Type;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+import org.fox.ttrss.types.Feed;
+import org.fox.ttrss.types.FeedCategory;
+import org.fox.ttrss.types.FeedList;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.http.AndroidHttpClient;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.Base64;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+
+public class FeedsFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener {
+ private final String TAG = this.getClass().getSimpleName();
+ private SharedPreferences m_prefs;
+ private FeedListAdapter m_adapter;
+ private FeedList m_feeds = new FeedList();
+ private FeedsActivity m_activity;
+ private Feed m_selectedFeed;
+ private FeedCategory m_activeCategory;
+ private static final String ICON_PATH = "/icons/";
+ private boolean m_enableFeedIcons;
+ private boolean m_feedIconsChecked = false;
+ private SwipeRefreshLayout m_swipeLayout;
+
+ public void initialize(FeedCategory cat) {
+ m_activeCategory = cat;
+ }
+
+ @SuppressLint("DefaultLocale")
+ class FeedUnreadComparator implements Comparator<Feed> {
+
+ @Override
+ public int compare(Feed a, Feed b) {
+ if (a.unread != b.unread)
+ return b.unread - a.unread;
+ else
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ }
+
+ }
+
+
+ @SuppressLint("DefaultLocale")
+ class FeedTitleComparator implements Comparator<Feed> {
+
+ @Override
+ public int compare(Feed a, Feed b) {
+ if (a.is_cat && b.is_cat)
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ else if (a.is_cat && !b.is_cat)
+ return -1;
+ else if (!a.is_cat && b.is_cat)
+ return 1;
+ else if (a.id >= 0 && b.id >= 0)
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ else
+ return a.id - b.id;
+ }
+
+ }
+
+ @SuppressLint("DefaultLocale")
+ class FeedOrderComparator implements Comparator<Feed> {
+
+ @Override
+ public int compare(Feed a, Feed b) {
+ if (a.id >= 0 && b.id >= 0)
+ if (a.is_cat && b.is_cat)
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ else if (a.is_cat && !b.is_cat)
+ return -1;
+ else if (!a.is_cat && b.is_cat)
+ return 1;
+ else if (a.order_id != 0 && b.order_id != 0)
+ return a.order_id - b.order_id;
+ else
+ return a.title.toUpperCase().compareTo(b.title.toUpperCase());
+ else
+ return a.id - b.id;
+ }
+
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo();
+ switch (item.getItemId()) {
+ case R.id.browse_articles:
+ if (true) {
+ Feed feed = getFeedAtPosition(info.position);
+ if (feed != null) {
+ m_activity.openFeedArticles(feed);
+ }
+ }
+ return true;
+ case R.id.browse_headlines:
+ if (true) {
+ Feed feed = getFeedAtPosition(info.position);
+ if (feed != null) {
+ m_activity.onFeedSelected(feed);
+ }
+ }
+ return true;
+ case R.id.browse_feeds:
+ if (true) {
+ Feed feed = getFeedAtPosition(info.position);
+ if (feed != null) {
+ m_activity.onCatSelected(new FeedCategory(feed.id, feed.title, feed.unread), false);
+ }
+ }
+ return true;
+ case R.id.unsubscribe_feed:
+ if (true) {
+ final Feed feed = getFeedAtPosition(info.position);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ m_activity)
+ .setMessage(getString(R.string.unsubscribe_from_prompt, feed.title))
+ .setPositiveButton(R.string.unsubscribe,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ m_activity.unsubscribeFeed(feed);
+
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ }
+ });
+
+ AlertDialog dlg = builder.create();
+ dlg.show();
+ }
+
+ return true;
+ case R.id.create_shortcut:
+ if (true) {
+ Feed feed = getFeedAtPosition(info.position);
+ if (feed != null) {
+ m_activity.createFeedShortcut(feed);
+ }
+ }
+ return true;
+ case R.id.catchup_feed:
+ if (true) {
+ final Feed feed = getFeedAtPosition(info.position);
+
+ if (feed != null) {
+ if (m_prefs.getBoolean("confirm_headlines_catchup", true)) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ m_activity)
+ .setMessage(getString(R.string.context_confirm_catchup, feed.title))
+ .setPositiveButton(R.string.catchup,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ m_activity.catchupFeed(feed);
+
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ }
+ });
+
+ AlertDialog dlg = builder.create();
+ dlg.show();
+ } else {
+ m_activity.catchupFeed(feed);
+ }
+ }
+ }
+ 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.feed_menu, menu);
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+ Feed feed = m_adapter.getItem(info.position);
+
+ if (feed != null)
+ menu.setHeaderTitle(feed.title);
+
+ if (!m_activity.isSmallScreen()) {
+ menu.findItem(R.id.browse_articles).setVisible(false);
+ }
+
+ if (!feed.is_cat) {
+ menu.findItem(R.id.browse_feeds).setVisible(false);
+ }
+
+ if (feed.id <= 0) {
+ menu.findItem(R.id.unsubscribe_feed).setVisible(false);
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ if (savedInstanceState != null) {
+ m_selectedFeed = savedInstanceState.getParcelable("selectedFeed");
+ m_feeds = savedInstanceState.getParcelable("feeds");
+ m_feedIconsChecked = savedInstanceState.getBoolean("feedIconsChecked");
+ m_activeCategory = savedInstanceState.getParcelable("activeCat");
+ }
+
+ View view = inflater.inflate(R.layout.feeds_fragment, container, false);
+
+ m_swipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.feeds_swipe_container);
+
+ m_swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ refresh(false);
+ }
+ });
+
+ if (!m_activity.isCompatMode()) {
+ m_swipeLayout.setColorScheme(android.R.color.holo_green_dark,
+ android.R.color.holo_red_dark,
+ android.R.color.holo_blue_dark,
+ android.R.color.holo_orange_dark);
+ }
+
+ ListView list = (ListView)view.findViewById(R.id.feeds);
+ m_adapter = new FeedListAdapter(getActivity(), R.layout.feeds_row, (ArrayList<Feed>)m_feeds);
+ list.setAdapter(m_adapter);
+ //list.setEmptyView(view.findViewById(R.id.no_feeds));
+ list.setOnItemClickListener(this);
+
+ registerForContextMenu(list);
+
+ m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false);
+
+ //Log.d(TAG, "mpTRA=" + m_activity.m_pullToRefreshAttacher);
+ //m_activity.m_pullToRefreshAttacher.addRefreshableView(list, this);
+
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ m_prefs.registerOnSharedPreferenceChangeListener(this);
+
+ m_activity = (FeedsActivity)activity;
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ refresh(false);
+
+ m_activity.initMenu();
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.setClassLoader(getClass().getClassLoader());
+ out.putParcelable("selectedFeed", m_selectedFeed);
+ out.putParcelable("feeds", m_feeds);
+ out.putBoolean("feedIconsChecked", m_feedIconsChecked);
+ out.putParcelable("activeCat", m_activeCategory);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> av, View view, int position, long id) {
+ ListView list = (ListView)av;
+
+ if (list != null) {
+ Feed feed = (Feed)list.getItemAtPosition(position);
+
+ if (feed.is_cat) {
+ if (m_activity.isSmallScreen() && "ARTICLES".equals(m_prefs.getString("default_view_mode", "HEADLINES")) &&
+ m_prefs.getBoolean("browse_cats_like_feeds", false)) {
+
+ m_activity.openFeedArticles(feed);
+
+ } else {
+ m_activity.onCatSelected(new FeedCategory(feed.id, feed.title, feed.unread));
+ }
+ } else {
+ if ("ARTICLES".equals(m_prefs.getString("default_view_mode", "HEADLINES"))) {
+ m_activity.openFeedArticles(feed);
+ } else {
+ m_activity.onFeedSelected(feed);
+ }
+ }
+
+ if (!m_activity.isSmallScreen())
+ m_selectedFeed = feed;
+
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ @SuppressWarnings({ "serial" })
+ public void refresh(boolean background) {
+ //FeedCategory cat = m_onlineServices.getActiveCategory();
+
+ m_swipeLayout.setRefreshing(true);
+
+ final int catId = (m_activeCategory != null) ? m_activeCategory.id : -4;
+
+ final String sessionId = m_activity.getSessionId();
+ final boolean unreadOnly = m_activity.getUnreadOnly();
+
+ FeedsRequest req = new FeedsRequest(getActivity().getApplicationContext(), catId);
+
+ if (sessionId != null) {
+ //m_activity.setLoadingStatus(R.string.blank, true);
+ //m_activity.setProgressBarVisibility(true);
+
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ put("op", "getFeeds");
+ put("sid", sessionId);
+ put("include_nested", "true");
+ put("cat_id", String.valueOf(catId));
+ if (unreadOnly) {
+ put("unread_only", String.valueOf(unreadOnly));
+ }
+ }
+ };
+
+ req.execute(map);
+
+ }
+ }
+
+ /* private void setLoadingStatus(int status, boolean showProgress) {
+ if (getView() != null) {
+ TextView tv = (TextView)getView().findViewById(R.id.loading_message);
+
+ if (tv != null) {
+ tv.setText(status);
+ }
+ }
+
+ if (getActivity() != null)
+ getActivity().setProgressBarIndeterminateVisibility(showProgress);
+ } */
+
+ @SuppressWarnings({ "serial" })
+ public void getFeedIcons() {
+
+ ApiRequest req = new ApiRequest(getActivity().getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ if (isDetached()) return;
+
+ if (result != null) {
+
+ try {
+ JsonElement iconsUrl = result.getAsJsonObject().get("icons_dir");
+
+ if (iconsUrl != null) {
+ String iconsStr = iconsUrl.getAsString();
+ String baseUrl = "";
+
+ if (!iconsStr.contains("://")) {
+ baseUrl = m_prefs.getString("ttrss_url", "").trim() + "/" + iconsStr;
+ } else {
+ baseUrl = iconsStr;
+ }
+
+ GetIconsTask git = new GetIconsTask(baseUrl);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ git.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, m_feeds);
+ else
+ git.execute(m_feeds);
+
+ m_feedIconsChecked = true;
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Error receiving icons configuration");
+ e.printStackTrace();
+ }
+
+ }
+ }
+ };
+
+ final String sessionId = m_activity.getSessionId();
+
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ put("sid", sessionId);
+ put("op", "getConfig");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ private class FeedsRequest extends ApiRequest {
+ private int m_catId;
+
+ public FeedsRequest(Context context, int catId) {
+ super(context);
+ m_catId = catId;
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ m_activity.setProgress(Math.round((((float)progress[0] / (float)progress[1]) * 10000)));
+ }
+
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (isDetached()) return;
+
+ if (getView() != null) {
+ ListView list = (ListView)getView().findViewById(R.id.feeds);
+
+ if (list != null) {
+ list.setEmptyView(getView().findViewById(R.id.no_feeds));
+ }
+ }
+
+ m_activity.setProgressBarVisibility(false);
+ //m_activity.m_pullToRefreshAttacher.setRefreshComplete();
+ m_swipeLayout.setRefreshing(false);
+
+ if (result != null) {
+ try {
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
+
+ Type listType = new TypeToken<List<Feed>>() {}.getType();
+ final List<Feed> feeds = new Gson().fromJson(content, listType);
+
+ m_feeds.clear();
+
+ for (Feed f : feeds)
+ if (f.id > -10 || m_catId != -4) // skip labels for flat feedlist for now
+ m_feeds.add(f);
+
+ sortFeeds();
+
+ /*if (m_feeds.size() == 0)
+ setLoadingStatus(R.string.no_feeds_to_display, false);
+ else */
+
+ m_activity.setLoadingStatus(R.string.blank, false);
+ //m_adapter.notifyDataSetChanged(); (done by sortFeeds)
+
+ if (m_enableFeedIcons && !m_feedIconsChecked && Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
+ getFeedIcons();
+
+ return;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ m_activity.login(true);
+ } else {
+ m_activity.setLoadingStatus(getErrorMessage(), false);
+ }
+ }
+ }
+
+ private class FeedListAdapter extends ArrayAdapter<Feed> {
+ private ArrayList<Feed> items;
+
+ public static final int VIEW_NORMAL = 0;
+ public static final int VIEW_SELECTED = 1;
+
+ public static final int VIEW_COUNT = VIEW_SELECTED+1;
+
+ public FeedListAdapter(Context context, int textViewResourceId, ArrayList<Feed> items) {
+ super(context, textViewResourceId, items);
+ this.items = items;
+ }
+
+ public int getViewTypeCount() {
+ return VIEW_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Feed feed = items.get(position);
+
+ if (!m_activity.isSmallScreen() && m_selectedFeed != null && feed.id == m_selectedFeed.id) {
+ return VIEW_SELECTED;
+ } else {
+ return VIEW_NORMAL;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+
+ Feed feed = items.get(position);
+
+ if (v == null) {
+ int layoutId = R.layout.feeds_row;
+
+ switch (getItemViewType(position)) {
+ case VIEW_SELECTED:
+ layoutId = R.layout.feeds_row_selected;
+ break;
+ }
+
+ LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(layoutId, null);
+
+ }
+
+ TextView tt = (TextView) v.findViewById(R.id.title);
+
+ if (tt != null) {
+ tt.setText(feed.title);
+ }
+
+ TextView tu = (TextView) v.findViewById(R.id.unread_counter);
+
+ if (tu != null) {
+ tu.setText(String.valueOf(feed.unread));
+ tu.setVisibility((feed.unread > 0) ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ ImageView icon = (ImageView)v.findViewById(R.id.icon);
+
+ if (icon != null) {
+
+ if (m_enableFeedIcons) {
+
+ try {
+ File storage = m_activity.getExternalCacheDir();
+
+ File iconFile = new File(storage.getAbsolutePath() + ICON_PATH + feed.id + ".ico");
+ if (iconFile.exists()) {
+ Bitmap bmpOrig = BitmapFactory.decodeFile(iconFile.getAbsolutePath());
+ if (bmpOrig != null) {
+ icon.setImageBitmap(bmpOrig);
+ }
+ } else {
+ icon.setImageResource(feed.unread > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+ } catch (NullPointerException e) {
+ icon.setImageResource(feed.unread > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+
+ } else {
+ icon.setImageResource(feed.unread > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+
+ }
+
+ ImageButton ib = (ImageButton) v.findViewById(R.id.feed_menu_button);
+
+ if (ib != null) {
+ ib.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().openContextMenu(v);
+ }
+ });
+ }
+
+ return v;
+ }
+ }
+
+ public void sortFeeds() {
+ Comparator<Feed> cmp;
+
+ if (m_prefs.getBoolean("sort_feeds_by_unread", false)) {
+ cmp = new FeedUnreadComparator();
+ } else {
+ if (m_activity.getApiLevel() >= 3) {
+ cmp = new FeedOrderComparator();
+ } else {
+ cmp = new FeedTitleComparator();
+ }
+ }
+
+ try {
+ Collections.sort(m_feeds, cmp);
+ } catch (IllegalArgumentException e) {
+ // sort order got changed in prefs or something
+ e.printStackTrace();
+ }
+
+ try {
+ m_adapter.notifyDataSetChanged();
+ } catch (NullPointerException e) {
+ // adapter missing
+ }
+ }
+
+ public class GetIconsTask extends AsyncTask<FeedList, Integer, Integer> {
+
+ private String m_baseUrl;
+
+ public GetIconsTask(String baseUrl) {
+ m_baseUrl = baseUrl.trim();
+ }
+
+ @Override
+ protected Integer doInBackground(FeedList... params) {
+
+ FeedList localList = new FeedList();
+
+ try {
+ localList.addAll(params[0]);
+
+ File storage = m_activity.getExternalCacheDir();
+ final File iconPath = new File(storage.getAbsolutePath() + ICON_PATH);
+ if (!iconPath.exists()) iconPath.mkdirs();
+
+ if (iconPath.exists()) {
+ for (Feed feed : localList) {
+ if (feed.id > 0 && feed.has_icon && !feed.is_cat) {
+ File outputFile = new File(iconPath.getAbsolutePath() + "/" + feed.id + ".ico");
+ String fetchUrl = m_baseUrl + "/" + feed.id + ".ico";
+
+ if (!outputFile.exists()) {
+ downloadFile(fetchUrl, outputFile.getAbsolutePath());
+ Thread.sleep(2000);
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Error while downloading feed icons");
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ protected void downloadFile(String fetchUrl, String outputFile) {
+ AndroidHttpClient client = AndroidHttpClient.newInstance("Tiny Tiny RSS");
+
+ /* ApiRequest.disableConnectionReuseIfNecessary(); */
+
+ /* ApiRequest.trustAllHosts(m_prefs.getBoolean("ssl_trust_any", false),
+ m_prefs.getBoolean("ssl_trust_any_host", false)); */
+
+ try {
+ URL url = new URL(fetchUrl);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+ conn.setConnectTimeout(1000);
+ conn.setReadTimeout(5000);
+
+ Log.d(TAG, "[downloadFile] " + url);
+
+ String httpLogin = m_prefs.getString("http_login", "");
+ String httpPassword = m_prefs.getString("http_password", "");
+
+ if (httpLogin.length() > 0) {
+ conn.setRequestProperty("Authorization", "Basic " +
+ Base64.encodeToString((httpLogin + ":" + httpPassword).getBytes("UTF-8"), Base64.NO_WRAP));
+ }
+
+ InputStream content = conn.getInputStream();
+
+ BufferedInputStream is = new BufferedInputStream(content, 1024);
+ 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();
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ client.close();
+ }
+
+ protected void onPostExecute(Integer result) {
+ if (isDetached()) return;
+
+ m_adapter.notifyDataSetChanged();
+ }
+
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+
+ sortFeeds();
+ m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false);
+
+ }
+
+ public Feed getFeedAtPosition(int position) {
+ try {
+ return m_adapter.getItem(position);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+ public Feed getSelectedFeed() {
+ return m_selectedFeed;
+ }
+
+ public void setSelectedFeed(Feed feed) {
+ m_selectedFeed = feed;
+
+ if (m_adapter != null) {
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ /* @Override
+ public void onRefreshStarted(View view) {
+ refresh(false);
+ } */
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/GlobalState.java b/orgfoxttrss/src/main/java/org/fox/ttrss/GlobalState.java
new file mode 100644
index 00000000..6f81fc5d
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/GlobalState.java
@@ -0,0 +1,63 @@
+package org.fox.ttrss;
+
+import java.util.ArrayList;
+
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.ArticleList;
+import org.fox.ttrss.types.Feed;
+
+import android.app.Application;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+public class GlobalState extends Application {
+ private static GlobalState m_singleton;
+
+ public ArticleList m_loadedArticles = new ArticleList();
+ public Feed m_activeFeed;
+ public Article m_activeArticle;
+ public int m_selectedArticleId;
+ public String m_sessionId;
+ public int m_apiLevel;
+ public boolean m_canUseProgress;
+
+ public static GlobalState getInstance(){
+ return m_singleton;
+ }
+
+ @Override
+ public final void onCreate() {
+ super.onCreate();
+ m_singleton = this;
+ }
+
+ public void save(Bundle out) {
+
+ out.setClassLoader(getClass().getClassLoader());
+ out.putParcelableArrayList("gs:loadedArticles", m_loadedArticles);
+ out.putParcelable("gs:activeFeed", m_activeFeed);
+ out.putParcelable("gs:activeArticle", m_activeArticle);
+ out.putString("gs:sessionId", m_sessionId);
+ out.putInt("gs:apiLevel", m_apiLevel);
+ out.putBoolean("gs:canUseProgress", m_canUseProgress);
+ out.putInt("gs:selectedArticleId", m_selectedArticleId);
+ }
+
+ public void load(Bundle in) {
+ if (m_loadedArticles.size() == 0 && in != null) {
+ ArrayList<Parcelable> list = in.getParcelableArrayList("gs:loadedArticles");
+
+ for (Parcelable p : list) {
+ m_loadedArticles.add((Article)p);
+ }
+
+ m_activeFeed = (Feed) in.getParcelable("gs:activeFeed");
+ m_activeArticle = (Article) in.getParcelable("gs:activeArticle");
+ m_sessionId = in.getString("gs:sessionId");
+ m_apiLevel = in.getInt("gs:apiLevel");
+ m_canUseProgress = in.getBoolean("gs:canUseProgress");
+ m_selectedArticleId = in.getInt("gs:selectedArticleId");
+ }
+
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesActivity.java
new file mode 100644
index 00000000..c4e0f0cc
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesActivity.java
@@ -0,0 +1,281 @@
+package org.fox.ttrss;
+
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.ArticleList;
+import org.fox.ttrss.types.Feed;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+
+public class HeadlinesActivity extends OnlineActivity implements HeadlinesEventListener {
+ private final String TAG = this.getClass().getSimpleName();
+
+ protected SharedPreferences m_prefs;
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ setAppTheme(m_prefs);
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.headlines_articles);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setStatusBarTint();
+ setSmallScreen(findViewById(R.id.sw600dp_anchor) == null);
+
+ GlobalState.getInstance().load(savedInstanceState);
+
+ if (isPortrait() || m_prefs.getBoolean("headlines_hide_sidebar", false)) {
+ findViewById(R.id.headlines_fragment).setVisibility(View.GONE);
+ }
+
+ if (savedInstanceState == null) {
+ Intent i = getIntent();
+
+ if (i.getExtras() != null) {
+ boolean shortcutMode = i.getBooleanExtra("shortcut_mode", false);
+
+ Log.d(TAG, "is_shortcut_mode: " + shortcutMode);
+
+ Feed tmpFeed;
+
+ if (shortcutMode) {
+ int feedId = i.getIntExtra("feed_id", 0);
+ boolean isCat = i.getBooleanExtra("feed_is_cat", false);
+ String feedTitle = i.getStringExtra("feed_title");
+
+ tmpFeed = new Feed(feedId, feedTitle, isCat);
+
+ GlobalState.getInstance().m_loadedArticles.clear();
+ } else {
+ tmpFeed = i.getParcelableExtra("feed");
+ }
+
+ final Feed feed = tmpFeed;
+
+ final Article article = i.getParcelableExtra("article");
+ final String searchQuery = i.getStringExtra("searchQuery");
+
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+
+ ft.replace(R.id.headlines_fragment, new LoadingFragment(), null);
+ ft.replace(R.id.article_fragment, new LoadingFragment(), null);
+
+ ft.commit();
+
+ setTitle(feed.title);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+
+ HeadlinesFragment hf = new HeadlinesFragment();
+ hf.initialize(feed, article);
+ hf.setSearchQuery(searchQuery);
+
+ ArticlePager af = new ArticlePager();
+ af.initialize(article != null ? hf.getArticleById(article.id) : new Article(), feed);
+ af.setSearchQuery(searchQuery);
+
+ ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
+ ft.replace(R.id.article_fragment, af, FRAG_ARTICLE);
+
+ ft.commit();
+ }
+ }, 25);
+
+ }
+ }
+
+ /* if (!isCompatMode()) {
+ ((ViewGroup)findViewById(R.id.headlines_fragment)).setLayoutTransition(new LayoutTransition());
+ ((ViewGroup)findViewById(R.id.article_fragment)).setLayoutTransition(new LayoutTransition());
+ } */
+ }
+
+ @Override
+ protected void refresh() {
+ super.refresh();
+
+
+ }
+
+ @Override
+ protected void loginSuccess(boolean refresh) {
+ Log.d(TAG, "loginSuccess");
+
+ setLoadingStatus(R.string.blank, false);
+ findViewById(R.id.loading_container).setVisibility(View.GONE);
+
+ initMenu();
+
+ if (refresh) refresh();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ GlobalState.getInstance().save(out);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ overridePendingTransition(0, R.anim.right_slide_out);
+ return true;
+ default:
+ Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void initMenu() {
+ super.initMenu();
+
+ if (m_menu != null && getSessionId() != null) {
+ m_menu.setGroupVisible(R.id.menu_group_feeds, false);
+
+ //HeadlinesFragment hf = (HeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ m_menu.setGroupVisible(R.id.menu_group_headlines, !isPortrait() && !isSmallScreen());
+ m_menu.findItem(R.id.headlines_toggle_sidebar).setVisible(!isPortrait() && !isSmallScreen());
+
+ ArticlePager af = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ m_menu.setGroupVisible(R.id.menu_group_article, af != null);
+
+ if (af != null) {
+ if (af.getSelectedArticle() != null && af.getSelectedArticle().attachments != null && af.getSelectedArticle().attachments.size() > 0) {
+ if (!isCompatMode() && (isSmallScreen() || !isPortrait())) {
+ m_menu.findItem(R.id.toggle_attachments).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ m_menu.findItem(R.id.toggle_attachments).setVisible(true);
+ } else {
+ if (!isCompatMode()) {
+ m_menu.findItem(R.id.toggle_attachments).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ }
+ m_menu.findItem(R.id.toggle_attachments).setVisible(false);
+ }
+ }
+
+ m_menu.findItem(R.id.search).setVisible(false);
+ }
+ }
+
+ @Override
+ public void onArticleListSelectionChange(ArticleList m_selectedArticles) {
+ initMenu();
+ }
+
+ @Override
+ public void onArticleSelected(Article article) {
+ onArticleSelected(article, true);
+ }
+
+ @Override
+ public void onArticleSelected(Article article, boolean open) {
+
+ if (article == null) return;
+
+ if (article.unread) {
+ article.unread = false;
+ saveArticleUnread(article);
+ }
+
+ if (open) {
+
+ final Article fArticle = article;
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ ArticlePager af = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ if (af != null) {
+ af.setActiveArticle(fArticle);
+ }
+ }
+ }, 10);
+
+ } else {
+ HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+ if (hf != null) {
+ hf.setActiveArticle(article);
+ }
+ }
+
+ GlobalState.getInstance().m_activeArticle = article;
+
+ initMenu();
+
+ }
+
+ @Override
+ public void onHeadlinesLoaded(boolean appended) {
+ HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (hf != null) {
+ Article article = hf.getActiveArticle();
+
+ if (article == null && hf.getAllArticles().size() > 0) {
+ article = hf.getAllArticles().get(0);
+
+ hf.setActiveArticle(article);
+
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+
+ ft.replace(R.id.article_fragment, new LoadingFragment(), null);
+
+ ft.commitAllowingStateLoss();
+
+ final Article fArticle = article;
+ final Feed fFeed = hf.getFeed();
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+
+ ArticlePager af = new ArticlePager();
+ af.initialize(fArticle, fFeed);
+
+ ft.replace(R.id.article_fragment, af, FRAG_ARTICLE);
+ ft.commitAllowingStateLoss();
+ }
+ }, 10);
+ }
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ overridePendingTransition(0, R.anim.right_slide_out);
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java b/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java
new file mode 100644
index 00000000..5494bb2b
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java
@@ -0,0 +1,11 @@
+package org.fox.ttrss;
+
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.ArticleList;
+
+public interface HeadlinesEventListener {
+ void onArticleListSelectionChange(ArticleList m_selectedArticles);
+ void onArticleSelected(Article article);
+ void onArticleSelected(Article article, boolean open);
+ void onHeadlinesLoaded(boolean appended);
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java
new file mode 100644
index 00000000..32670252
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java
@@ -0,0 +1,1179 @@
+package org.fox.ttrss;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.TimeZone;
+
+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 org.fox.ttrss.util.TypefaceCache;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources.Theme;
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.OpenableColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.text.Html;
+import android.util.DisplayMetrics;
+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.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.google.gson.JsonElement;
+import com.nostra13.universalimageloader.core.DisplayImageOptions;
+import com.nostra13.universalimageloader.core.ImageLoader;
+import com.nostra13.universalimageloader.core.assist.FailReason;
+import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
+
+public class HeadlinesFragment extends Fragment implements OnItemClickListener, OnScrollListener {
+ public static enum ArticlesSelection { ALL, NONE, UNREAD };
+
+ public static final int HEADLINES_REQUEST_SIZE = 30;
+ public static final int HEADLINES_BUFFER_MAX = 500;
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ private Feed m_feed;
+ private Article m_activeArticle;
+ private String m_searchQuery = "";
+ private boolean m_refreshInProgress = false;
+ private boolean m_autoCatchupDisabled = false;
+
+ private SharedPreferences m_prefs;
+
+ private ArticleListAdapter m_adapter;
+ private ArticleList m_articles = GlobalState.getInstance().m_loadedArticles;
+ private ArticleList m_selectedArticles = new ArticleList();
+ private ArticleList m_readArticles = new ArticleList();
+ private HeadlinesEventListener m_listener;
+ private OnlineActivity m_activity;
+ private SwipeRefreshLayout m_swipeLayout;
+ private int m_maxImageSize = 0;
+
+ public ArticleList getSelectedArticles() {
+ return m_selectedArticles;
+ }
+
+ public void initialize(Feed feed) {
+ m_feed = feed;
+ }
+
+ public void initialize(Feed feed, Article activeArticle) {
+ m_feed = feed;
+
+ if (activeArticle != null) {
+ m_activeArticle = getArticleById(activeArticle.id);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo();
+
+ switch (item.getItemId()) {
+ case R.id.set_labels:
+ if (true) {
+ Article article = getArticleAtPosition(info.position);
+
+ if (article != null) {
+ if (m_activity.getApiLevel() != 7) {
+ m_activity.editArticleLabels(article);
+ } else {
+ m_activity.toast(R.string.server_function_not_available);
+ }
+ }
+ }
+ return true;
+ case R.id.article_set_note:
+ if (true) {
+ Article article = getArticleAtPosition(info.position);
+
+ if (article != null) {
+ m_activity.editArticleNote(article);
+ }
+ }
+ return true;
+
+ case R.id.headlines_article_link_copy:
+ if (true) {
+ Article article = getArticleAtPosition(info.position);
+
+ if (article != null) {
+ m_activity.copyToClipboard(article.link);
+ }
+ }
+ return true;
+ case R.id.headlines_article_link_open:
+ if (true) {
+ Article article = getArticleAtPosition(info.position);
+
+ if (article != null) {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(article.link));
+ startActivity(browserIntent);
+
+ if (article.unread) {
+ article.unread = false;
+ m_activity.saveArticleUnread(article);
+ }
+ }
+ }
+ return true;
+ case R.id.selection_toggle_marked:
+ if (true) {
+ ArticleList selected = getSelectedArticles();
+
+ if (selected.size() > 0) {
+ for (Article a : selected)
+ a.marked = !a.marked;
+
+ m_activity.toggleArticlesMarked(selected);
+ //updateHeadlines();
+ } else {
+ Article article = getArticleAtPosition(info.position);
+ if (article != null) {
+ article.marked = !article.marked;
+ m_activity.saveArticleMarked(article);
+ //updateHeadlines();
+ }
+ }
+ m_adapter.notifyDataSetChanged();
+ }
+ return true;
+ case R.id.selection_toggle_published:
+ if (true) {
+ ArticleList selected = getSelectedArticles();
+
+ if (selected.size() > 0) {
+ for (Article a : selected)
+ a.published = !a.published;
+
+ m_activity.toggleArticlesPublished(selected);
+ //updateHeadlines();
+ } else {
+ Article article = getArticleAtPosition(info.position);
+ if (article != null) {
+ article.published = !article.published;
+ m_activity.saveArticlePublished(article);
+ //updateHeadlines();
+ }
+ }
+ m_adapter.notifyDataSetChanged();
+ }
+ return true;
+ case R.id.selection_toggle_unread:
+ if (true) {
+ ArticleList selected = getSelectedArticles();
+
+ if (selected.size() > 0) {
+ for (Article a : selected)
+ a.unread = !a.unread;
+
+ m_activity.toggleArticlesUnread(selected);
+ //updateHeadlines();
+ } else {
+ Article article = getArticleAtPosition(info.position);
+ if (article != null) {
+ article.unread = !article.unread;
+ m_activity.saveArticleUnread(article);
+ //updateHeadlines();
+ }
+ }
+ m_adapter.notifyDataSetChanged();
+ }
+ return true;
+ case R.id.headlines_share_article:
+ if (true) {
+ Article article = getArticleAtPosition(info.position);
+ if (article != null)
+ m_activity.shareArticle(article);
+ }
+ return true;
+ case R.id.catchup_above:
+ if (true) {
+ Article article = getArticleAtPosition(info.position);
+ if (article != null) {
+ ArticleList articles = getAllArticles();
+ ArticleList tmp = new ArticleList();
+ for (Article a : articles) {
+ if (article.id == a.id)
+ break;
+
+ if (a.unread) {
+ a.unread = false;
+ tmp.add(a);
+ }
+ }
+ if (tmp.size() > 0) {
+ m_activity.toggleArticlesUnread(tmp);
+ //updateHeadlines();
+ }
+ }
+ m_adapter.notifyDataSetChanged();
+ }
+ 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.headlines_context_menu, menu);
+
+ if (m_selectedArticles.size() > 0) {
+ menu.setHeaderTitle(R.string.headline_context_multiple);
+ menu.setGroupVisible(R.id.menu_group_single_article, false);
+ } else {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ Article article = getArticleAtPosition(info.position);
+ menu.setHeaderTitle(Html.fromHtml(article.title));
+ menu.setGroupVisible(R.id.menu_group_single_article, true);
+ }
+
+ menu.findItem(R.id.set_labels).setEnabled(m_activity.getApiLevel() >= 1);
+ menu.findItem(R.id.article_set_note).setEnabled(m_activity.getApiLevel() >= 1);
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ if (savedInstanceState != null) {
+ m_feed = savedInstanceState.getParcelable("feed");
+ //m_articles = savedInstanceState.getParcelable("articles");
+ m_activeArticle = savedInstanceState.getParcelable("activeArticle");
+ m_selectedArticles = savedInstanceState.getParcelable("selectedArticles");
+ m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery");
+ }
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
+ m_maxImageSize = (int) (128 * metrics.density + 0.5);
+
+ Log.d(TAG, "maxImageSize=" + m_maxImageSize);
+
+ View view = inflater.inflate(R.layout.headlines_fragment, container, false);
+
+ m_swipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.headlines_swipe_container);
+
+ m_swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ refresh(false, true);
+ }
+ });
+
+ if (!m_activity.isCompatMode()) {
+ m_swipeLayout.setColorScheme(android.R.color.holo_green_dark,
+ android.R.color.holo_red_dark,
+ android.R.color.holo_blue_dark,
+ android.R.color.holo_orange_dark);
+ }
+
+
+ ListView list = (ListView)view.findViewById(R.id.headlines);
+ m_adapter = new ArticleListAdapter(getActivity(), R.layout.headlines_row, (ArrayList<Article>)m_articles);
+
+ /* if (!m_activity.isCompatMode()) {
+ AnimationSet set = new AnimationSet(true);
+
+ Animation animation = new AlphaAnimation(0.0f, 1.0f);
+ animation.setDuration(500);
+ set.addAnimation(animation);
+
+ animation = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, 50.0f,Animation.RELATIVE_TO_SELF, 0.0f,
+ Animation.RELATIVE_TO_SELF, 0.0f,Animation.RELATIVE_TO_SELF, 0.0f
+ );
+ animation.setDuration(1000);
+ set.addAnimation(animation);
+
+ LayoutAnimationController controller = new LayoutAnimationController(set, 0.5f);
+
+ list.setLayoutAnimation(controller);
+ } */
+
+ list.setAdapter(m_adapter);
+ list.setOnItemClickListener(this);
+ list.setOnScrollListener(this);
+ //list.setEmptyView(view.findViewById(R.id.no_headlines));
+ registerForContextMenu(list);
+
+ //m_activity.m_pullToRefreshAttacher.addRefreshableView(list, this);
+
+ //if (m_activity.isSmallScreen())
+ //view.findViewById(R.id.headlines_fragment).setPadding(0, 0, 0, 0);
+
+ Log.d(TAG, "onCreateView, feed=" + m_feed);
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (GlobalState.getInstance().m_activeArticle != null) {
+ m_activeArticle = GlobalState.getInstance().m_activeArticle;
+ GlobalState.getInstance().m_activeArticle = null;
+ }
+
+ if (m_activeArticle != null) {
+ setActiveArticle(m_activeArticle);
+ }
+
+ if (m_articles.size() == 0 || !m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
+ if (m_activity.getSupportFragmentManager().findFragmentByTag(CommonActivity.FRAG_ARTICLE) == null) {
+ refresh(false);
+ GlobalState.getInstance().m_activeFeed = m_feed;
+ }
+ } else {
+ notifyUpdated();
+ }
+
+ m_activity.initMenu();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ m_activity = (OnlineActivity) activity;
+ m_listener = (HeadlinesEventListener) activity;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> av, View view, int position, long id) {
+ ListView list = (ListView)av;
+
+ Log.d(TAG, "onItemClick=" + position);
+
+ if (list != null) {
+ Article article = (Article)list.getItemAtPosition(position);
+ if (article.id >= 0) {
+ m_listener.onArticleSelected(article);
+
+ // only set active article when it makes sense (in HeadlinesActivity)
+ if (getActivity().findViewById(R.id.article_fragment) != null) {
+ m_activeArticle = article;
+ }
+
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+ }
+
+ public void refresh(boolean append) {
+ refresh(append, false);
+ }
+
+ @SuppressWarnings({ "serial" })
+ public void refresh(boolean append, boolean userInitiated) {
+ if (m_activity != null && m_feed != null) {
+ m_refreshInProgress = true;
+
+ m_swipeLayout.setRefreshing(true);
+ m_activity.setProgressBarVisibility(true);
+
+ if (!m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
+ append = false;
+ }
+
+ // new stuff may appear on top, scroll back to show it
+ if (!append) {
+ if (getView() != null) {
+ Log.d(TAG, "scroll hack");
+ ListView list = (ListView)getView().findViewById(R.id.headlines);
+ m_autoCatchupDisabled = true;
+ list.setSelection(0);
+ m_autoCatchupDisabled = false;
+ list.setEmptyView(null);
+ m_adapter.clear();
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ final boolean fappend = append;
+ final String sessionId = m_activity.getSessionId();
+ final boolean isCat = m_feed.is_cat;
+
+ HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity, m_feed) {
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ m_activity.setProgress(Math.round((((float)progress[0] / (float)progress[1]) * 10000)));
+ }
+
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (isDetached()) return;
+
+ if (getView() != null) {
+ ListView list = (ListView)getView().findViewById(R.id.headlines);
+
+ if (list != null) {
+ list.setEmptyView(getView().findViewById(R.id.no_headlines));
+ }
+ }
+
+ m_activity.setProgressBarVisibility(false);
+
+ super.onPostExecute(result);
+
+ if (isAdded()) {
+ m_swipeLayout.setRefreshing(false);
+ }
+
+ if (result != null) {
+ m_refreshInProgress = false;
+
+ if (m_articles.indexOf(m_activeArticle) == -1)
+ m_activeArticle = null;
+
+ m_adapter.notifyDataSetChanged();
+ m_listener.onHeadlinesLoaded(fappend);
+
+ } else {
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ m_activity.login(true);
+ } else {
+ m_activity.setLoadingStatus(getErrorMessage(), false);
+ }
+ }
+ }
+ };
+
+ int skip = 0;
+
+ if (append) {
+ // adaptive, all_articles, marked, published, unread
+ String viewMode = m_activity.getViewMode();
+ int numUnread = 0;
+ int numAll = m_articles.size();
+
+ for (Article a : m_articles) {
+ if (a.unread) ++numUnread;
+ }
+
+ if ("marked".equals(viewMode)) {
+ skip = numAll;
+ } else if ("published".equals(viewMode)) {
+ skip = numAll;
+ } else if ("unread".equals(viewMode)) {
+ skip = numUnread;
+ } else if (m_searchQuery != null && m_searchQuery.length() > 0) {
+ skip = numAll;
+ } else if ("adaptive".equals(viewMode)) {
+ skip = numUnread > 0 ? numUnread : numAll;
+ } else {
+ skip = numAll;
+ }
+
+ } else {
+ m_activity.setLoadingStatus(R.string.blank, true);
+ }
+
+ final int fskip = skip;
+
+ final boolean allowForceUpdate = m_activity.getApiLevel() >= 9 &&
+ !m_feed.is_cat && m_feed.id > 0 && !append && userInitiated &&
+ skip == 0;
+
+ Log.d(TAG, "allowForceUpdate=" + allowForceUpdate + " userInitiated=" + userInitiated);
+
+
+ req.setOffset(skip);
+
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ put("op", "getHeadlines");
+ put("sid", sessionId);
+ put("feed_id", String.valueOf(m_feed.id));
+ put("show_content", "true");
+ put("include_attachments", "true");
+ put("view_mode", m_activity.getViewMode());
+ put("limit", String.valueOf(HEADLINES_REQUEST_SIZE));
+ put("offset", String.valueOf(0));
+ put("skip", String.valueOf(fskip));
+ put("include_nested", "true");
+ put("order_by", m_prefs.getBoolean("oldest_first", false) ? "date_reverse" : "");
+
+ if (isCat) put("is_cat", "true");
+
+ if (allowForceUpdate) {
+ put("force_update", "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);
+
+ out.setClassLoader(getClass().getClassLoader());
+ out.putParcelable("feed", m_feed);
+ //out.putParcelable("articles", m_articles);
+ out.putParcelable("activeArticle", m_activeArticle);
+ out.putParcelable("selectedArticles", m_selectedArticles);
+ out.putCharSequence("searchQuery", m_searchQuery);
+ }
+
+ /* private void setLoadingStatus(int status, boolean showProgress) {
+ if (getView() != null) {
+ TextView tv = (TextView)getView().findViewById(R.id.loading_message);
+
+ if (tv != null) {
+ tv.setText(status);
+ }
+ }
+
+ if (getActivity() != null)
+ getActivity().setProgressBarIndeterminateVisibility(showProgress);
+ } */
+
+ /* private class HeadlinesRequest extends ApiRequest {
+ int m_offset = 0;
+
+ public HeadlinesRequest(Context context) {
+ super(context);
+ }
+
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ try {
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
+ Type listType = new TypeToken<List<Article>>() {}.getType();
+ final List<Article> 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
+ 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);
+
+ m_canLoadMore = true;
+ } else {
+ m_canLoadMore = false;
+ }
+
+ m_adapter.notifyDataSetChanged();
+
+ if (m_articles.size() == 0)
+ setLoadingStatus(R.string.no_headlines_to_display, false);
+ else
+ setLoadingStatus(R.string.blank, false);
+
+ m_refreshInProgress = false;
+
+ return;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (m_lastError == ApiError.LOGIN_FAILED) {
+ m_activity.login();
+ } else {
+ setLoadingStatus(getErrorMessage(), false);
+ }
+ m_refreshInProgress = false;
+ }
+
+ public void setOffset(int skip) {
+ m_offset = skip;
+ }
+ } */
+
+ static class HeadlineViewHolder {
+ public TextView titleView;
+ public TextView feedTitleView;
+ public ImageView markedView;
+ public ImageView publishedView;
+ public TextView excerptView;
+ public ImageView flavorImageView;
+ public TextView authorView;
+ public TextView dateView;
+ public CheckBox selectionBoxView;
+ public ImageView menuButtonView;
+ public ViewGroup flavorImageHolder;
+
+ }
+
+ private class ArticleListAdapter extends ArrayAdapter<Article> {
+ private ArrayList<Article> items;
+
+ public static final int VIEW_NORMAL = 0;
+ public static final int VIEW_UNREAD = 1;
+ public static final int VIEW_SELECTED = 2;
+ public static final int VIEW_SELECTED_UNREAD = 3;
+ public static final int VIEW_LOADMORE = 4;
+
+ public static final int VIEW_COUNT = VIEW_LOADMORE+1;
+
+ private final Integer[] origTitleColors = new Integer[VIEW_COUNT];
+ private final int titleHighScoreUnreadColor;
+
+ public ArticleListAdapter(Context context, int textViewResourceId, ArrayList<Article> items) {
+ super(context, textViewResourceId, items);
+ this.items = items;
+
+ Theme theme = context.getTheme();
+ TypedValue tv = new TypedValue();
+ theme.resolveAttribute(R.attr.headlineTitleHighScoreUnreadTextColor, tv, true);
+ titleHighScoreUnreadColor = tv.data;
+ }
+
+ public int getViewTypeCount() {
+ return VIEW_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Article a = items.get(position);
+
+ if (a.id == -1) {
+ return VIEW_LOADMORE;
+ } else if (m_activeArticle != null && a.id == m_activeArticle.id && a.unread) {
+ return VIEW_SELECTED_UNREAD;
+ } else if (m_activeArticle != null && a.id == m_activeArticle.id) {
+ return VIEW_SELECTED;
+ } else if (a.unread) {
+ return VIEW_UNREAD;
+ } else {
+ return VIEW_NORMAL;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ View v = convertView;
+
+ final Article article = items.get(position);
+ HeadlineViewHolder holder;
+
+ int headlineFontSize = Integer.parseInt(m_prefs.getString("headlines_font_size_sp", "13"));
+ int headlineSmallFontSize = Math.max(10, Math.min(18, headlineFontSize - 2));
+
+ if (v == null) {
+ int layoutId = R.layout.headlines_row;
+
+ switch (getItemViewType(position)) {
+ case VIEW_LOADMORE:
+ layoutId = R.layout.headlines_row_loadmore;
+ break;
+ case VIEW_UNREAD:
+ layoutId = R.layout.headlines_row_unread;
+ break;
+ case VIEW_SELECTED:
+ layoutId = R.layout.headlines_row_selected;
+ break;
+ case VIEW_SELECTED_UNREAD:
+ layoutId = R.layout.headlines_row_selected_unread;
+ break;
+ }
+
+ LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(layoutId, null);
+
+ holder = new HeadlineViewHolder();
+ holder.titleView = (TextView)v.findViewById(R.id.title);
+
+ holder.feedTitleView = (TextView)v.findViewById(R.id.feed_title);
+ holder.markedView = (ImageView)v.findViewById(R.id.marked);
+ holder.publishedView = (ImageView)v.findViewById(R.id.published);
+ holder.excerptView = (TextView)v.findViewById(R.id.excerpt);
+ holder.flavorImageView = (ImageView) v.findViewById(R.id.flavor_image);
+ holder.authorView = (TextView)v.findViewById(R.id.author);
+ holder.dateView = (TextView) v.findViewById(R.id.date);
+ holder.selectionBoxView = (CheckBox) v.findViewById(R.id.selected);
+ holder.menuButtonView = (ImageView) v.findViewById(R.id.article_menu_button);
+ holder.flavorImageHolder = (ViewGroup) v.findViewById(R.id.flavorImageHolder);
+
+ v.setTag(holder);
+
+ // http://code.google.com/p/android/issues/detail?id=3414
+ ((ViewGroup)v).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ } else {
+ holder = (HeadlineViewHolder) v.getTag();
+ }
+
+ if (holder.titleView != null) {
+ holder.titleView.setText(Html.fromHtml(article.title));
+
+ if (m_prefs.getBoolean("enable_condensed_fonts", false)) {
+ Typeface tf = TypefaceCache.get(m_activity, "sans-serif-condensed", article.unread ? Typeface.BOLD : Typeface.NORMAL);
+
+ if (tf != null && !tf.equals(holder.titleView.getTypeface())) {
+ holder.titleView.setTypeface(tf);
+ }
+
+ holder.titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, headlineFontSize + 5));
+ } else {
+ holder.titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, headlineFontSize + 3));
+ }
+
+ adjustTitleTextView(article.score, holder.titleView, position);
+ }
+
+
+
+ if (holder.feedTitleView != null) {
+ if (article.feed_title != null && (m_feed.is_cat || m_feed.id < 0)) {
+
+ /* if (article.feed_title.length() > 20)
+ ft.setText(article.feed_title.substring(0, 20) + "...");
+ else */
+
+ holder.feedTitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize);
+ holder.feedTitleView.setText(article.feed_title);
+
+ } else {
+ holder.feedTitleView.setVisibility(View.GONE);
+ }
+
+ }
+
+
+
+ if (holder.markedView != null) {
+ holder.markedView.setImageResource(article.marked ? R.drawable.ic_star_full : R.drawable.ic_star_empty);
+
+ holder.markedView.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ article.marked = !article.marked;
+ m_adapter.notifyDataSetChanged();
+
+ m_activity.saveArticleMarked(article);
+ }
+ });
+ }
+
+
+
+ if (holder.publishedView != null) {
+ holder.publishedView.setImageResource(article.published ? R.drawable.ic_published : R.drawable.ic_unpublished);
+
+ holder.publishedView.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ article.published = !article.published;
+ m_adapter.notifyDataSetChanged();
+
+ m_activity.saveArticlePublished(article);
+ }
+ });
+ }
+
+ String articleContent = article.content != null ? article.content : "";
+
+ if (holder.excerptView != null) {
+ if (!m_prefs.getBoolean("headlines_show_content", true)) {
+ holder.excerptView.setVisibility(View.GONE);
+ } else {
+ String excerpt = Jsoup.parse(articleContent).text();
+
+ if (excerpt.length() > CommonActivity.EXCERPT_MAX_SIZE)
+ excerpt = excerpt.substring(0, CommonActivity.EXCERPT_MAX_SIZE) + "...";
+
+ holder.excerptView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineFontSize);
+ holder.excerptView.setText(excerpt);
+ }
+ }
+
+
+
+ if (holder.flavorImageView != null && m_prefs.getBoolean("headlines_show_flavor_image", true)) {
+ holder.flavorImageView.setVisibility(View.GONE);
+
+ Document doc = Jsoup.parse(articleContent);
+
+ Element img = doc.select("img").first();
+ if (doc != null) {
+
+ if (img != null) {
+ String imgSrc = img.attr("src");
+
+ // retarded schema-less urls
+ if (imgSrc.indexOf("//") == 0)
+ imgSrc = "http:" + imgSrc;
+
+ DisplayImageOptions options = new DisplayImageOptions.Builder().
+ cacheInMemory(true).
+ cacheOnDisk(true).
+ build();
+
+ final ImageView flavorImageView = holder.flavorImageView;
+
+ ImageLoader.getInstance().displayImage(imgSrc, holder.flavorImageView, options, new ImageLoadingListener() {
+
+ @Override
+ public void onLoadingCancelled(String arg0,
+ View arg1) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onLoadingComplete(String arg0,
+ View arg1, Bitmap arg2) {
+ // TODO Auto-generated method stub
+
+ if (!isAdded()) return;
+
+ if (arg2.getWidth() > 128 && arg2.getHeight() > 128) {
+ if (arg0 != null && !arg0.equals(arg1.getTag())) {
+ if (!m_activity.isCompatMode() && flavorImageView.getVisibility() != View.VISIBLE) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(flavorImageView, "alpha", 0f, 1f);
+ anim.setDuration(500);
+ anim.start();
+ }
+ }
+
+ flavorImageView.setTag(arg0);
+ flavorImageView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onLoadingFailed(String arg0,
+ View arg1, FailReason arg2) {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public void onLoadingStarted(String arg0,
+ View arg1) {
+ // TODO Auto-generated method stub
+
+ }
+
+ });
+ }
+
+ }
+ } else if (holder.flavorImageHolder != null) {
+ holder.flavorImageHolder.setVisibility(View.GONE);
+ }
+
+ String articleAuthor = article.author != null ? article.author : "";
+
+
+ if (holder.authorView != null) {
+ holder.authorView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize);
+
+ if (articleAuthor.length() > 0) {
+ holder.authorView.setText(getString(R.string.author_formatted, articleAuthor));
+ } else {
+ holder.authorView.setText("");
+ }
+ }
+
+ if (holder.dateView != null) {
+ holder.dateView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize);
+
+ Date d = new Date((long)article.updated * 1000);
+ DateFormat df = new SimpleDateFormat("MMM dd, HH:mm");
+ df.setTimeZone(TimeZone.getDefault());
+ holder.dateView.setText(df.format(d));
+ }
+
+
+ if (holder.selectionBoxView != null) {
+ holder.selectionBoxView.setChecked(m_selectedArticles.contains(article));
+ holder.selectionBoxView.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View view) {
+ CheckBox cb = (CheckBox)view;
+
+ if (cb.isChecked()) {
+ if (!m_selectedArticles.contains(article))
+ m_selectedArticles.add(article);
+ } else {
+ m_selectedArticles.remove(article);
+ }
+
+ m_listener.onArticleListSelectionChange(m_selectedArticles);
+
+ Log.d(TAG, "num selected: " + m_selectedArticles.size());
+ }
+ });
+ }
+
+ if (holder.menuButtonView != null) {
+ //if (m_activity.isDarkTheme())
+ // ib.setImageResource(R.drawable.ic_mailbox_collapsed_holo_dark);
+
+ holder.menuButtonView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().openContextMenu(v);
+ }
+ });
+ }
+
+ return v;
+ }
+
+ private void adjustTitleTextView(int score, TextView tv, int position) {
+ int viewType = getItemViewType(position);
+ if (origTitleColors[viewType] == null)
+ // store original color
+ origTitleColors[viewType] = Integer.valueOf(tv.getCurrentTextColor());
+
+ if (score < -500) {
+ tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
+ } else if (score > 500) {
+ tv.setTextColor(titleHighScoreUnreadColor);
+ tv.setPaintFlags(tv.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ } else {
+ tv.setTextColor(origTitleColors[viewType].intValue());
+ tv.setPaintFlags(tv.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ }
+ }
+ }
+
+
+ public void notifyUpdated() {
+ m_adapter.notifyDataSetChanged();
+ }
+
+ public ArticleList getAllArticles() {
+ return m_articles;
+ }
+
+ public void setActiveArticle(Article article) {
+ if (article != m_activeArticle) {
+ m_activeArticle = article;
+ m_adapter.notifyDataSetChanged();
+
+ ListView list = (ListView)getView().findViewById(R.id.headlines);
+
+ if (list != null && article != null) {
+ int position = m_adapter.getPosition(article);
+ list.setSelection(position);
+ }
+ }
+ }
+
+ public void setSelection(ArticlesSelection select) {
+ m_selectedArticles.clear();
+
+ if (select != ArticlesSelection.NONE) {
+ for (Article a : m_articles) {
+ if (select == ArticlesSelection.ALL || select == ArticlesSelection.UNREAD && a.unread) {
+ m_selectedArticles.add(a);
+ }
+ }
+ }
+
+ m_adapter.notifyDataSetChanged();
+ }
+
+ public Article getArticleAtPosition(int position) {
+ try {
+ return m_adapter.getItem(position);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ } catch (NullPointerException e) {
+ return null;
+ }
+ }
+
+ public Article getArticleById(int id) {
+ for (Article a : m_articles) {
+ if (a.id == id)
+ return a;
+ }
+ return null;
+ }
+
+ public ArticleList getUnreadArticles() {
+ ArticleList tmp = new ArticleList();
+ for (Article a : m_articles) {
+ if (a.unread) tmp.add(a);
+ }
+ return tmp;
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (!m_refreshInProgress && m_articles.findById(-1) != null && firstVisibleItem + visibleItemCount == m_articles.size()) {
+ refresh(true);
+ }
+
+ if (m_prefs.getBoolean("headlines_mark_read_scroll", false) && firstVisibleItem > 0 && !m_autoCatchupDisabled) {
+ Article a = m_articles.get(firstVisibleItem - 1);
+
+ if (a != null && a.unread) {
+ a.unread = false;
+ m_readArticles.add(a);
+ m_feed.unread--;
+ }
+ }
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == SCROLL_STATE_IDLE && m_prefs.getBoolean("headlines_mark_read_scroll", false)) {
+ if (!m_readArticles.isEmpty()) {
+ m_activity.toggleArticlesUnread(m_readArticles);
+ m_activity.refresh(false);
+ m_readArticles.clear();
+ }
+ }
+ }
+
+ public Article getActiveArticle() {
+ return m_activeArticle;
+ }
+
+ public int getArticlePosition(Article article) {
+ try {
+ return m_adapter.getPosition(article);
+ } catch (NullPointerException e) {
+ return -1;
+ }
+ }
+
+ public String getSearchQuery() {
+ return m_searchQuery;
+ }
+
+ public void setSearchQuery(String query) {
+ if (!m_searchQuery.equals(query)) {
+ m_searchQuery = query;
+ refresh(false);
+ }
+ }
+
+ public Feed getFeed() {
+ return m_feed;
+ }
+
+ /* class DownloadFlavorImagesTask extends AsyncTask<ImageView, Void, Bitmap> {
+ ImageView imageView = null;
+ @Override
+ protected Bitmap doInBackground(ImageView... imageViews) {
+ this.imageView = imageViews[0];
+ return download((URL)imageView.getTag());
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ if (result != null) {
+ imageView.setImageBitmap(result);
+ imageView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private Bitmap download(URL url) {
+ try {
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+ conn.setDoInput(true);
+ conn.setUseCaches(true);
+ conn.connect();
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+ byte[] buf = new byte[256];
+ int read = 0;
+
+ while ((read = conn.getInputStream().read(buf)) >= 0) {
+ bos.write(buf, 0, read);
+ }
+
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+
+ byte[] bitmap = bos.toByteArray();
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(bitmap, 0, bitmap.length, options);
+ options.inJustDecodeBounds = false;
+
+ int inSampleSize = CommonActivity.calculateInSampleSize(options, 128, 128);
+
+ Bitmap decodedBitmap = BitmapFactory.decodeByteArray(bitmap, 0, bitmap.length, options);
+
+ return decodedBitmap;
+ } catch (OutOfMemoryError e) {
+ Log.d(TAG, "OOM while trying to decode headline flavor image. :(");
+ e.printStackTrace();
+ } catch (IOException e) {
+ //
+ }
+
+ return null;
+ }
+ } */
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/LoadingFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/LoadingFragment.java
new file mode 100644
index 00000000..f0802b0a
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/LoadingFragment.java
@@ -0,0 +1,18 @@
+package org.fox.ttrss;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class LoadingFragment extends Fragment {
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.loading_fragment, container, false);
+
+ return view;
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/OnlineActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/OnlineActivity.java
new file mode 100644
index 00000000..e33a02b7
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/OnlineActivity.java
@@ -0,0 +1,1765 @@
+package org.fox.ttrss;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+
+import org.fox.ttrss.offline.OfflineActivity;
+import org.fox.ttrss.offline.OfflineDownloadService;
+import org.fox.ttrss.offline.OfflineUploadService;
+import org.fox.ttrss.share.SubscribeActivity;
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.ArticleList;
+import org.fox.ttrss.types.Feed;
+import org.fox.ttrss.types.Label;
+import org.fox.ttrss.widget.SmallWidgetProvider;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.select.Elements;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnMultiChoiceClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v7.view.ActionMode;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.EditText;
+import android.widget.SearchView;
+import android.widget.TextView;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import com.nostra13.universalimageloader.core.ImageLoader;
+import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
+
+public class OnlineActivity extends CommonActivity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private final static int TRIAL_DAYS = 8;
+
+ protected SharedPreferences m_prefs;
+ protected Menu m_menu;
+
+ protected int m_offlineModeStatus = 0;
+
+ private ActionMode m_headlinesActionMode;
+ private HeadlinesActionModeCallback m_headlinesActionModeCallback;
+
+ private String m_lastImageHitTestUrl;
+
+ //protected PullToRefreshAttacher m_pullToRefreshAttacher;
+
+ protected abstract class OnLoginFinishedListener {
+ public abstract void OnLoginSuccess();
+ public abstract void OnLoginFailed();
+ };
+
+ private BroadcastReceiver m_broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context content, Intent intent) {
+
+ if (intent.getAction().equals(OfflineDownloadService.INTENT_ACTION_SUCCESS)) {
+
+ m_offlineModeStatus = 2;
+
+ switchOffline();
+
+ } else if (intent.getAction().equals(OfflineUploadService.INTENT_ACTION_SUCCESS)) {
+ Log.d(TAG, "offline upload service reports success");
+ toast(R.string.offline_sync_success);
+ }
+ }
+ };
+
+
+ @TargetApi(11)
+ private class HeadlinesActionModeCallback implements ActionMode.Callback {
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ m_headlinesActionMode = null;
+
+ HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (hf != null) {
+ ArticleList selected = hf.getSelectedArticles();
+ if (selected.size() > 0) {
+ selected.clear();
+ initMenu();
+ hf.notifyUpdated();
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.headlines_action_menu, menu);
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ onOptionsItemSelected(item);
+ return false;
+ }
+ };
+
+ protected String getSessionId() {
+ return GlobalState.getInstance().m_sessionId;
+ }
+
+ protected void setSessionId(String sessionId) {
+ GlobalState.getInstance().m_sessionId = sessionId;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ ApiRequest.disableConnectionReuseIfNecessary();
+
+ // we use that before parent onCreate so let's init locally
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ setAppTheme(m_prefs);
+
+ super.onCreate(savedInstanceState);
+
+ if (canUseProgress()) {
+ requestWindowFeature(Window.FEATURE_PROGRESS);
+ }
+
+ //requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ setProgressBarVisibility(false);
+ setProgressBarIndeterminateVisibility(false);
+
+// SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE);
+
+ SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE);
+
+ boolean isOffline = localPrefs.getBoolean("offline_mode_active", false);
+
+ Log.d(TAG, "m_isOffline=" + isOffline);
+
+ setContentView(R.layout.login);
+
+ setStatusBarTint();
+
+ ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()).build();
+ ImageLoader.getInstance().init(config);
+ ImageLoader.getInstance().clearDiskCache();
+
+ //m_pullToRefreshAttacher = PullToRefreshAttacher.get(this);
+
+ if (isOffline) {
+ switchOfflineSuccess();
+ } else {
+ checkTrial(false);
+
+ /* if (getIntent().getExtras() != null) {
+ Intent i = getIntent();
+ } */
+
+ if (savedInstanceState != null) {
+ m_offlineModeStatus = savedInstanceState.getInt("offlineModeStatus");
+ }
+
+ m_headlinesActionModeCallback = new HeadlinesActionModeCallback();
+ }
+ }
+
+ protected boolean canUseProgress() {
+ return GlobalState.getInstance().m_canUseProgress;
+ }
+
+ private void switchOffline() {
+ if (m_offlineModeStatus == 2) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ OnlineActivity.this)
+ .setMessage(R.string.dialog_offline_success)
+ .setPositiveButton(R.string.dialog_offline_go,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ m_offlineModeStatus = 0;
+
+ SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = localPrefs.edit();
+ editor.putBoolean("offline_mode_active", true);
+ editor.commit();
+
+ Intent offline = new Intent(
+ OnlineActivity.this,
+ OfflineActivity.class);
+ offline.putExtra("initial", true);
+ startActivity(offline);
+ finish();
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ m_offlineModeStatus = 0;
+
+ }
+ });
+
+ AlertDialog dlg = builder.create();
+ dlg.show();
+
+ } else if (m_offlineModeStatus == 0) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setMessage(R.string.dialog_offline_switch_prompt)
+ .setPositiveButton(R.string.dialog_offline_go,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ if (getSessionId() != null) {
+ Log.d(TAG, "offline: starting");
+
+ m_offlineModeStatus = 1;
+
+ Intent intent = new Intent(
+ OnlineActivity.this,
+ OfflineDownloadService.class);
+ intent.putExtra("sessionId", getSessionId());
+
+ startService(intent);
+ }
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ //
+ }
+ });
+
+ AlertDialog dlg = builder.create();
+ dlg.show();
+ } else if (m_offlineModeStatus == 1) {
+ cancelOfflineSync();
+ }
+ }
+
+ private boolean hasPendingOfflineData() {
+ try {
+ Cursor c = getReadableDb().query("articles",
+ new String[] { "COUNT(*)" }, "modified = 1", null, null, null,
+ null);
+ if (c.moveToFirst()) {
+ int modified = c.getInt(0);
+ c.close();
+
+ return modified > 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 onStop() {
+ super.onStop();
+
+ Intent initialUpdateIntent = new Intent(SmallWidgetProvider.FORCE_UPDATE_ACTION);
+ sendBroadcast(initialUpdateIntent);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ unregisterReceiver(m_broadcastReceiver);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ private void syncOfflineData() {
+ Log.d(TAG, "offlineSync: starting");
+
+ Intent intent = new Intent(
+ OnlineActivity.this,
+ OfflineUploadService.class);
+
+ intent.putExtra("sessionId", getSessionId());
+
+ 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 (getSessionId() != 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);
+ 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() {
+ login(false, null);
+ }
+
+ public void login(boolean refresh) {
+ login(refresh, null);
+ }
+
+ public void login(boolean refresh, OnLoginFinishedListener listener) {
+ if (m_prefs.getString("ttrss_url", "").trim().length() == 0) {
+
+ setLoadingStatus(R.string.login_need_configure, false);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.dialog_need_configure_prompt)
+ .setCancelable(false)
+ .setPositiveButton(R.string.dialog_open_preferences, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // launch preferences
+
+ Intent intent = new Intent(OnlineActivity.this,
+ PreferencesActivity.class);
+ startActivityForResult(intent, 0);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+
+ } else {
+ setLoadingStatus(R.string.login_in_progress, true);
+
+ LoginRequest ar = new LoginRequest(getApplicationContext(), refresh, listener);
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("op", "login");
+ put("user", m_prefs.getString("login", "").trim());
+ put("password", m_prefs.getString("password", "").trim());
+ }
+ };
+
+ ar.execute(map);
+
+ setLoadingStatus(R.string.login_in_progress, true);
+ }
+ }
+
+ protected void loginSuccess(boolean refresh) {
+ setLoadingStatus(R.string.blank, false);
+
+ initMenu();
+
+ Intent intent = new Intent(OnlineActivity.this, FeedsActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+
+ startActivityForResult(intent, 0);
+ overridePendingTransition(0, 0);
+
+ if (hasPendingOfflineData())
+ syncOfflineData();
+
+ finish();
+ }
+
+ public void checkTrial(boolean notify) {
+ boolean isTrial = getPackageManager().checkSignatures(
+ getPackageName(), "org.fox.ttrss.key") != PackageManager.SIGNATURE_MATCH;
+
+ if (isTrial) {
+ long firstStart = m_prefs.getLong("date_firstlaunch_trial", -1);
+
+ if (firstStart == -1) {
+ firstStart = System.currentTimeMillis();
+
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putLong("date_firstlaunch_trial", firstStart);
+ editor.commit();
+ }
+
+ if (!notify && System.currentTimeMillis() > firstStart + (TRIAL_DAYS * 24 * 60 * 60 * 1000)) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.trial_expired)
+ .setMessage(R.string.trial_expired_message)
+ .setCancelable(false)
+ .setPositiveButton(getString(R.string.trial_purchase),
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ openUnlockUrl();
+ finish();
+
+ }
+ })
+ .setNegativeButton(getString(R.string.cancel),
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ finish();
+
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+
+ } else {
+ long daysLeft = Math.round((firstStart + (TRIAL_DAYS * 24 * 60 * 60 * 1000) - System.currentTimeMillis()) / (24 * 60 * 60 * 1000));
+
+ if (notify) {
+ toast(getString(R.string.trial_mode_prompt, Long.valueOf(daysLeft)));
+ }
+ }
+ } else if (notify) {
+ //toast(R.string.trial_thanks);
+ }
+ }
+
+ private void openUnlockUrl() {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=org.fox.ttrss.key"));
+ startActivity(intent);
+ } catch (ActivityNotFoundException ae) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse("https://play.google.com/store/apps/details?id=org.fox.ttrss.key"));
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ toast(R.string.error_other_error);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ /* AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo(); */
+
+ final ArticlePager ap = (ArticlePager)getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ switch (item.getItemId()) {
+ case R.id.article_img_open:
+ if (getLastContentImageHitTestUrl() != null) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(getLastContentImageHitTestUrl()));
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ toast(R.string.error_other_error);
+ }
+ }
+ return true;
+ case R.id.article_img_copy:
+ if (getLastContentImageHitTestUrl() != null) {
+ copyToClipboard(getLastContentImageHitTestUrl());
+ }
+ return true;
+ case R.id.article_img_share:
+ if (getLastContentImageHitTestUrl() != null) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+
+ intent.setType("image/png");
+ intent.putExtra(Intent.EXTRA_SUBJECT, getLastContentImageHitTestUrl());
+ intent.putExtra(Intent.EXTRA_TEXT, getLastContentImageHitTestUrl());
+
+ startActivity(Intent.createChooser(intent, getLastContentImageHitTestUrl()));
+ }
+ return true;
+ case R.id.article_img_view_caption:
+ if (getLastContentImageHitTestUrl() != null) {
+
+ // Android doesn't give us an easy way to access title tags;
+ // we'll use Jsoup on the body text to grab the title text
+ // from the first image tag with this url. This will show
+ // the wrong text if an image is used multiple times.
+ Document doc = Jsoup.parse(ap.getSelectedArticle().content);
+ Elements es = doc.getElementsByAttributeValue("src", getLastContentImageHitTestUrl());
+ if (es.size() > 0){
+ if (es.get(0).hasAttr("title")){
+ Dialog dia = new Dialog(this);
+ if (es.get(0).hasAttr("alt")){
+ dia.setTitle(es.get(0).attr("alt"));
+ } else {
+ dia.setTitle(es.get(0).attr("title"));
+ }
+ TextView titleText = new TextView(this);
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ titleText.setPaddingRelative(24, 24, 24, 24);
+ } else {
+ titleText.setPadding(24, 24, 24, 24);
+ }
+
+ titleText.setTextSize(16);
+ titleText.setText(es.get(0).attr("title"));
+ dia.setContentView(titleText);
+ dia.show();
+ } else {
+ toast(R.string.no_caption_to_display);
+ }
+ } else {
+ toast(R.string.no_caption_to_display);
+ }
+ }
+ return true;
+ case R.id.article_link_share:
+ if (ap != null && ap.getSelectedArticle() != null) {
+ shareArticle(ap.getSelectedArticle());
+ }
+ return true;
+ case R.id.article_link_copy:
+ Log.d(TAG, "article_link_copy");
+ if (ap != null && ap.getSelectedArticle() != null) {
+ copyToClipboard(ap.getSelectedArticle().link);
+ }
+ return true;
+ default:
+ Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+ final ArticlePager ap = (ArticlePager)getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ switch (item.getItemId()) {
+ /* case android.R.id.home:
+ finish();
+ return true; */
+ case R.id.headlines_toggle_sidebar:
+ if (true && !isSmallScreen()) {
+ View v = findViewById(R.id.headlines_fragment);
+
+ if (v != null) {
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putBoolean("headlines_hide_sidebar", !m_prefs.getBoolean("headlines_hide_sidebar", false));
+ editor.commit();
+
+ v.setVisibility(m_prefs.getBoolean("headlines_hide_sidebar", false) ? View.GONE : View.VISIBLE);
+ }
+ }
+ return true;
+ case R.id.subscribe_to_feed:
+ Intent subscribe = new Intent(OnlineActivity.this, SubscribeActivity.class);
+ startActivityForResult(subscribe, 0);
+ return true;
+ case R.id.toggle_attachments:
+ if (true) {
+ Article article = ap.getSelectedArticle();
+
+ if (article != null && article.attachments != null && article.attachments.size() > 0) {
+ CharSequence[] items = new CharSequence[article.attachments.size()];
+ final CharSequence[] itemUrls = new CharSequence[article.attachments.size()];
+
+ for (int i = 0; i < article.attachments.size(); i++) {
+ items[i] = article.attachments.get(i).title != null ? article.attachments.get(i).content_url :
+ article.attachments.get(i).content_url;
+
+ itemUrls[i] = article.attachments.get(i).content_url;
+ }
+
+ Dialog dialog = new Dialog(OnlineActivity.this);
+ AlertDialog.Builder builder = new AlertDialog.Builder(OnlineActivity.this)
+ .setTitle(R.string.attachments_prompt)
+ .setCancelable(true)
+ .setSingleChoiceItems(items, 0, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ //
+ }
+ }).setNeutralButton(R.string.attachment_copy, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int selectedPosition = ((AlertDialog)dialog).getListView().getCheckedItemPosition();
+
+ copyToClipboard((String)itemUrls[selectedPosition]);
+ }
+ }).setPositiveButton(R.string.attachment_view, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ int selectedPosition = ((AlertDialog)dialog).getListView().getCheckedItemPosition();
+
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse((String)itemUrls[selectedPosition]));
+ startActivity(browserIntent);
+
+ dialog.cancel();
+ }
+ }).setNegativeButton(R.string.dialog_cancel, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+ }
+ }
+ return true;
+ case R.id.donate:
+ if (true) {
+ openUnlockUrl();
+ }
+ return true;
+ case R.id.logout:
+ logout();
+ return true;
+ case R.id.login:
+ login();
+ return true;
+ case R.id.go_offline:
+ switchOffline();
+ return true;
+ case R.id.article_set_note:
+ if (ap != null && ap.getSelectedArticle() != null) {
+ editArticleNote(ap.getSelectedArticle());
+ }
+ return true;
+ case R.id.preferences:
+ Intent intent = new Intent(OnlineActivity.this,
+ PreferencesActivity.class);
+ startActivityForResult(intent, 0);
+ return true;
+ case R.id.search:
+ if (hf != null && isCompatMode()) {
+ Dialog dialog = new Dialog(this);
+
+ final EditText edit = new EditText(this);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.search)
+ .setPositiveButton(getString(R.string.search),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ String query = edit.getText().toString().trim();
+
+ hf.setSearchQuery(query);
+
+ }
+ })
+ .setNegativeButton(getString(R.string.cancel),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ //
+
+ }
+ }).setView(edit);
+
+ dialog = builder.create();
+ dialog.show();
+ }
+ return true;
+ case R.id.headlines_mark_as_read:
+ if (hf != null) {
+
+ int count = hf.getUnreadArticles().size();
+
+ boolean confirm = m_prefs.getBoolean("confirm_headlines_catchup", true);
+
+ if (count > 0) {
+ if (confirm) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ OnlineActivity.this)
+ .setMessage(getString(R.string.mark_num_headlines_as_read, count))
+ .setPositiveButton(R.string.catchup,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ catchupVisibleArticles();
+
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ }
+ });
+
+ AlertDialog dlg = builder.create();
+ dlg.show();
+ } else {
+ catchupVisibleArticles();
+ }
+ }
+ }
+ return true;
+ case R.id.headlines_view_mode:
+ if (hf != null) {
+ Dialog dialog = new Dialog(this);
+
+ String viewMode = getViewMode();
+
+ //Log.d(TAG, "viewMode:" + getViewMode());
+
+ int selectedIndex = 0;
+
+ if (viewMode.equals("all_articles")) {
+ selectedIndex = 1;
+ } else if (viewMode.equals("marked")) {
+ selectedIndex = 2;
+ } else if (viewMode.equals("published")) {
+ selectedIndex = 3;
+ } else if (viewMode.equals("unread")) {
+ selectedIndex = 4;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.headlines_set_view_mode)
+ .setSingleChoiceItems(
+ new String[] {
+ getString(R.string.headlines_adaptive),
+ getString(R.string.headlines_all_articles),
+ getString(R.string.headlines_starred),
+ getString(R.string.headlines_published),
+ getString(R.string.headlines_unread) },
+ selectedIndex, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ switch (which) {
+ case 0:
+ setViewMode("adaptive");
+ break;
+ case 1:
+ setViewMode("all_articles");
+ break;
+ case 2:
+ setViewMode("marked");
+ break;
+ case 3:
+ setViewMode("published");
+ break;
+ case 4:
+ setViewMode("unread");
+ break;
+ }
+ dialog.cancel();
+
+ refresh();
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+
+ }
+ return true;
+ case R.id.headlines_select:
+ if (hf != null) {
+ Dialog dialog = new Dialog(this);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.headlines_select_dialog)
+ .setSingleChoiceItems(
+ new String[] {
+ getString(R.string.headlines_select_all),
+ getString(R.string.headlines_select_unread),
+ getString(R.string.headlines_select_none) },
+ 0, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ switch (which) {
+ case 0:
+ hf.setSelection(HeadlinesFragment.ArticlesSelection.ALL);
+ break;
+ case 1:
+ hf.setSelection(HeadlinesFragment.ArticlesSelection.UNREAD);
+ break;
+ case 2:
+ hf.setSelection(HeadlinesFragment.ArticlesSelection.NONE);
+ break;
+ }
+ dialog.cancel();
+ initMenu();
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+ }
+ return true;
+ case R.id.share_article:
+ if (ap != null) {
+ shareArticle(ap.getSelectedArticle());
+ }
+ return true;
+ case R.id.toggle_marked:
+ if (ap != null & ap.getSelectedArticle() != null) {
+ Article a = ap.getSelectedArticle();
+ a.marked = !a.marked;
+ saveArticleMarked(a);
+ if (hf != null) hf.notifyUpdated();
+ }
+ return true;
+ /* case R.id.selection_select_none:
+ if (hf != null) {
+ ArticleList selected = hf.getSelectedArticles();
+ if (selected.size() > 0) {
+ selected.clear();
+ initMenu();
+ hf.notifyUpdated();
+ }
+ }
+ return true; */
+ case R.id.selection_toggle_unread:
+ if (hf != null) {
+ ArticleList selected = hf.getSelectedArticles();
+
+ if (selected.size() > 0) {
+ for (Article a : selected)
+ a.unread = !a.unread;
+
+ toggleArticlesUnread(selected);
+ hf.notifyUpdated();
+ initMenu();
+ }
+ }
+ return true;
+ case R.id.selection_toggle_marked:
+ if (hf != null) {
+ ArticleList selected = hf.getSelectedArticles();
+
+ if (selected.size() > 0) {
+ for (Article a : selected)
+ a.marked = !a.marked;
+
+ toggleArticlesMarked(selected);
+ hf.notifyUpdated();
+ initMenu();
+ }
+ }
+ return true;
+ case R.id.selection_toggle_published:
+ if (hf != null) {
+ ArticleList selected = hf.getSelectedArticles();
+
+ if (selected.size() > 0) {
+ for (Article a : selected)
+ a.published = !a.published;
+
+ toggleArticlesPublished(selected);
+ hf.notifyUpdated();
+ initMenu();
+ }
+ }
+ return true;
+ case R.id.toggle_published:
+ if (ap != null && ap.getSelectedArticle() != null) {
+ Article a = ap.getSelectedArticle();
+ a.published = !a.published;
+ saveArticlePublished(a);
+ if (hf != null) hf.notifyUpdated();
+ }
+ return true;
+ case R.id.catchup_above:
+ if (hf != null) {
+ if (ap != null && ap.getSelectedArticle() != null) {
+ Article article = ap.getSelectedArticle();
+
+ ArticleList articles = hf.getAllArticles();
+ ArticleList tmp = new ArticleList();
+ for (Article a : articles) {
+ if (article.id == a.id)
+ break;
+
+ if (a.unread) {
+ a.unread = false;
+ tmp.add(a);
+ }
+ }
+ if (tmp.size() > 0) {
+ toggleArticlesUnread(tmp);
+ hf.notifyUpdated();
+ initMenu();
+ }
+ }
+ }
+ return true;
+ case R.id.set_unread:
+ if (ap != null && ap.getSelectedArticle() != null) {
+ Article a = ap.getSelectedArticle();
+
+ if (a != null) {
+ a.unread = !a.unread;
+ saveArticleUnread(a);
+ }
+
+ if (hf != null) hf.notifyUpdated();
+ }
+ return true;
+ case R.id.set_labels:
+ if (ap != null && ap.getSelectedArticle() != null) {
+ if (getApiLevel() != 7) {
+ editArticleLabels(ap.getSelectedArticle());
+ } else {
+ toast(R.string.server_function_not_available);
+ }
+
+ }
+ return true;
+ case R.id.update_headlines:
+ if (hf != null) {
+ //m_pullToRefreshAttacher.setRefreshing(true);
+ hf.refresh(false, true);
+ }
+ return true;
+ default:
+ Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ protected void catchupVisibleArticles() {
+ final HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (hf != null) {
+ ArticleList articles = hf.getUnreadArticles();
+
+ for (Article a : articles)
+ a.unread = false;
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ if (hf.isAdded()) {
+ hf.refresh(false);
+ }
+ }
+ };
+
+ final String articleIds = articlesToIdString(articles);
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", articleIds);
+ put("mode", "0");
+ put("field", "2");
+ }
+ };
+ req.execute(map);
+ }
+ }
+
+ public void editArticleNote(final Article article) {
+ String note = "";
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(article.title);
+ final EditText topicEdit = new EditText(this);
+ topicEdit.setText(note);
+ builder.setView(topicEdit);
+
+ builder.setPositiveButton(R.string.article_set_note, new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String note = topicEdit.getText().toString().trim();
+
+ saveArticleNote(article, note);
+ article.published = true;
+ article.note = note;
+
+ saveArticlePublished(article);
+
+ HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+ if (hf != null) hf.notifyUpdated();
+ }
+ });
+
+ builder.setNegativeButton(R.string.dialog_cancel, new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ //
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ public void editArticleLabels(Article article) {
+ final int articleId = article.id;
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ Type listType = new TypeToken<List<Label>>() {}.getType();
+ final List<Label> labels = new Gson().fromJson(result, listType);
+
+ CharSequence[] items = new CharSequence[labels.size()];
+ final int[] itemIds = new int[labels.size()];
+ boolean[] checkedItems = new boolean[labels.size()];
+
+ for (int i = 0; i < labels.size(); i++) {
+ items[i] = labels.get(i).caption;
+ itemIds[i] = labels.get(i).id;
+ checkedItems[i] = labels.get(i).checked;
+ }
+
+ Dialog dialog = new Dialog(OnlineActivity.this);
+ AlertDialog.Builder builder = new AlertDialog.Builder(OnlineActivity.this)
+ .setTitle(R.string.article_set_labels)
+ .setMultiChoiceItems(items, checkedItems, new OnMultiChoiceClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which, final boolean isChecked) {
+ final int labelId = itemIds[which];
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "setArticleLabel");
+ put("label_id", String.valueOf(labelId));
+ put("article_ids", String.valueOf(articleId));
+ if (isChecked) put("assign", "true");
+ }
+ };
+
+ ApiRequest req = new ApiRequest(m_context);
+ req.execute(map);
+
+ }
+ }).setPositiveButton(R.string.dialog_close, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+
+ }
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "getLabels");
+ put("article_id", String.valueOf(articleId));
+ }
+ };
+
+ req.execute(map);
+ }
+
+ protected void logout() {
+ setSessionId(null);
+
+ findViewById(R.id.loading_container).setVisibility(View.VISIBLE);
+ setLoadingStatus(R.string.login_ready, false);
+
+ initMenu();
+ }
+
+ protected void loginFailure() {
+ setSessionId(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();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("offlineModeStatus", m_offlineModeStatus);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ ApiRequest.trustAllHosts(m_prefs.getBoolean("ssl_trust_any", false),
+ m_prefs.getBoolean("ssl_trust_any_host", false));
+
+ 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 (getSessionId() == null) {
+ login();
+ } else {
+ loginSuccess(false);
+ }
+ }
+
+ public Menu getMenu() {
+ return m_menu;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main_menu, menu);
+
+ m_menu = menu;
+
+ initMenu();
+
+ List<PackageInfo> pkgs = getPackageManager()
+ .getInstalledPackages(0);
+
+ for (PackageInfo p : pkgs) {
+ if ("org.fox.ttrss.key".equals(p.packageName)) {
+ Log.d(TAG, "license apk found");
+ menu.findItem(R.id.donate).setVisible(false);
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ protected int getApiLevel() {
+ return GlobalState.getInstance().m_apiLevel;
+ }
+
+ protected void setApiLevel(int apiLevel) {
+ GlobalState.getInstance().m_apiLevel = apiLevel;
+ }
+
+ @SuppressWarnings({ "unchecked", "serial" })
+ public void saveArticleUnread(final Article article) {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ //toast(R.string.article_set_unread);
+ initMenu();
+ }
+ };
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", String.valueOf(article.id));
+ put("mode", article.unread ? "1" : "0");
+ put("field", "2");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ public void saveArticleMarked(final Article article) {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ //toast(article.marked ? R.string.notify_article_marked : R.string.notify_article_unmarked);
+ initMenu();
+ }
+ };
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", String.valueOf(article.id));
+ put("mode", article.marked ? "1" : "0");
+ put("field", "0");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ @SuppressWarnings({ "unchecked", "serial" })
+ public void saveArticlePublished(final Article article) {
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ //toast(article.published ? R.string.notify_article_published : R.string.notify_article_unpublished);
+ initMenu();
+ }
+ };
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", String.valueOf(article.id));
+ put("mode", article.published ? "1" : "0");
+ put("field", "1");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ @SuppressWarnings({ "unchecked", "serial" })
+ public void saveArticleNote(final Article article, final String note) {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ toast(R.string.notify_article_note_set);
+ }
+ };
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", String.valueOf(article.id));
+ put("mode", "1");
+ put("data", note);
+ put("field", "3");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ public static String articlesToIdString(ArticleList articles) {
+ String tmp = "";
+
+ for (Article a : articles)
+ tmp += String.valueOf(a.id) + ",";
+
+ return tmp.replaceAll(",$", "");
+ }
+
+ public void shareText(String text) {
+
+ Intent intent = new Intent(Intent.ACTION_SEND);
+
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, text);
+
+ startActivity(Intent.createChooser(intent, text));
+ }
+
+ public void shareArticle(Article article) {
+ if (article != null) {
+
+ Intent intent = getShareIntent(article);
+
+ startActivity(Intent.createChooser(intent,
+ getString(R.string.share_article)));
+ }
+ }
+
+ protected Intent getShareIntent(Article article) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_SUBJECT, article.title);
+ intent.putExtra(Intent.EXTRA_TEXT, article.link);
+
+ return intent;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (m_prefs.getBoolean("use_volume_keys", false)) {
+ ArticlePager ap = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ if (ap != null && ap.isAdded()) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ ap.selectArticle(false);
+ return true;
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ ap.selectArticle(true);
+ return true;
+ }
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ // Handle onKeyUp too to suppress beep
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (m_prefs.getBoolean("use_volume_keys", false)) {
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ return true;
+ }
+ }
+
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public void catchupFeed(final Feed feed) {
+ Log.d(TAG, "catchupFeed=" + feed);
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ // refresh?
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "catchupFeed");
+ put("feed_id", String.valueOf(feed.id));
+ if (feed.is_cat)
+ put("is_cat", "1");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ public void toggleArticlesMarked(final ArticleList articles) {
+ ApiRequest req = new ApiRequest(getApplicationContext());
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", articlesToIdString(articles));
+ put("mode", "2");
+ put("field", "0");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ public void toggleArticlesUnread(final ArticleList articles) {
+ ApiRequest req = new ApiRequest(getApplicationContext());
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", articlesToIdString(articles));
+ put("mode", "2");
+ put("field", "2");
+ }
+ };
+
+ req.execute(map);
+ //refresh();
+ }
+
+ public void toggleArticlesPublished(final ArticleList articles) {
+ ApiRequest req = new ApiRequest(getApplicationContext());
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "updateArticle");
+ put("article_ids", articlesToIdString(articles));
+ put("mode", "2");
+ put("field", "1");
+ }
+ };
+
+ req.execute(map);
+ }
+
+
+ protected void initMenu() {
+ if (m_menu != null) {
+ if (getSessionId() != null) {
+ m_menu.setGroupVisible(R.id.menu_group_logged_in, true);
+ m_menu.setGroupVisible(R.id.menu_group_logged_out, false);
+ } else {
+ m_menu.setGroupVisible(R.id.menu_group_logged_in, false);
+ m_menu.setGroupVisible(R.id.menu_group_logged_out, true);
+ }
+
+ m_menu.setGroupVisible(R.id.menu_group_headlines, false);
+ m_menu.setGroupVisible(R.id.menu_group_article, false);
+ m_menu.setGroupVisible(R.id.menu_group_feeds, false);
+
+ m_menu.findItem(R.id.set_labels).setEnabled(getApiLevel() >= 1);
+ m_menu.findItem(R.id.article_set_note).setEnabled(getApiLevel() >= 1);
+ m_menu.findItem(R.id.subscribe_to_feed).setEnabled(getApiLevel() >= 5);
+
+ MenuItem search = m_menu.findItem(R.id.search);
+ search.setEnabled(getApiLevel() >= 2);
+
+ ArticlePager ap = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ if (ap != null) {
+ Article article = ap.getSelectedArticle();
+
+ if (article != null) {
+ m_menu.findItem(R.id.toggle_marked).setIcon(article.marked ? R.drawable.ic_important_light :
+ R.drawable.ic_unimportant_light);
+
+ m_menu.findItem(R.id.toggle_published).setIcon(article.published ? R.drawable.ic_menu_published_light :
+ R.drawable.ic_menu_unpublished_light);
+
+ m_menu.findItem(R.id.set_unread).setIcon(article.unread ? R.drawable.ic_unread_light :
+ R.drawable.ic_read_light);
+ }
+ }
+
+ HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (hf != null) {
+ if (hf.getSelectedArticles().size() > 0 && m_headlinesActionMode == null) {
+ m_headlinesActionMode = startSupportActionMode(m_headlinesActionModeCallback);
+ } else if (hf.getSelectedArticles().size() == 0 && m_headlinesActionMode != null) {
+ m_headlinesActionMode.finish();
+ }
+ }
+
+ if (!isCompatMode()) {
+ SearchView searchView = (SearchView) search.getActionView();
+
+ if (searchView != null) {
+ searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ private String query = "";
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ HeadlinesFragment frag = (HeadlinesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_HEADLINES);
+
+ if (frag != null) {
+ frag.setSearchQuery(query);
+ this.query = query;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (newText.equals("") && !newText.equals(this.query)) {
+ HeadlinesFragment frag = (HeadlinesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_HEADLINES);
+
+ if (frag != null) {
+ frag.setSearchQuery(newText);
+ this.query = newText;
+ }
+ }
+
+ return false;
+ }
+ });
+ }
+ }
+ }
+ }
+
+ protected void refresh(boolean includeHeadlines) {
+ FeedCategoriesFragment cf = (FeedCategoriesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_CATS);
+
+ if (cf != null) {
+ cf.refresh(false);
+ }
+
+ FeedsFragment ff = (FeedsFragment) getSupportFragmentManager().findFragmentByTag(FRAG_FEEDS);
+
+ if (ff != null) {
+ ff.refresh(false);
+ }
+
+ if (includeHeadlines) {
+ HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (hf != null) {
+ hf.refresh(false);
+ }
+
+ ArticlePager af = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ if (af != null) {
+ af.refresh(false);
+ }
+ }
+ }
+
+ protected void refresh() {
+ refresh(true);
+ }
+
+ protected class LoginRequest extends ApiRequest {
+ boolean m_refreshAfterLogin = false;
+ OnLoginFinishedListener m_listener;
+
+ public LoginRequest(Context context, boolean refresh, OnLoginFinishedListener listener) {
+ super(context);
+ m_refreshAfterLogin = refresh;
+ m_listener = listener;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ try {
+ JsonObject content = result.getAsJsonObject();
+
+ if (content != null) {
+ setSessionId(content.get("session_id").getAsString());
+
+ JsonElement apiLevel = content.get("api_level");
+
+ GlobalState.getInstance().m_canUseProgress = m_canUseProgress;
+
+ Log.d(TAG, "Authenticated! canUseProgress=" + m_canUseProgress);
+
+ if (apiLevel != null) {
+ setApiLevel(apiLevel.getAsInt());
+ Log.d(TAG, "Received API level: " + getApiLevel());
+
+ if (m_listener != null) {
+ m_listener.OnLoginSuccess();
+ } else {
+ loginSuccess(m_refreshAfterLogin);
+ }
+
+ } else {
+
+ ApiRequest req = new ApiRequest(m_context) {
+ protected void onPostExecute(JsonElement result) {
+ setApiLevel(0);
+
+ if (result != null) {
+ try {
+ setApiLevel(result.getAsJsonObject().get("level").getAsInt());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else if (m_lastError != ApiError.API_UNKNOWN_METHOD) {
+ // Unknown method means old tt-rss, in that case we assume API 0 and continue
+
+ setLoadingStatus(getErrorMessage(), false);
+
+ if (m_listener != null) {
+ m_listener.OnLoginFailed();
+ } else {
+ loginFailure();
+ }
+
+ return;
+ }
+
+ Log.d(TAG, "Received API level: " + getApiLevel());
+
+ loginSuccess(m_refreshAfterLogin);
+
+ return;
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", getSessionId());
+ put("op", "getApiLevel");
+ }
+ };
+
+ req.execute(map);
+
+ setLoadingStatus(R.string.loading_message, true);
+ }
+
+ return;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ setSessionId(null);
+ setLoadingStatus(getErrorMessage(), false);
+
+ loginFailure();
+ }
+
+ }
+
+ public void setViewMode(String viewMode) {
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putString("view_mode", viewMode);
+ editor.commit();
+ }
+
+ public String getViewMode() {
+ return m_prefs.getString("view_mode", "adaptive");
+ }
+
+ public void setLastContentImageHitTestUrl(String url) {
+ m_lastImageHitTestUrl = url;
+ }
+
+ public String getLastContentImageHitTestUrl() {
+ return m_lastImageHitTestUrl;
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/PreferencesActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/PreferencesActivity.java
new file mode 100644
index 00000000..f42154a7
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/PreferencesActivity.java
@@ -0,0 +1,25 @@
+package org.fox.ttrss;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+public class PreferencesActivity extends PreferenceActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.preferences);
+
+ boolean compatMode = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB;
+
+ if (compatMode) {
+ findPreference("dim_status_bar").setEnabled(false);
+ findPreference("webview_hardware_accel").setEnabled(false);
+ }
+
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ findPreference("enable_condensed_fonts").setEnabled(false);
+ }
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineActivity.java
new file mode 100644
index 00000000..fd05b596
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineActivity.java
@@ -0,0 +1,881 @@
+package org.fox.ttrss.offline;
+
+import org.fox.ttrss.CommonActivity;
+import org.fox.ttrss.PreferencesActivity;
+import org.fox.ttrss.R;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.select.Elements;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v7.view.ActionMode;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.EditText;
+import android.widget.SearchView;
+import android.widget.TextView;
+
+public class OfflineActivity extends CommonActivity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ protected SharedPreferences m_prefs;
+ protected Menu m_menu;
+
+ private ActionMode m_headlinesActionMode;
+ private HeadlinesActionModeCallback m_headlinesActionModeCallback;
+
+ private String m_lastImageHitTestUrl;
+
+ @SuppressLint("NewApi")
+ private class HeadlinesActionModeCallback implements ActionMode.Callback {
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ m_headlinesActionMode = null;
+ deselectAllArticles();
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.headlines_action_menu, menu);
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ onOptionsItemSelected(item);
+ return false;
+ }
+ };
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ /* AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo(); */
+
+ final OfflineArticlePager ap = (OfflineArticlePager)getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ switch (item.getItemId()) {
+ case R.id.article_img_open:
+ if (getLastContentImageHitTestUrl() != null) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(getLastContentImageHitTestUrl()));
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ toast(R.string.error_other_error);
+ }
+ }
+ return true;
+ case R.id.article_img_copy:
+ if (getLastContentImageHitTestUrl() != null) {
+ copyToClipboard(getLastContentImageHitTestUrl());
+ }
+ return true;
+ case R.id.article_img_share:
+ if (getLastContentImageHitTestUrl() != null) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+
+ intent.setType("image/png");
+ intent.putExtra(Intent.EXTRA_SUBJECT, getLastContentImageHitTestUrl());
+ intent.putExtra(Intent.EXTRA_TEXT, getLastContentImageHitTestUrl());
+
+ startActivity(Intent.createChooser(intent, getLastContentImageHitTestUrl()));
+ }
+ return true;
+ case R.id.article_img_view_caption:
+ if (getLastContentImageHitTestUrl() != null) {
+
+ String content = "";
+
+ Cursor article = getArticleById(ap.getSelectedArticleId());
+
+ if (article != null) {
+ content = article.getString(article.getColumnIndex("content"));
+ article.close();
+ }
+
+ // Android doesn't give us an easy way to access title tags;
+ // we'll use Jsoup on the body text to grab the title text
+ // from the first image tag with this url. This will show
+ // the wrong text if an image is used multiple times.
+ Document doc = Jsoup.parse(content);
+ Elements es = doc.getElementsByAttributeValue("src", getLastContentImageHitTestUrl());
+ if (es.size() > 0){
+ if (es.get(0).hasAttr("title")){
+ Dialog dia = new Dialog(this);
+ if (es.get(0).hasAttr("alt")){
+ dia.setTitle(es.get(0).attr("alt"));
+ } else {
+ dia.setTitle(es.get(0).attr("title"));
+ }
+ TextView titleText = new TextView(this);
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ titleText.setPaddingRelative(24, 24, 24, 24);
+ } else {
+ titleText.setPadding(24, 24, 24, 24);
+ }
+
+ titleText.setTextSize(16);
+ titleText.setText(es.get(0).attr("title"));
+ dia.setContentView(titleText);
+ dia.show();
+ } else {
+ toast(R.string.no_caption_to_display);
+ }
+ } else {
+ toast(R.string.no_caption_to_display);
+ }
+ }
+ return true;
+ default:
+ Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
+ return super.onContextItemSelected(item);
+ }
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ setAppTheme(m_prefs);
+
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_PROGRESS);
+
+ setProgressBarVisibility(false);
+
+ setContentView(R.layout.login);
+
+ 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_headlinesActionModeCallback = new HeadlinesActionModeCallback();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+ }
+
+ protected void selectArticles(int feedId, boolean isCat, int mode) {
+ switch (mode) {
+ case 0:
+ SQLiteStatement stmtSelectAll = null;
+
+ if (isCat) {
+ stmtSelectAll = getWritableDb().compileStatement(
+ "UPDATE articles SET selected = 1 WHERE feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)");
+ } else {
+ stmtSelectAll = getWritableDb().compileStatement(
+ "UPDATE articles SET selected = 1 WHERE feed_id = ?");
+ }
+
+ stmtSelectAll.bindLong(1, feedId);
+ stmtSelectAll.execute();
+ stmtSelectAll.close();
+
+ break;
+ case 1:
+
+ SQLiteStatement stmtSelectUnread = null;
+
+ if (isCat) {
+ stmtSelectUnread = getWritableDb().compileStatement(
+ "UPDATE articles SET selected = 1 WHERE feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?) AND unread = 1");
+ } else {
+ stmtSelectUnread = getWritableDb().compileStatement(
+ "UPDATE articles SET selected = 1 WHERE feed_id = ? AND unread = 1");
+ }
+
+ stmtSelectUnread.bindLong(1, feedId);
+ stmtSelectUnread.execute();
+ stmtSelectUnread.close();
+
+ break;
+ case 2:
+ deselectAllArticles();
+ break;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_HEADLINES);
+
+ /* final OfflineFeedsFragment off = (OfflineFeedsFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_FEEDS); */
+
+ /* final OfflineFeedCategoriesFragment ocf = (OfflineFeedCategoriesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_CATS); */
+
+ final OfflineArticlePager oap = (OfflineArticlePager) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_ARTICLE);
+
+ switch (item.getItemId()) {
+ /* case android.R.id.home:
+ finish();
+ return true; */
+ case R.id.headlines_toggle_sidebar:
+ if (true && !isSmallScreen()) {
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putBoolean("headlines_hide_sidebar", !m_prefs.getBoolean("headlines_hide_sidebar", false));
+ editor.commit();
+
+ if (ohf != null && ohf.isAdded()) {
+ ohf.getView().setVisibility(m_prefs.getBoolean("headlines_hide_sidebar", false) ? View.GONE : View.VISIBLE);
+ }
+ }
+ return true;
+ case R.id.go_online:
+ switchOnline();
+ return true;
+ case R.id.search:
+ if (ohf != null && isCompatMode()) {
+ Dialog dialog = new Dialog(this);
+
+ final EditText edit = new EditText(this);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.search)
+ .setPositiveButton(getString(R.string.search),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ String query = edit.getText().toString().trim();
+
+ ohf.setSearchQuery(query);
+
+ }
+ })
+ .setNegativeButton(getString(R.string.cancel),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ //
+
+ }
+ }).setView(edit);
+
+ dialog = builder.create();
+ dialog.show();
+ }
+
+ return true;
+ case R.id.preferences:
+ Intent intent = new Intent(this, PreferencesActivity.class);
+ startActivityForResult(intent, 0);
+ return true;
+ case R.id.headlines_view_mode:
+ if (ohf != null) {
+ Dialog dialog = new Dialog(this);
+
+ String viewMode = getViewMode();
+
+ //Log.d(TAG, "viewMode:" + getViewMode());
+
+ int selectedIndex = 0;
+
+ if (viewMode.equals("all_articles")) {
+ selectedIndex = 0;
+ } else if (viewMode.equals("marked")) {
+ selectedIndex = 1;
+ } else if (viewMode.equals("published")) {
+ selectedIndex = 2;
+ } else if (viewMode.equals("unread")) {
+ selectedIndex = 3;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.headlines_set_view_mode)
+ .setSingleChoiceItems(
+ new String[] {
+ /* getString(R.string.headlines_adaptive), */
+ getString(R.string.headlines_all_articles),
+ getString(R.string.headlines_starred),
+ getString(R.string.headlines_published),
+ getString(R.string.headlines_unread) },
+ selectedIndex, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ switch (which) {
+ /* case 0:
+ setViewMode("adaptive");
+ break; */
+ case 0:
+ setViewMode("all_articles");
+ break;
+ case 1:
+ setViewMode("marked");
+ break;
+ case 2:
+ setViewMode("published");
+ break;
+ case 3:
+ setViewMode("unread");
+ break;
+ }
+ dialog.cancel();
+
+ refresh();
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+
+ }
+ return true;
+ case R.id.headlines_select:
+ if (ohf != null) {
+ Dialog dialog = new Dialog(this);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.headlines_select_dialog);
+
+ builder.setSingleChoiceItems(new String[] {
+ getString(R.string.headlines_select_all),
+ getString(R.string.headlines_select_unread),
+ getString(R.string.headlines_select_none) }, 0,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ selectArticles(ohf.getFeedId(), ohf.getFeedIsCat(), which);
+ initMenu();
+ refresh();
+
+ dialog.cancel();
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+ }
+ return true;
+ case R.id.headlines_mark_as_read:
+ if (ohf != null) {
+ final int feedId = ohf.getFeedId();
+ final boolean isCat = ohf.getFeedIsCat();
+
+ int count = getUnreadArticleCount(feedId, isCat);
+ boolean confirm = m_prefs.getBoolean("confirm_headlines_catchup", true);
+
+ if (count > 0) {
+ if (confirm) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ OfflineActivity.this)
+ .setMessage(getString(R.string.mark_num_headlines_as_read, count))
+ .setPositiveButton(R.string.catchup,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ catchupFeed(feedId, isCat);
+
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new Dialog.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ }
+ });
+
+ AlertDialog dlg = builder.create();
+ dlg.show();
+
+
+ } else {
+ catchupFeed(feedId, isCat);
+ }
+ }
+ }
+ return true;
+ case R.id.share_article:
+ if (true) {
+ int articleId = oap.getSelectedArticleId();
+
+ shareArticle(articleId);
+ }
+ return true;
+ case R.id.toggle_marked:
+ if (oap != null) {
+ int articleId = oap.getSelectedArticleId();
+
+ SQLiteStatement stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, marked = NOT marked WHERE "
+ + BaseColumns._ID + " = ?");
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+
+ refresh();
+ }
+ return true;
+ /* case R.id.selection_select_none:
+ deselectAllArticles();
+ return true; */
+ case R.id.selection_toggle_unread:
+ if (getSelectedArticleCount() > 0) {
+ SQLiteStatement stmt = getWritableDb()
+ .compileStatement(
+ "UPDATE articles SET modified = 1, unread = NOT unread WHERE selected = 1");
+ stmt.execute();
+ stmt.close();
+
+ refresh();
+ }
+ return true;
+ case R.id.selection_toggle_marked:
+ if (getSelectedArticleCount() > 0) {
+ SQLiteStatement stmt = getWritableDb()
+ .compileStatement(
+ "UPDATE articles SET modified = 1, marked = NOT marked WHERE selected = 1");
+ stmt.execute();
+ stmt.close();
+
+ refresh();
+ }
+ return true;
+ case R.id.selection_toggle_published:
+ if (getSelectedArticleCount() > 0) {
+ SQLiteStatement stmt = getWritableDb()
+ .compileStatement(
+ "UPDATE articles SET modified = 1, published = NOT published WHERE selected = 1");
+ stmt.execute();
+ stmt.close();
+
+ refresh();
+ }
+ return true;
+ case R.id.toggle_published:
+ if (oap != null) {
+ int articleId = oap.getSelectedArticleId();
+
+ SQLiteStatement stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, published = NOT published WHERE "
+ + BaseColumns._ID + " = ?");
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+
+ refresh();
+ }
+ return true;
+ case R.id.catchup_above:
+ if (oap != null) {
+ int articleId = oap.getSelectedArticleId();
+ int feedId = oap.getFeedId();
+ boolean isCat = oap.getFeedIsCat();
+
+ SQLiteStatement stmt = null;
+
+ if (isCat) {
+ stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 WHERE " +
+ "updated >= (SELECT updated FROM articles WHERE " + BaseColumns._ID + " = ?) " +
+ "AND feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)");
+ } else {
+ stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 WHERE " +
+ "updated >= (SELECT updated FROM articles WHERE " + BaseColumns._ID + " = ?) " +
+ "AND feed_id = ?");
+ }
+
+ stmt.bindLong(1, articleId);
+ stmt.bindLong(2, feedId);
+ stmt.execute();
+ stmt.close();
+
+ refresh();
+ }
+ return true;
+ case R.id.set_unread:
+ if (oap != null) {
+ int articleId = oap.getSelectedArticleId();
+
+ SQLiteStatement stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 1 WHERE "
+ + BaseColumns._ID + " = ?");
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+
+ refresh();
+ }
+ 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;
+ }
+
+ @SuppressLint("NewApi")
+ protected void initMenu() {
+ if (m_menu != null) {
+ m_menu.setGroupVisible(R.id.menu_group_headlines, false);
+ m_menu.setGroupVisible(R.id.menu_group_article, false);
+ m_menu.setGroupVisible(R.id.menu_group_feeds, false);
+
+ OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (hf != null) {
+ if (hf.getSelectedArticleCount() > 0 && m_headlinesActionMode == null) {
+ m_headlinesActionMode = startSupportActionMode(m_headlinesActionModeCallback);
+ } else if (hf.getSelectedArticleCount() == 0 && m_headlinesActionMode != null) {
+ m_headlinesActionMode.finish();
+ }
+ }
+
+ OfflineArticlePager ap = (OfflineArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ if (ap != null) {
+ int articleId = ap.getSelectedArticleId();
+
+ Cursor article = getArticleById(articleId);
+
+ if (article != null) {
+ boolean unread = article.getInt(article.getColumnIndex("unread")) == 1;
+ boolean marked = article.getInt(article.getColumnIndex("marked")) == 1;
+ boolean published = article.getInt(article.getColumnIndex("published")) == 1;
+
+ m_menu.findItem(R.id.toggle_marked).setIcon(marked ? R.drawable.ic_important_light :
+ R.drawable.ic_unimportant_light);
+
+ m_menu.findItem(R.id.toggle_published).setIcon(published ? R.drawable.ic_menu_published_light :
+ R.drawable.ic_menu_unpublished_light);
+
+ m_menu.findItem(R.id.set_unread).setIcon(unread ? R.drawable.ic_unread_light :
+ R.drawable.ic_read_light);
+
+ article.close();
+ }
+ }
+
+ if (!isCompatMode()) {
+ MenuItem search = m_menu.findItem(R.id.search);
+
+ SearchView searchView = (SearchView) search.getActionView();
+
+ if (searchView != null) {
+ searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ private String query = "";
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ OfflineHeadlinesFragment frag = (OfflineHeadlinesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_HEADLINES);
+
+ if (frag != null) {
+ frag.setSearchQuery(query);
+ this.query = query;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (newText.equals("") && !newText.equals(this.query)) {
+ OfflineHeadlinesFragment frag = (OfflineHeadlinesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_HEADLINES);
+
+ if (frag != null) {
+ frag.setSearchQuery(newText);
+ this.query = newText;
+ }
+ }
+
+ return 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;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (m_prefs.getBoolean("use_volume_keys", false)) {
+ OfflineArticlePager ap = (OfflineArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ if (ap != null && ap.isAdded()) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ ap.selectArticle(false);
+ return true;
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ ap.selectArticle(true);
+ return true;
+ }
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ // Handle onKeyUp too to suppress beep
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (m_prefs.getBoolean("use_volume_keys", false)) {
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ return true;
+ }
+ }
+
+ return super.onKeyUp(keyCode, event);
+ }
+
+ 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) {
+ if (article != null) {
+ 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;
+ } else {
+ return null;
+ }
+ }
+
+ 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.string.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;
+ }
+
+ protected int getUnreadArticleCount(int feedId, boolean isCat) {
+
+ Cursor c;
+
+ if (isCat) {
+ c = getReadableDb().query("articles",
+ new String[] { "COUNT(*)" }, "unread = 1 AND feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)",
+ new String[] { String.valueOf(feedId) },
+ null, null, null);
+ } else {
+ c = getReadableDb().query("articles",
+ new String[] { "COUNT(*)" }, "unread = 1 AND feed_id = ?",
+ new String[] { String.valueOf(feedId) },
+ null, null, null);
+ }
+
+ c.moveToFirst();
+ int selected = c.getInt(0);
+ c.close();
+
+ return selected;
+ }
+
+ protected void deselectAllArticles() {
+ getWritableDb().execSQL("UPDATE articles SET selected = 0 ");
+ refresh();
+ }
+
+ protected void refresh() {
+ OfflineFeedsFragment ff = (OfflineFeedsFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_FEEDS);
+
+ if (ff != null) {
+ ff.refresh();
+ }
+
+ OfflineFeedCategoriesFragment cf = (OfflineFeedCategoriesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_CATS);
+
+ if (cf != null) {
+ cf.refresh();
+ }
+
+ OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getSupportFragmentManager()
+ .findFragmentByTag(FRAG_HEADLINES);
+
+ if (ohf != null) {
+ ohf.refresh();
+ }
+
+ initMenu();
+ }
+
+ public void catchupFeed(int feedId, boolean isCat) {
+ if (isCat) {
+ SQLiteStatement stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 WHERE feed_id IN (SELECT "+
+ BaseColumns._ID+" FROM feeds WHERE cat_id = ?)");
+ stmt.bindLong(1, feedId);
+ stmt.execute();
+ stmt.close();
+ } else {
+ SQLiteStatement stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 WHERE feed_id = ?");
+ stmt.bindLong(1, feedId);
+ stmt.execute();
+ stmt.close();
+ }
+
+ refresh();
+ }
+
+ public void setLastContentImageHitTestUrl(String url) {
+ m_lastImageHitTestUrl = url;
+ }
+
+ public String getLastContentImageHitTestUrl() {
+ return m_lastImageHitTestUrl;
+ }
+
+ public void setViewMode(String viewMode) {
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putString("offline_view_mode", viewMode);
+ editor.commit();
+ }
+
+ public String getViewMode() {
+ return m_prefs.getString("offline_view_mode", "adaptive");
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticleFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticleFragment.java
new file mode 100644
index 00000000..985150a9
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticleFragment.java
@@ -0,0 +1,439 @@
+package org.fox.ttrss.offline;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.fox.ttrss.CommonActivity;
+import org.fox.ttrss.R;
+import org.fox.ttrss.util.ImageCacheService;
+import org.fox.ttrss.util.TypefaceCache;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebView.HitTestResult;
+import android.widget.TextView;
+
+public class OfflineArticleFragment extends Fragment {
+ 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 void initialize(int articleId) {
+ 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")));
+
+ String title = m_cursor.getString(m_cursor.getColumnIndex("title"));
+
+ if (v.getId() == R.id.content) {
+ HitTestResult result = ((WebView)v).getHitTestResult();
+
+ if (result != null && (result.getType() == HitTestResult.IMAGE_TYPE || result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE)) {
+ menu.setHeaderTitle(result.getExtra());
+ getActivity().getMenuInflater().inflate(R.menu.article_content_img_context_menu, menu);
+
+ /* FIXME I have no idea how to do this correctly ;( */
+
+ m_activity.setLastContentImageHitTestUrl(result.getExtra());
+
+ } else {
+ menu.setHeaderTitle(title);
+ getActivity().getMenuInflater().inflate(R.menu.article_link_context_menu, menu);
+ }
+ } else {
+ menu.setHeaderTitle(title);
+ getActivity().getMenuInflater().inflate(R.menu.article_link_context_menu, menu);
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ if (savedInstanceState != null) {
+ m_articleId = savedInstanceState.getInt("articleId");
+ }
+
+ boolean useTitleWebView = m_prefs.getBoolean("article_compat_view", false);
+
+ View view = inflater.inflate(useTitleWebView ? R.layout.article_fragment_compat : 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()) {
+ if (!useTitleWebView) {
+ View scroll = view.findViewById(R.id.article_scrollview);
+
+ if (scroll != null) {
+ final float scale = getResources().getDisplayMetrics().density;
+
+ if (m_activity.isSmallScreen()) {
+ scroll.setPadding((int)(8 * scale + 0.5f),
+ (int)(5 * scale + 0.5f),
+ (int)(8 * scale + 0.5f),
+ 0);
+ } else {
+ scroll.setPadding((int)(25 * scale + 0.5f),
+ (int)(10 * scale + 0.5f),
+ (int)(25 * scale + 0.5f),
+ 0);
+
+ }
+
+ }
+ }
+
+ int articleFontSize = Integer.parseInt(m_prefs.getString("article_font_size_sp", "16"));
+ int articleSmallFontSize = Math.max(10, Math.min(18, articleFontSize - 2));
+
+ TextView title = (TextView)view.findViewById(R.id.title);
+
+ final String link = m_cursor.getString(m_cursor.getColumnIndex("link"));
+
+ if (title != null) {
+
+ if (m_prefs.getBoolean("enable_condensed_fonts", false)) {
+ Typeface tf = TypefaceCache.get(m_activity, "sans-serif-condensed", Typeface.NORMAL);
+
+ if (tf != null && !tf.equals(title.getTypeface())) {
+ title.setTypeface(tf);
+ }
+
+ title.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, articleFontSize + 5));
+ } else {
+ title.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, articleFontSize + 3));
+ }
+
+ 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.setText(titleStr);
+ //title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
+ title.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ URL url = new URL(link.trim());
+ String uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(),
+ url.getPort(), url.getPath(), url.getQuery(), url.getRef()).toString();
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ m_activity.toast(R.string.error_other_error);
+ }
+ }
+ });
+
+ registerForContextMenu(title);
+ }
+
+ TextView comments = (TextView)view.findViewById(R.id.comments);
+
+ if (comments != null) {
+ comments.setVisibility(View.GONE);
+ }
+
+ TextView note = (TextView)view.findViewById(R.id.note);
+
+ if (note != null) {
+ note.setVisibility(View.GONE);
+ }
+
+ final WebView web = (WebView)view.findViewById(R.id.content);
+
+ if (web != null) {
+
+ web.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ HitTestResult result = ((WebView)v).getHitTestResult();
+
+ if (result != null && (result.getType() == HitTestResult.IMAGE_TYPE || result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE)) {
+ registerForContextMenu(web);
+ m_activity.openContextMenu(web);
+ unregisterForContextMenu(web);
+ return true;
+ } else {
+ if (m_activity.isCompatMode()) {
+ KeyEvent shiftPressEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0);
+ shiftPressEvent.dispatch(web);
+ }
+
+ return false;
+ }
+ }
+ });
+
+ web.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onProgressChanged(WebView view, int progress) {
+ m_activity.setProgress(Math.round(((float)progress / 100f) * 10000));
+ if (progress == 100) {
+ m_activity.setProgressBarVisibility(false);
+ }
+ }
+ });
+
+ String content;
+ String cssOverride = "";
+
+ WebSettings ws = web.getSettings();
+ ws.setSupportZoom(false);
+
+ TypedValue tv = new TypedValue();
+ getActivity().getTheme().resolveAttribute(R.attr.linkColor, tv, true);
+
+ // prevent flicker in ics
+ if (!m_prefs.getBoolean("webview_hardware_accel", true) || useTitleWebView) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ web.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ }
+
+ String theme = m_prefs.getString("theme", CommonActivity.THEME_DEFAULT);
+
+ if (CommonActivity.THEME_HOLO.equals(theme)) {
+ cssOverride = "body { background : transparent; color : #e0e0e0}";
+ } else if (CommonActivity.THEME_DARK.equals(theme)) {
+ cssOverride = "body { background : transparent; color : #e0e0e0}";
+ } else {
+ cssOverride = "body { background : transparent; }";
+ }
+
+ if (useTitleWebView || android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
+ web.setBackgroundColor(Color.TRANSPARENT);
+ } else {
+ // seriously?
+ web.setBackgroundColor(Color.argb(1, 0, 0, 0));
+ }
+
+ String hexColor = String.format("#%06X", (0xFFFFFF & tv.data));
+ cssOverride += " a:link {color: "+hexColor+";} a:visited { color: "+hexColor+";}";
+
+ cssOverride += " table { width : 100%; }";
+
+ 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(m_activity, url)) {
+ img.attr("src", "file://" + ImageCacheService.getCacheFileName(m_activity, url));
+ }
+ }
+ }
+
+ // thanks webview for crashing on <video> tag
+ Elements videos = doc.select("video");
+
+ for (Element video : videos)
+ video.remove();
+
+ articleContent = doc.toString();
+ }
+
+ if (m_prefs.getBoolean("justify_article_text", true)) {
+ cssOverride += "body { text-align : justify; } ";
+ }
+
+ ws.setDefaultFontSize(articleFontSize);
+
+ content =
+ "<html>" +
+ "<head>" +
+ "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">" +
+ "<meta name=\"viewport\" content=\"width=device-width, user-scalable=no\" />" +
+ "<style type=\"text/css\">" +
+ "body { padding : 0px; margin : 0px; line-height : 130%; }" +
+ cssOverride +
+ "img { max-width : 100%; width : auto; height : auto; }" +
+ "</style>" +
+ "</head>" +
+ "<body>" + articleContent;
+
+ if (useTitleWebView) {
+ content += "<p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p>";
+ }
+
+ content += "</body></html>";
+
+ try {
+ String baseUrl = null;
+
+ try {
+ URL url = new URL(link);
+ baseUrl = url.getProtocol() + "://" + url.getHost();
+ } catch (MalformedURLException e) {
+ //
+ }
+
+ web.loadDataWithBaseURL(baseUrl, content, "text/html", "utf-8", null);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ }
+
+
+ }
+
+ TextView dv = (TextView)view.findViewById(R.id.date);
+
+ if (dv != null) {
+ dv.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+
+ Date d = new Date(m_cursor.getInt(m_cursor.getColumnIndex("updated")) * 1000L);
+ DateFormat df = new SimpleDateFormat("MMM dd, HH:mm");
+ dv.setText(df.format(d));
+ }
+
+ TextView author = (TextView)view.findViewById(R.id.author);
+
+ boolean hasAuthor = false;
+
+ if (author != null) {
+ author.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+
+ int authorIndex = m_cursor.getColumnIndex("author");
+ if (authorIndex >= 0)
+ author.setText(m_cursor.getString(authorIndex));
+ else
+ author.setVisibility(View.GONE);
+
+ hasAuthor = true;
+ }
+
+ TextView tagv = (TextView)view.findViewById(R.id.tags);
+
+ if (tagv != null) {
+ tagv.setTextSize(TypedValue.COMPLEX_UNIT_SP, articleSmallFontSize);
+
+ int feedTitleIndex = m_cursor.getColumnIndex("feed_title");
+
+ if (feedTitleIndex != -1 /* && m_isCat */) {
+ String fTitle = m_cursor.getString(feedTitleIndex);
+
+ int authorIndex = m_cursor.getColumnIndex("author");
+
+ if (!hasAuthor && authorIndex >= 0) {
+ fTitle += " (" + getString(R.string.author_formatted, m_cursor.getString(authorIndex)) + ")";
+ }
+
+ tagv.setText(fTitle);
+ } else {
+ String tagsStr = m_cursor.getString(m_cursor.getColumnIndex("tags"));
+ tagv.setText(tagsStr);
+ }
+ }
+
+ }
+
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ m_cursor.close();
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("articleId", m_articleId);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+
+ m_activity = (OfflineActivity) activity;
+
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticlePager.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticlePager.java
new file mode 100644
index 00000000..510ab97b
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineArticlePager.java
@@ -0,0 +1,298 @@
+package org.fox.ttrss.offline;
+
+import org.fox.ttrss.R;
+
+import com.viewpagerindicator.UnderlinePageIndicator;
+
+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.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;
+import android.view.WindowManager;
+
+public class OfflineArticlePager extends Fragment {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private PagerAdapter m_adapter;
+ private OfflineActivity m_activity;
+ private OfflineHeadlinesEventListener m_listener;
+ private boolean m_isCat;
+ private int m_feedId;
+ private int m_articleId;
+ private String m_searchQuery = "";
+ private Cursor m_cursor;
+ private SharedPreferences m_prefs;
+
+ public int getFeedId() {
+ return m_feedId;
+ }
+
+ public boolean getFeedIsCat() {
+ return m_isCat;
+ }
+
+ public Cursor createCursor() {
+ String feedClause = null;
+
+ if (m_isCat) {
+ feedClause = "feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)";
+ } else {
+ feedClause = "feed_id = ?";
+ }
+
+ String viewMode = m_activity.getViewMode();
+
+ if ("adaptive".equals(viewMode)) {
+ // TODO: implement adaptive
+ } else if ("marked".equals(viewMode)) {
+ feedClause += "AND (marked = 1)";
+ } else if ("published".equals(viewMode)) {
+ feedClause += "AND (published = 1)";
+ } else if ("unread".equals(viewMode)) {
+ feedClause += "AND (unread = 1)";
+ } else { // all_articles
+ //
+ }
+
+ String orderBy = (m_prefs.getBoolean("offline_oldest_first", false)) ? "updated" : "updated DESC";
+
+ if (m_searchQuery == null || m_searchQuery.equals("")) {
+ return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
+ new String[] { "articles."+BaseColumns._ID, "feeds.title AS feed_title" }, feedClause,
+ new String[] { String.valueOf(m_feedId) }, null, null, orderBy);
+ } else {
+ return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
+ new String[] { "articles."+BaseColumns._ID },
+ feedClause + " AND (articles.title LIKE '%' || ? || '%' OR content LIKE '%' || ? || '%')",
+ new String[] { String.valueOf(m_feedId), m_searchQuery, m_searchQuery }, null, null, orderBy);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+ }
+
+ private class PagerAdapter extends FragmentStatePagerAdapter {
+ public PagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ Log.d(TAG, "getItem: " + position);
+
+ if (m_cursor.moveToPosition(position)) {
+
+ if (m_prefs.getBoolean("dim_status_bar", false) && getView() != null && !m_activity.isCompatMode()) {
+ getView().setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
+ }
+
+ OfflineArticleFragment oaf = new OfflineArticleFragment();
+ oaf.initialize(m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID)));
+
+ return oaf;
+ }
+
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return m_cursor.getCount();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!m_activity.isCompatMode() && m_prefs.getBoolean("dim_status_bar", false)) {
+ getView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ }
+
+ if (m_prefs.getBoolean("full_screen_mode", false)) {
+ m_activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ /* if (!m_activity.isCompatMode()) {
+ m_activity.getSupportActionBar().hide();
+ } */
+ }
+ }
+
+ public void initialize(int articleId, int feedId, boolean isCat) {
+ m_feedId = feedId;
+ m_isCat = isCat;
+ m_articleId = articleId;
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.article_pager, container, false);
+
+ if (savedInstanceState != null) {
+ m_articleId = savedInstanceState.getInt("articleId", 0);
+ m_feedId = savedInstanceState.getInt("feedId", 0);
+ m_isCat = savedInstanceState.getBoolean("isCat", false);
+ }
+
+ Log.d(TAG, "feed=" + m_feedId + "; iscat=" + m_isCat);
+
+ m_cursor = createCursor();
+
+ m_adapter = new PagerAdapter(getActivity().getSupportFragmentManager());
+
+ int position = 0;
+
+ Log.d(TAG, "maId=" + m_articleId);
+
+ if (m_articleId != 0) {
+ if (m_cursor.moveToFirst()) {
+
+ while (!m_cursor.isAfterLast()) {
+ if (m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID)) == m_articleId) {
+ position = m_cursor.getPosition();
+ break;
+ }
+ m_cursor.moveToNext();
+ }
+
+ Log.d(TAG, "(1)maId=" + m_articleId);
+ m_listener.onArticleSelected(m_articleId, false);
+ }
+ } else {
+ if (m_cursor.moveToFirst()) {
+ m_articleId = m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID));
+ m_listener.onArticleSelected(m_articleId, false);
+
+ Log.d(TAG, "(2)maId=" + m_articleId);
+ }
+ }
+
+
+ ViewPager pager = (ViewPager) view.findViewById(R.id.article_pager);
+
+ pager.setAdapter(m_adapter);
+
+ UnderlinePageIndicator indicator = (UnderlinePageIndicator)view.findViewById(R.id.article_titles);
+ indicator.setViewPager(pager);
+
+ pager.setCurrentItem(position);
+ indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+
+ @Override
+ public void onPageScrollStateChanged(int arg0) {
+ }
+
+ @Override
+ public void onPageScrolled(int arg0, float arg1, int arg2) {
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (m_cursor.moveToPosition(position)) {
+ int articleId = m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID));
+
+ m_articleId = articleId;
+ m_listener.onArticleSelected(articleId, false);
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_activity = (OfflineActivity)activity;
+ m_listener = (OfflineHeadlinesEventListener)activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+
+ }
+
+ public void refresh() {
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+
+ m_cursor = createCursor();
+
+ if (m_cursor != null) {
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ public int getSelectedArticleId() {
+ return m_articleId;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("articleId", m_articleId);
+ out.putInt("feedId", m_feedId);
+ out.putBoolean("isCat", m_isCat);
+
+ }
+
+ public void setSearchQuery(String searchQuery) {
+ m_searchQuery = searchQuery;
+ }
+
+ public void setArticleId(int articleId) {
+ m_articleId = articleId;
+
+ int position = getArticleIdPosition(articleId);
+
+ ViewPager pager = (ViewPager) getView().findViewById(R.id.article_pager);
+
+ pager.setCurrentItem(position);
+
+ }
+
+ public int getArticleIdPosition(int articleId) {
+ m_cursor.moveToFirst();
+
+ while (!m_cursor.isAfterLast()) {
+ if (m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID)) == articleId) {
+ return m_cursor.getPosition();
+ }
+ m_cursor.moveToNext();
+ }
+
+ return -1;
+ }
+
+ public void selectArticle(boolean next) {
+ int position = getArticleIdPosition(m_articleId);
+
+ if (position != -1) {
+ if (next)
+ position++;
+ else
+ position--;
+
+ Log.d(TAG, "pos=" + position);
+
+ if (m_cursor.moveToPosition(position)) {
+ setArticleId(m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID)));
+ }
+ }
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineDownloadService.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineDownloadService.java
new file mode 100644
index 00000000..2d65c890
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineDownloadService.java
@@ -0,0 +1,500 @@
+package org.fox.ttrss.offline;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.OnlineActivity;
+import org.fox.ttrss.R;
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.Feed;
+import org.fox.ttrss.types.FeedCategory;
+import org.fox.ttrss.util.DatabaseHelper;
+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.ActivityManager;
+import android.app.ActivityManager.RunningServiceInfo;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+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.Binder;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.util.Log;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+
+public class OfflineDownloadService extends Service {
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ public static final int NOTIFY_DOWNLOADING = 1;
+ public static final String INTENT_ACTION_SUCCESS = "org.fox.ttrss.intent.action.DownloadComplete";
+ public static final String INTENT_ACTION_CANCEL = "org.fox.ttrss.intent.action.Cancel";
+
+ private static final int OFFLINE_SYNC_SEQ = 50;
+ private static final int OFFLINE_SYNC_MAX = OFFLINE_SYNC_SEQ * 10;
+
+ private SQLiteDatabase m_writableDb;
+ private SQLiteDatabase m_readableDb;
+ private int m_articleOffset = 0;
+ private String m_sessionId;
+ private NotificationManager m_nmgr;
+
+ private boolean m_batchMode = false;
+ private boolean m_downloadInProgress = false;
+ private boolean m_downloadImages = false;
+ private int m_syncMax;
+ private SharedPreferences m_prefs;
+ private boolean m_canProceed = true;
+
+ private final IBinder m_binder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ OfflineDownloadService getService() {
+ return OfflineDownloadService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return m_binder;
+ }
+
+ @Override
+ 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 = Integer.parseInt(m_prefs.getString("offline_sync_max", String.valueOf(OFFLINE_SYNC_MAX)));
+
+ initDatabase();
+ }
+
+ @SuppressWarnings("deprecation")
+ private void updateNotification(String msg) {
+ Notification notification = new Notification(R.drawable.icon,
+ getString(R.string.notify_downloading_title), System.currentTimeMillis());
+
+ Intent intent = new Intent(this, OnlineActivity.class);
+ intent.setAction(INTENT_ACTION_CANCEL);
+
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+ intent, 0);
+
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
+
+ notification.setLatestEventInfo(this, getString(R.string.notify_downloading_title), msg, contentIntent);
+
+ m_nmgr.notify(NOTIFY_DOWNLOADING, notification);
+ }
+
+ private void updateNotification(int msgResId) {
+ updateNotification(getString(msgResId));
+ }
+
+ private void downloadFailed() {
+ m_readableDb.close();
+ m_writableDb.close();
+
+ m_nmgr.cancel(NOTIFY_DOWNLOADING);
+
+ // TODO send notification to activity?
+
+ m_downloadInProgress = false;
+ stopSelf();
+ }
+
+ private boolean isCacheServiceRunning() {
+ ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
+ for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
+ if ("org.fox.ttrss.util.ImageCacheService".equals(service.service.getClassName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void downloadComplete() {
+ m_downloadInProgress = false;
+
+ // if cache service is running, it will send a finished intent on its own
+ if (!isCacheServiceRunning()) {
+ m_nmgr.cancel(NOTIFY_DOWNLOADING);
+
+ if (m_batchMode) {
+
+ SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = localPrefs.edit();
+ editor.putBoolean("offline_mode_active", true);
+ editor.commit();
+
+ } else {
+ Intent intent = new Intent();
+ intent.setAction(INTENT_ACTION_SUCCESS);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ sendBroadcast(intent);
+ }
+ } else {
+ updateNotification(getString(R.string.notify_downloading_images, 0));
+ }
+
+ m_readableDb.close();
+ m_writableDb.close();
+
+ stopSelf();
+ }
+
+ private void initDatabase() {
+ DatabaseHelper dh = new DatabaseHelper(getApplicationContext());
+ m_writableDb = dh.getWritableDatabase();
+ m_readableDb = dh.getReadableDatabase();
+ }
+
+ /* private synchronized SQLiteDatabase getReadableDb() {
+ return m_readableDb;
+ } */
+
+ private synchronized SQLiteDatabase getWritableDb() {
+ return m_writableDb;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void downloadArticles() {
+ Log.d(TAG, "offline: downloading articles... offset=" + m_articleOffset);
+
+ updateNotification(getString(R.string.notify_downloading_articles, m_articleOffset));
+
+ OfflineArticlesRequest req = new OfflineArticlesRequest(this);
+
+ @SuppressWarnings("serial")
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ put("op", "getHeadlines");
+ put("sid", m_sessionId);
+ put("feed_id", "-4");
+ put("view_mode", "unread");
+ put("show_content", "true");
+ put("skip", String.valueOf(m_articleOffset));
+ put("limit", String.valueOf(OFFLINE_SYNC_SEQ));
+ }
+ };
+
+ req.execute(map);
+ }
+
+ private void downloadFeeds() {
+
+ updateNotification(R.string.notify_downloading_feeds);
+
+ getWritableDb().execSQL("DELETE FROM feeds;");
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ @Override
+ protected JsonElement doInBackground(HashMap<String, String>... params) {
+ JsonElement content = super.doInBackground(params);
+
+ if (content != null) {
+
+ try {
+ Type listType = new TypeToken<List<Feed>>() {}.getType();
+ List<Feed> feeds = new Gson().fromJson(content, listType);
+
+ SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO feeds " +
+ "("+BaseColumns._ID+", title, feed_url, has_icon, cat_id) " +
+ "VALUES (?, ?, ?, ?, ?);");
+
+ for (Feed feed : feeds) {
+ stmtInsert.bindLong(1, feed.id);
+ stmtInsert.bindString(2, feed.title);
+ stmtInsert.bindString(3, feed.feed_url);
+ stmtInsert.bindLong(4, feed.has_icon ? 1 : 0);
+ stmtInsert.bindLong(5, feed.cat_id);
+
+ stmtInsert.execute();
+ }
+
+ stmtInsert.close();
+
+ Log.d(TAG, "offline: done downloading feeds");
+
+ m_articleOffset = 0;
+
+ getWritableDb().execSQL("DELETE FROM articles;");
+ } catch (Exception e) {
+ e.printStackTrace();
+ updateNotification(R.string.offline_switch_error);
+ downloadFailed();
+ }
+ }
+
+ return content;
+ }
+
+ @Override
+ protected void onPostExecute(JsonElement content) {
+ if (content != null) {
+ if (m_canProceed) {
+ downloadArticles();
+ } else {
+ downloadFailed();
+ }
+ } else {
+ updateNotification(getErrorMessage());
+ downloadFailed();
+ }
+ }
+
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ put("op", "getFeeds");
+ put("sid", m_sessionId);
+ put("cat_id", "-3");
+ put("unread_only", "true");
+ }
+ };
+
+ req.execute(map);
+ }
+
+ private void downloadCategories() {
+
+ updateNotification(R.string.notify_downloading_feeds);
+
+ getWritableDb().execSQL("DELETE FROM categories;");
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected JsonElement doInBackground(HashMap<String, String>... params) {
+ JsonElement content = super.doInBackground(params);
+
+ if (content != null) {
+ try {
+ Type listType = new TypeToken<List<FeedCategory>>() {}.getType();
+ List<FeedCategory> cats = new Gson().fromJson(content, listType);
+
+ SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO categories " +
+ "("+BaseColumns._ID+", title) " +
+ "VALUES (?, ?);");
+
+ for (FeedCategory cat : cats) {
+ stmtInsert.bindLong(1, cat.id);
+ stmtInsert.bindString(2, cat.title);
+
+ stmtInsert.execute();
+ }
+
+ stmtInsert.close();
+
+ Log.d(TAG, "offline: done downloading categories");
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ updateNotification(R.string.offline_switch_error);
+ downloadFailed();
+ }
+ }
+
+ return content;
+ }
+ @Override
+ protected void onPostExecute(JsonElement content) {
+ if (content != null) {
+ if (m_canProceed) {
+ downloadFeeds();
+ } else {
+ downloadFailed();
+ }
+ } else {
+ updateNotification(getErrorMessage());
+ downloadFailed();
+ }
+ }
+
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String,String> map = new HashMap<String,String>() {
+ {
+ put("op", "getCategories");
+ put("sid", m_sessionId);
+ //put("cat_id", "-3");
+ put("unread_only", "true");
+ }
+ };
+
+ req.execute(map);
+ }
+
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ m_nmgr.cancel(NOTIFY_DOWNLOADING);
+
+ m_canProceed = false;
+ Log.d(TAG, "onDestroy");
+
+ //m_readableDb.close();
+ //m_writableDb.close();
+ }
+
+ public class OfflineArticlesRequest extends ApiRequest {
+ List<Article> m_articles;
+
+ public OfflineArticlesRequest(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected JsonElement doInBackground(HashMap<String, String>... params) {
+ JsonElement content = super.doInBackground(params);
+
+ if (content != null) {
+
+ try {
+ Type listType = new TypeToken<List<Article>>() {}.getType();
+ m_articles = new Gson().fromJson(content, listType);
+
+ SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO articles " +
+ "("+BaseColumns._ID+", unread, marked, published, score, updated, is_updated, title, link, feed_id, tags, content, author) " +
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
+
+ for (Article article : m_articles) {
+
+ String tagsString = "";
+
+ for (String t : article.tags) {
+ tagsString += t + ", ";
+ }
+
+ tagsString = tagsString.replaceAll(", $", "");
+
+ int index = 1;
+ stmtInsert.bindLong(index++, article.id);
+ stmtInsert.bindLong(index++, article.unread ? 1 : 0);
+ stmtInsert.bindLong(index++, article.marked ? 1 : 0);
+ stmtInsert.bindLong(index++, article.published ? 1 : 0);
+ stmtInsert.bindLong(index++, article.score);
+ stmtInsert.bindLong(index++, article.updated);
+ stmtInsert.bindLong(index++, article.is_updated ? 1 : 0);
+ stmtInsert.bindString(index++, article.title);
+ stmtInsert.bindString(index++, article.link);
+ stmtInsert.bindLong(index++, article.feed_id);
+ stmtInsert.bindString(index++, tagsString); // comma-separated tags
+ stmtInsert.bindString(index++, article.content);
+ stmtInsert.bindString(index++, article.author != null ? article.author : "");
+
+ 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(OfflineDownloadService.this, url)) {
+ Intent intent = new Intent(OfflineDownloadService.this,
+ ImageCacheService.class);
+
+ intent.putExtra("url", url);
+ startService(intent);
+ }
+ }
+ }
+ }
+ }
+
+ try {
+ stmtInsert.execute();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ m_articleOffset += m_articles.size();
+
+ Log.d(TAG, "offline: received " + m_articles.size() + " articles; canProc=" + m_canProceed);
+
+ stmtInsert.close();
+
+ } catch (Exception e) {
+ updateNotification(R.string.offline_switch_error);
+ Log.d(TAG, "offline: failed: exception when loading articles");
+ e.printStackTrace();
+ downloadFailed();
+ }
+
+ }
+
+ return content;
+ }
+
+ @Override
+ protected void onPostExecute(JsonElement content) {
+ if (content != null) {
+
+ if (m_canProceed && m_articles != null) {
+ if (m_articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < m_syncMax) {
+ downloadArticles();
+ } else {
+ downloadComplete();
+ }
+ } else {
+ downloadFailed();
+ }
+
+ } else {
+ Log.d(TAG, "offline: failed: " + getErrorMessage());
+ updateNotification(getErrorMessage());
+ downloadFailed();
+ }
+ }
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ try {
+ if (getWritableDb().isDbLockedByCurrentThread() || getWritableDb().isDbLockedByOtherThreads()) {
+ return;
+ }
+
+ m_sessionId = intent.getStringExtra("sessionId");
+ m_batchMode = intent.getBooleanExtra("batchMode", false);
+
+ if (!m_downloadInProgress) {
+ if (m_downloadImages) ImageCacheService.cleanupCache(this, false);
+
+ updateNotification(R.string.notify_downloading_init);
+ m_downloadInProgress = true;
+
+ downloadCategories();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedCategoriesFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedCategoriesFragment.java
new file mode 100644
index 00000000..8fde8176
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedCategoriesFragment.java
@@ -0,0 +1,337 @@
+package org.fox.ttrss.offline;
+
+import org.fox.ttrss.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SimpleCursorAdapter;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class OfflineFeedCategoriesFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener {
+ private final String TAG = this.getClass().getSimpleName();
+ private SharedPreferences m_prefs;
+ private FeedCategoryListAdapter m_adapter;
+ private int m_selectedCatId;
+ private Cursor m_cursor;
+ private OfflineFeedsActivity m_activity;
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+
+ getActivity().getMenuInflater().inflate(R.menu.category_menu, menu);
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+ Cursor cursor = (Cursor)m_adapter.getItem(info.position);
+
+ if (cursor != null)
+ menu.setHeaderTitle(cursor.getString(cursor.getColumnIndex("title")));
+
+ if (!m_activity.isSmallScreen()) {
+ menu.findItem(R.id.browse_articles).setVisible(false);
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ public Cursor createCursor() {
+ String unreadOnly = BaseColumns._ID + "> 0 AND " + (m_activity.getUnreadOnly() ? "unread > 0" : "1");
+
+ String order = m_prefs.getBoolean("sort_feeds_by_unread", false) ? "unread DESC, title" : "title";
+
+ return m_activity.getReadableDb().query("cats_unread",
+ null, unreadOnly, null, null, null, order);
+ }
+
+ public void refresh() {
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+
+ m_cursor = createCursor();
+
+ if (m_cursor != null && m_adapter != null) {
+ m_adapter.changeCursor(m_cursor);
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refresh();
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo();
+
+ switch (item.getItemId()) {
+ case R.id.browse_articles:
+ if (true) {
+ int catId = getCatIdAtPosition(info.position);
+ if (catId != -10000) {
+ m_activity.openFeedArticles(catId, true);
+ }
+ }
+ return true;
+ case R.id.browse_headlines:
+ if (true) {
+ int catId = getCatIdAtPosition(info.position);
+ if (catId != -10000) {
+ m_activity.onCatSelected(catId, true);
+ }
+ }
+ return true;
+ case R.id.browse_feeds:
+ if (true) {
+ int catId = getCatIdAtPosition(info.position);
+ if (catId != -10000) {
+ m_activity.onCatSelected(catId, false);
+ }
+ }
+ return true;
+ case R.id.catchup_category:
+ if (true) {
+ int catId = getCatIdAtPosition(info.position);
+ if (catId != -10000) {
+ m_activity.catchupFeed(catId, true);
+ }
+ }
+ return true;
+ default:
+ Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ if (savedInstanceState != null) {
+ m_selectedCatId = savedInstanceState.getInt("selectedFeedId");
+ }
+
+ View view = inflater.inflate(R.layout.feeds_fragment, container, false);
+
+ ListView list = (ListView)view.findViewById(R.id.feeds);
+
+ m_cursor = createCursor();
+
+ m_adapter = new FeedCategoryListAdapter(getActivity(), R.layout.feeds_row, m_cursor,
+ new String[] { "title", "unread" }, new int[] { R.id.title, R.id.unread_counter }, 0);
+
+ list.setAdapter(m_adapter);
+ list.setOnItemClickListener(this);
+ list.setEmptyView(view.findViewById(R.id.no_feeds));
+ registerForContextMenu(list);
+
+ view.findViewById(R.id.loading_container).setVisibility(View.GONE);
+
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_activity = (OfflineFeedsActivity)activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ m_prefs.registerOnSharedPreferenceChangeListener(this);
+
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("selectedFeedId", m_selectedCatId);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> av, View view, int position, long id) {
+ ListView list = (ListView)getActivity().findViewById(R.id.feeds);
+
+ if (list != null) {
+ Cursor cursor = (Cursor) list.getItemAtPosition(position);
+
+ if (cursor != null) {
+ int feedId = (int) cursor.getLong(0);
+ Log.d(TAG, "clicked on feed " + feedId);
+
+ if (m_activity.isSmallScreen() && "ARTICLES".equals(m_prefs.getString("default_view_mode", "HEADLINES")) &&
+ m_prefs.getBoolean("browse_cats_like_feeds", false)) {
+
+ m_activity.openFeedArticles(feedId, true);
+
+ } else {
+ m_activity.onCatSelected(feedId);
+ }
+
+ /* if (!m_activity.isSmallScreen())
+ m_selectedCatId = feedId; */
+
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+ }
+
+ /* public void setLoadingStatus(int status, boolean showProgress) {
+ if (getView() != null) {
+ TextView tv = (TextView)getView().findViewById(R.id.loading_message);
+
+ if (tv != null) {
+ tv.setText(status);
+ }
+ }
+
+ getActivity().setProgressBarIndeterminateVisibility(showProgress);
+ } */
+
+ private class FeedCategoryListAdapter extends SimpleCursorAdapter {
+
+
+ public FeedCategoryListAdapter(Context context, int layout, Cursor c,
+ String[] from, int[] to, int flags) {
+ super(context, layout, c, from, to, flags);
+ }
+
+ public static final int VIEW_NORMAL = 0;
+ public static final int VIEW_SELECTED = 1;
+
+ public static final int VIEW_COUNT = VIEW_SELECTED+1;
+
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Cursor cursor = (Cursor) this.getItem(position);
+
+ if (!m_activity.isSmallScreen() && cursor.getLong(0) == m_selectedCatId) {
+ return VIEW_SELECTED;
+ } else {
+ return VIEW_NORMAL;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+
+ Cursor cursor = (Cursor)getItem(position);
+
+ if (v == null) {
+ int layoutId = R.layout.feeds_row;
+
+ switch (getItemViewType(position)) {
+ case VIEW_SELECTED:
+ layoutId = R.layout.feeds_row_selected;
+ break;
+ }
+
+ LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(layoutId, null);
+
+ }
+
+ TextView tt = (TextView) v.findViewById(R.id.title);
+
+ if (tt != null) {
+ tt.setText(cursor.getString(cursor.getColumnIndex("title")));
+ }
+
+ TextView tu = (TextView) v.findViewById(R.id.unread_counter);
+
+ if (tu != null) {
+ tu.setText(String.valueOf(cursor.getInt(cursor.getColumnIndex("unread"))));
+ tu.setVisibility((cursor.getInt(cursor.getColumnIndex("unread")) > 0) ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ ImageView icon = (ImageView)v.findViewById(R.id.icon);
+
+ if (icon != null) {
+ icon.setImageResource(cursor.getInt(cursor.getColumnIndex("unread")) > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+
+ ImageButton ib = (ImageButton) v.findViewById(R.id.feed_menu_button);
+
+ if (ib != null) {
+ ib.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().openContextMenu(v);
+ }
+ });
+ }
+
+
+ return v;
+ }
+ }
+
+ public void sortCategories() {
+ try {
+ refresh();
+ } catch (NullPointerException e) {
+ // activity is gone?
+ } catch (IllegalStateException e) {
+ // we're probably closing and DB is gone already
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+
+ sortCategories();
+ }
+
+ public int getCatIdAtPosition(int position) {
+ Cursor c = (Cursor)m_adapter.getItem(position);
+
+ if (c != null) {
+ int catId = c.getInt(0);
+ return catId;
+ }
+
+ return -10000;
+ }
+
+ public void setSelectedFeedId(int feedId) {
+ m_selectedCatId = feedId;
+ refresh();
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsActivity.java
new file mode 100644
index 00000000..f7263fe0
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsActivity.java
@@ -0,0 +1,349 @@
+package org.fox.ttrss.offline;
+
+import org.fox.ttrss.GlobalState;
+import org.fox.ttrss.R;
+
+import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;
+
+import android.animation.LayoutTransition;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.database.sqlite.SQLiteStatement;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+public class OfflineFeedsActivity extends OfflineActivity implements OfflineHeadlinesEventListener {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private boolean m_actionbarUpEnabled = false;
+ private int m_actionbarRevertDepth = 0;
+ private SlidingMenu m_slidingMenu;
+ private boolean m_feedIsSelected = false;
+ private boolean m_feedWasSelected = false;
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ setAppTheme(m_prefs);
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.headlines);
+
+ setStatusBarTint();
+ setSmallScreen(findViewById(R.id.sw600dp_anchor) == null &&
+ findViewById(R.id.sw600dp_port_anchor) == null);
+
+ GlobalState.getInstance().load(savedInstanceState);
+
+ if (isSmallScreen() || findViewById(R.id.sw600dp_port_anchor) != null) {
+ m_slidingMenu = new SlidingMenu(this);
+
+ /* if (findViewById(R.id.sw600dp_port_anchor) != null) {
+ m_slidingMenu.setBehindWidth(getScreenWidthInPixel() * 2/3);
+ } */
+
+ m_slidingMenu.setMode(SlidingMenu.LEFT);
+ m_slidingMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
+ m_slidingMenu.attachToActivity(this, SlidingMenu.SLIDING_CONTENT);
+ m_slidingMenu.setSlidingEnabled(true);
+ m_slidingMenu.setMenu(R.layout.feeds);
+
+ m_slidingMenu.setOnClosedListener(new SlidingMenu.OnClosedListener() {
+
+ @Override
+ public void onClosed() {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ m_actionbarUpEnabled = true;
+ m_feedIsSelected = true;
+
+ initMenu();
+ }
+ });
+
+ m_slidingMenu.setOnOpenedListener(new SlidingMenu.OnOpenedListener() {
+
+ @Override
+ public void onOpened() {
+ if (m_actionbarRevertDepth == 0) {
+ m_actionbarUpEnabled = false;
+ m_feedIsSelected = false;
+ getSupportActionBar().setDisplayHomeAsUpEnabled(false);
+ refresh();
+ }
+
+ initMenu();
+ }
+ });
+ }
+
+ if (savedInstanceState != null) {
+
+ m_actionbarUpEnabled = savedInstanceState.getBoolean("actionbarUpEnabled");
+ m_actionbarRevertDepth = savedInstanceState.getInt("actionbarRevertDepth");
+ m_feedIsSelected = savedInstanceState.getBoolean("feedIsSelected");
+ m_feedWasSelected = savedInstanceState.getBoolean("feedWasSelected");
+
+ if (findViewById(R.id.sw600dp_port_anchor) != null && m_feedWasSelected && m_slidingMenu != null) {
+ m_slidingMenu.setBehindWidth(getScreenWidthInPixel() * 2/3);
+ }
+
+ if (m_slidingMenu != null && m_feedIsSelected == false) {
+ m_slidingMenu.showMenu();
+ } else if (m_slidingMenu != null) {
+ m_actionbarUpEnabled = true;
+ } else {
+ m_actionbarUpEnabled = m_actionbarRevertDepth > 0;
+ }
+
+ if (m_actionbarUpEnabled) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ } else {
+ if (m_slidingMenu != null)
+ m_slidingMenu.showMenu();
+
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+
+ if (m_prefs.getBoolean("enable_cats", false)) {
+ ft.replace(R.id.feeds_fragment, new OfflineFeedCategoriesFragment(), FRAG_CATS);
+ } else {
+ ft.replace(R.id.feeds_fragment, new OfflineFeedsFragment(), FRAG_FEEDS);
+ }
+
+ ft.commit();
+ }
+
+ setLoadingStatus(R.string.blank, false);
+
+ initMenu();
+
+ if (!isCompatMode() && !isSmallScreen()) {
+ ((ViewGroup)findViewById(R.id.headlines_fragment)).setLayoutTransition(new LayoutTransition());
+ ((ViewGroup)findViewById(R.id.feeds_fragment)).setLayoutTransition(new LayoutTransition());
+ }
+ }
+
+ public void openFeedArticles(int feedId, boolean isCat) {
+ if (isSmallScreen()) {
+ Intent intent = new Intent(OfflineFeedsActivity.this, OfflineHeadlinesActivity.class);
+
+ intent.putExtra("feed", feedId);
+ intent.putExtra("isCat", isCat);
+ intent.putExtra("article", 0);
+ startActivityForResult(intent, 0);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (m_actionbarRevertDepth > 0) {
+
+ if (m_feedIsSelected && m_slidingMenu != null && !m_slidingMenu.isMenuShowing()) {
+ m_slidingMenu.showMenu();
+ } else {
+ m_actionbarRevertDepth = m_actionbarRevertDepth - 1;
+ m_actionbarUpEnabled = m_actionbarRevertDepth > 0;
+ getSupportActionBar().setDisplayHomeAsUpEnabled(m_actionbarUpEnabled);
+
+ onBackPressed();
+ }
+ } else if (m_slidingMenu != null && !m_slidingMenu.isMenuShowing()) {
+ m_slidingMenu.showMenu();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (m_actionbarUpEnabled)
+ onBackPressed();
+ return true;
+ case R.id.show_feeds:
+ setUnreadOnly(!getUnreadOnly());
+ initMenu();
+ refresh();
+ return true;
+ default:
+ Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putBoolean("actionbarUpEnabled", m_actionbarUpEnabled);
+ out.putInt("actionbarRevertDepth", m_actionbarRevertDepth);
+ out.putBoolean("feedIsSelected", m_feedIsSelected);
+ out.putBoolean("feedWasSelected", m_feedWasSelected);
+
+
+ //if (m_slidingMenu != null )
+ // out.putBoolean("slidingMenuVisible", m_slidingMenu.isMenuShowing());
+
+ GlobalState.getInstance().save(out);
+ }
+
+ public void initMenu() {
+ super.initMenu();
+
+ if (m_menu != null) {
+ Fragment ff = getSupportFragmentManager().findFragmentByTag(FRAG_FEEDS);
+ Fragment cf = getSupportFragmentManager().findFragmentByTag(FRAG_CATS);
+ OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ if (m_slidingMenu != null) {
+ m_menu.setGroupVisible(R.id.menu_group_feeds, m_slidingMenu.isMenuShowing());
+ m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.isAdded() && !m_slidingMenu.isMenuShowing());
+ } else {
+ m_menu.setGroupVisible(R.id.menu_group_feeds, (ff != null && ff.isAdded()) || (cf != null && cf.isAdded()));
+ m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.isAdded());
+ }
+
+ m_menu.findItem(R.id.headlines_toggle_sidebar).setVisible(false);
+
+ MenuItem item = m_menu.findItem(R.id.show_feeds);
+
+ if (getUnreadOnly()) {
+ item.setTitle(R.string.menu_all_feeds);
+ } else {
+ item.setTitle(R.string.menu_unread_feeds);
+ }
+ }
+ }
+
+ public void onCatSelected(int catId) {
+ onCatSelected(catId, m_prefs.getBoolean("browse_cats_like_feeds", false));
+ }
+
+ public void onCatSelected(int catId, boolean openAsFeed) {
+ OfflineFeedCategoriesFragment fc = (OfflineFeedCategoriesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_CATS);
+
+ if (openAsFeed) {
+ if (fc != null) {
+ fc.setSelectedFeedId(catId);
+ }
+
+ onFeedSelected(catId, true, true);
+ } else {
+ if (fc != null) {
+ fc.setSelectedFeedId(-1);
+ }
+
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+
+ OfflineFeedsFragment ff = new OfflineFeedsFragment();
+ ff.initialize(catId);
+
+ ft.replace(R.id.feeds_fragment, ff, FRAG_FEEDS);
+ ft.addToBackStack(null);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ m_actionbarUpEnabled = true;
+ m_actionbarRevertDepth = m_actionbarRevertDepth + 1;
+
+ ft.commit();
+ }
+ }
+
+ public void onFeedSelected(int feedId) {
+ onFeedSelected(feedId, false, true);
+ }
+
+ public void onFeedSelected(final int feedId, final boolean isCat, boolean open) {
+
+ if (open) {
+ if (!isSmallScreen()) {
+ LinearLayout container = (LinearLayout) findViewById(R.id.fragment_container);
+ if (container != null) {
+ container.setWeightSum(3f);
+ }
+ }
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ FragmentTransaction ft = getSupportFragmentManager()
+ .beginTransaction();
+
+ OfflineHeadlinesFragment hf = new OfflineHeadlinesFragment();
+ hf.initialize(feedId, isCat);
+ ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
+
+ ft.commit();
+
+ m_feedIsSelected = true;
+ m_feedWasSelected = true;
+
+ if (m_slidingMenu != null) {
+ if (findViewById(R.id.sw600dp_port_anchor) != null) {
+ m_slidingMenu.setBehindWidth(getScreenWidthInPixel() * 2/3);
+ }
+
+ m_slidingMenu.showContent();
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ m_actionbarUpEnabled = true;
+ }
+ }
+ }, 10);
+ }
+ }
+
+ @Override
+ public void onArticleSelected(int articleId, boolean open) {
+
+ if (!open) {
+ SQLiteStatement stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 " + "WHERE " + BaseColumns._ID
+ + " = ?");
+
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+ }
+
+ initMenu();
+
+ if (open) {
+ OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ Intent intent = new Intent(OfflineFeedsActivity.this, OfflineHeadlinesActivity.class);
+ intent.putExtra("feed", hf.getFeedId());
+ intent.putExtra("isCat", hf.getFeedIsCat());
+ intent.putExtra("article", articleId);
+
+ startActivityForResult(intent, 0);
+
+ overridePendingTransition(R.anim.right_slide_in, 0);
+
+ } else {
+ refresh();
+ }
+
+ initMenu();
+
+ }
+
+ @Override
+ public void onArticleSelected(int articleId) {
+ onArticleSelected(articleId, true);
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsFragment.java
new file mode 100644
index 00000000..8c04d0cd
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsFragment.java
@@ -0,0 +1,372 @@
+package org.fox.ttrss.offline;
+
+import java.io.File;
+
+import org.fox.ttrss.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SimpleCursorAdapter;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class OfflineFeedsFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener {
+ private final String TAG = this.getClass().getSimpleName();
+ private SharedPreferences m_prefs;
+ private FeedListAdapter m_adapter;
+ private static final String ICON_PATH = "/data/org.fox.ttrss/icons/";
+ private int m_selectedFeedId;
+ private int m_catId = -1;
+ private boolean m_enableFeedIcons;
+ private Cursor m_cursor;
+ private OfflineFeedsActivity m_activity;
+
+ public void initialize(int catId) {
+ m_catId = catId;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refresh();
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo();
+ switch (item.getItemId()) {
+ case R.id.browse_articles:
+ if (true) {
+ int feedId = getFeedIdAtPosition(info.position);
+ if (feedId != -10000) {
+ m_activity.openFeedArticles(feedId, false);
+ }
+ }
+ return true;
+ case R.id.browse_headlines:
+ if (true) {
+ int feedId = getFeedIdAtPosition(info.position);
+ if (feedId != -10000) {
+ m_activity.onFeedSelected(feedId);
+ }
+ }
+ return true;
+ case R.id.catchup_feed:
+ int feedId = getFeedIdAtPosition(info.position);
+ if (feedId != -10000) {
+ m_activity.catchupFeed(feedId, false);
+ }
+ 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.feed_menu, menu);
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+ Cursor cursor = (Cursor)m_adapter.getItem(info.position);
+
+ if (cursor != null)
+ menu.setHeaderTitle(cursor.getString(cursor.getColumnIndex("title")));
+
+ if (!m_activity.isSmallScreen()) {
+ menu.findItem(R.id.browse_articles).setVisible(false);
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ public Cursor createCursor() {
+ String unreadOnly = m_activity.getUnreadOnly() ? "unread > 0" : "1";
+ String order = m_prefs.getBoolean("sort_feeds_by_unread", false) ? "unread DESC, title" : "title";
+
+ if (m_catId != -1) {
+ return m_activity.getReadableDb().query("feeds_unread",
+ null, unreadOnly + " AND cat_id = ?", new String[] { String.valueOf(m_catId) }, null, null, order);
+ } else {
+ return m_activity.getReadableDb().query("feeds_unread",
+ null, unreadOnly, null, null, null, order);
+ }
+ }
+
+ public void refresh() {
+ try {
+ if (!isAdded()) return;
+
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+
+ m_cursor = createCursor();
+
+ if (m_cursor != null && m_adapter != null) {
+ m_adapter.changeCursor(m_cursor);
+ m_adapter.notifyDataSetChanged();
+ }
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ if (savedInstanceState != null) {
+ m_selectedFeedId = savedInstanceState.getInt("selectedFeedId");
+ m_catId = savedInstanceState.getInt("catId");
+ }
+
+ View view = inflater.inflate(R.layout.feeds_fragment, container, false);
+
+ ListView list = (ListView)view.findViewById(R.id.feeds);
+
+ m_cursor = createCursor();
+
+ m_adapter = new FeedListAdapter(getActivity(), R.layout.feeds_row, m_cursor,
+ new String[] { "title", "unread" }, new int[] { R.id.title, R.id.unread_counter }, 0);
+
+ list.setAdapter(m_adapter);
+ list.setOnItemClickListener(this);
+ list.setEmptyView(view.findViewById(R.id.no_feeds));
+ registerForContextMenu(list);
+
+ view.findViewById(R.id.loading_container).setVisibility(View.GONE);
+
+ m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false);
+
+ return view;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_activity = (OfflineFeedsActivity)activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ m_prefs.registerOnSharedPreferenceChangeListener(this);
+
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("selectedFeedId", m_selectedFeedId);
+ out.putInt("catId", m_catId);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> av, View view, int position, long id) {
+ ListView list = (ListView)getActivity().findViewById(R.id.feeds);
+
+ if (list != null) {
+ Cursor cursor = (Cursor) list.getItemAtPosition(position);
+
+ if (cursor != null) {
+ int feedId = (int) cursor.getLong(0);
+ Log.d(TAG, "clicked on feed " + feedId);
+
+ if (!m_activity.isSmallScreen() && "ARTICLES".equals(m_prefs.getString("default_view_mode", "HEADLINES"))) {
+ m_activity.openFeedArticles(feedId, false);
+ } else {
+ m_activity.onFeedSelected(feedId);
+ }
+
+ if (!m_activity.isSmallScreen())
+ m_selectedFeedId = feedId;
+
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+ }
+
+ /* public void setLoadingStatus(int status, boolean showProgress) {
+ if (getView() != null) {
+ TextView tv = (TextView)getView().findViewById(R.id.loading_message);
+
+ if (tv != null) {
+ tv.setText(status);
+ }
+ }
+
+ getActivity().setProgressBarIndeterminateVisibility(showProgress);
+ } */
+
+ private class FeedListAdapter extends SimpleCursorAdapter {
+
+
+ public FeedListAdapter(Context context, int layout, Cursor c,
+ String[] from, int[] to, int flags) {
+ super(context, layout, c, from, to, flags);
+ }
+
+ public static final int VIEW_NORMAL = 0;
+ public static final int VIEW_SELECTED = 1;
+
+ public static final int VIEW_COUNT = VIEW_SELECTED+1;
+
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Cursor cursor = (Cursor) this.getItem(position);
+
+ if (!m_activity.isSmallScreen() && cursor.getLong(0) == m_selectedFeedId) {
+ return VIEW_SELECTED;
+ } else {
+ return VIEW_NORMAL;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+
+ Cursor cursor = (Cursor)getItem(position);
+
+ if (v == null) {
+ int layoutId = R.layout.feeds_row;
+
+ switch (getItemViewType(position)) {
+ case VIEW_SELECTED:
+ layoutId = R.layout.feeds_row_selected;
+ break;
+ }
+
+ LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(layoutId, null);
+
+ }
+
+ TextView tt = (TextView) v.findViewById(R.id.title);
+
+ if (tt != null) {
+ tt.setText(cursor.getString(cursor.getColumnIndex("title")));
+ }
+
+ TextView tu = (TextView) v.findViewById(R.id.unread_counter);
+
+ if (tu != null) {
+ tu.setText(String.valueOf(cursor.getInt(cursor.getColumnIndex("unread"))));
+ tu.setVisibility((cursor.getInt(cursor.getColumnIndex("unread")) > 0) ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ ImageView icon = (ImageView)v.findViewById(R.id.icon);
+
+ if (icon != null) {
+
+ if (m_enableFeedIcons) {
+
+ try {
+ File storage = Environment.getExternalStorageDirectory();
+
+ File iconFile = new File(storage.getAbsolutePath() + ICON_PATH + cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)) + ".ico");
+ if (iconFile.exists()) {
+ Bitmap bmpOrig = BitmapFactory.decodeFile(iconFile.getAbsolutePath());
+ if (bmpOrig != null) {
+ icon.setImageBitmap(bmpOrig);
+ }
+ } else {
+ icon.setImageResource(cursor.getInt(cursor.getColumnIndex("unread")) > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+ } catch (NullPointerException e) {
+ icon.setImageResource(cursor.getInt(cursor.getColumnIndex("unread")) > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+
+ } else {
+ icon.setImageResource(cursor.getInt(cursor.getColumnIndex("unread")) > 0 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+ }
+
+ }
+
+ ImageButton ib = (ImageButton) v.findViewById(R.id.feed_menu_button);
+
+ if (ib != null) {
+ ib.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().openContextMenu(v);
+ }
+ });
+ }
+
+ return v;
+ }
+ }
+
+ public void sortFeeds() {
+ try {
+ refresh();
+ } catch (NullPointerException e) {
+ // activity is gone?
+ } catch (IllegalStateException e) {
+ // we're probably closing and DB is gone already
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+
+ sortFeeds();
+ m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false);
+
+ }
+
+ public int getFeedIdAtPosition(int position) {
+ Cursor c = (Cursor)m_adapter.getItem(position);
+
+ if (c != null) {
+ int feedId = c.getInt(0);
+ return feedId;
+ }
+
+ return -10000;
+ }
+
+ public void setSelectedFeedId(int feedId) {
+ m_selectedFeedId = feedId;
+ refresh();
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesActivity.java
new file mode 100644
index 00000000..de57c985
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesActivity.java
@@ -0,0 +1,167 @@
+package org.fox.ttrss.offline;
+
+import org.fox.ttrss.GlobalState;
+import org.fox.ttrss.R;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteStatement;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+
+public class OfflineHeadlinesActivity extends OfflineActivity implements OfflineHeadlinesEventListener {
+ private final String TAG = this.getClass().getSimpleName();
+
+ protected SharedPreferences m_prefs;
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ setAppTheme(m_prefs);
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.headlines_articles);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setStatusBarTint();
+ setSmallScreen(findViewById(R.id.sw600dp_anchor) == null);
+
+ if (isPortrait() || m_prefs.getBoolean("headlines_hide_sidebar", false)) {
+ findViewById(R.id.headlines_fragment).setVisibility(View.GONE);
+ }
+
+ if (savedInstanceState == null) {
+ Intent i = getIntent();
+
+ if (i.getExtras() != null) {
+ int feedId = i.getIntExtra("feed", 0);
+ boolean isCat = i.getBooleanExtra("isCat", false);
+ int articleId = i.getIntExtra("article", 0);
+ String searchQuery = i.getStringExtra("searchQuery");
+
+ OfflineHeadlinesFragment hf = new OfflineHeadlinesFragment();
+ hf.initialize(feedId, isCat);
+
+ OfflineArticlePager af = new OfflineArticlePager();
+ af.initialize(articleId, feedId, isCat);
+
+ hf.setActiveArticleId(articleId);
+
+ hf.setSearchQuery(searchQuery);
+ af.setSearchQuery(searchQuery);
+
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+
+ ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
+ ft.replace(R.id.article_fragment, af, FRAG_ARTICLE);
+
+ ft.commit();
+
+ Cursor c;
+
+ if (isCat) {
+ c = getCatById(feedId);
+ } else {
+ c = getFeedById(feedId);
+ }
+
+ if (c != null) {
+ setTitle(c.getString(c.getColumnIndex("title")));
+ c.close();
+ }
+
+ }
+ }
+
+ setLoadingStatus(R.string.blank, false);
+ findViewById(R.id.loading_container).setVisibility(View.GONE);
+
+ initMenu();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ overridePendingTransition(0, R.anim.right_slide_out);
+ return true;
+ default:
+ Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onArticleSelected(int articleId, boolean open) {
+
+ if (!open) {
+ SQLiteStatement stmt = getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 " + "WHERE " + BaseColumns._ID
+ + " = ?");
+
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+ }
+
+ if (open) {
+ OfflineArticlePager af = (OfflineArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ af.setArticleId(articleId);
+ } else {
+ OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ hf.setActiveArticleId(articleId);
+ }
+
+ GlobalState.getInstance().m_selectedArticleId = articleId;
+
+ initMenu();
+ refresh();
+ }
+
+ @Override
+ protected void initMenu() {
+ super.initMenu();
+
+ if (m_menu != null) {
+ m_menu.setGroupVisible(R.id.menu_group_feeds, false);
+
+ //OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
+
+ m_menu.setGroupVisible(R.id.menu_group_headlines, !isPortrait() && !isSmallScreen());
+ m_menu.findItem(R.id.headlines_toggle_sidebar).setVisible(!isPortrait() && !isSmallScreen());
+
+ Fragment af = getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
+
+ m_menu.setGroupVisible(R.id.menu_group_article, af != null);
+
+ m_menu.findItem(R.id.search).setVisible(false);
+ }
+ }
+
+ @Override
+ public void onArticleSelected(int articleId) {
+ onArticleSelected(articleId, true);
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ overridePendingTransition(0, R.anim.right_slide_out);
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesEventListener.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesEventListener.java
new file mode 100644
index 00000000..0818a66b
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesEventListener.java
@@ -0,0 +1,7 @@
+package org.fox.ttrss.offline;
+
+
+public interface OfflineHeadlinesEventListener {
+ void onArticleSelected(int articleId, boolean open);
+ void onArticleSelected(int articleId);
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesFragment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesFragment.java
new file mode 100644
index 00000000..7f9d73f7
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesFragment.java
@@ -0,0 +1,774 @@
+package org.fox.ttrss.offline;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.fox.ttrss.CommonActivity;
+import org.fox.ttrss.GlobalState;
+import org.fox.ttrss.R;
+import org.fox.ttrss.util.TypefaceCache;
+import org.jsoup.Jsoup;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources.Theme;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteStatement;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SimpleCursorAdapter;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.text.Html;
+import android.text.Html.ImageGetter;
+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.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class OfflineHeadlinesFragment extends Fragment implements OnItemClickListener {
+ public static enum ArticlesSelection { ALL, NONE, UNREAD };
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ private int m_feedId;
+ private boolean m_feedIsCat = false;
+ private int m_activeArticleId;
+ private String m_searchQuery = "";
+
+ private SharedPreferences m_prefs;
+
+ private Cursor m_cursor;
+ private ArticleListAdapter m_adapter;
+
+ private OfflineHeadlinesEventListener m_listener;
+ private OfflineActivity m_activity;
+ private SwipeRefreshLayout m_swipeLayout;
+
+ public void initialize(int feedId, boolean isCat) {
+ m_feedId = feedId;
+ m_feedIsCat = isCat;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+ }
+
+ public int getSelectedArticleCount() {
+ Cursor c = m_activity.getReadableDb().query("articles",
+ new String[] { "COUNT(*)" }, "selected = 1", null, null, null, null);
+ c.moveToFirst();
+ int selected = c.getInt(0);
+ c.close();
+
+ return selected;
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo();
+
+ switch (item.getItemId()) {
+ case R.id.article_link_copy:
+ if (true) {
+ int articleId = getArticleIdAtPosition(info.position);
+
+ Cursor article = m_activity.getArticleById(articleId);
+
+ if (article != null) {
+ m_activity.copyToClipboard(article.getString(article.getColumnIndex("link")));
+ article.close();
+ }
+ }
+ return true;
+ case R.id.selection_toggle_marked:
+ if (getSelectedArticleCount() > 0) {
+ SQLiteStatement stmt = m_activity.getWritableDb()
+ .compileStatement(
+ "UPDATE articles SET modified = 1, marked = NOT marked WHERE selected = 1");
+ stmt.execute();
+ stmt.close();
+ } else {
+ int articleId = getArticleIdAtPosition(info.position);
+
+ SQLiteStatement stmt = m_activity.getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, marked = NOT marked WHERE "
+ + BaseColumns._ID + " = ?");
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+ }
+ refresh();
+ return true;
+ case R.id.selection_toggle_published:
+ if (getSelectedArticleCount() > 0) {
+ SQLiteStatement stmt = m_activity.getWritableDb()
+ .compileStatement(
+ "UPDATE articles SET modified = 1, published = NOT published WHERE selected = 1");
+ stmt.execute();
+ stmt.close();
+ } else {
+ int articleId = getArticleIdAtPosition(info.position);
+
+ SQLiteStatement stmt = m_activity.getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, published = NOT published WHERE "
+ + BaseColumns._ID + " = ?");
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+ }
+ refresh();
+ return true;
+ case R.id.selection_toggle_unread:
+ if (getSelectedArticleCount() > 0) {
+ SQLiteStatement stmt = m_activity.getWritableDb()
+ .compileStatement(
+ "UPDATE articles SET modified = 1, unread = NOT unread WHERE selected = 1");
+ stmt.execute();
+ stmt.close();
+ } else {
+ int articleId = getArticleIdAtPosition(info.position);
+
+ SQLiteStatement stmt = m_activity.getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = NOT unread WHERE "
+ + BaseColumns._ID + " = ?");
+ stmt.bindLong(1, articleId);
+ stmt.execute();
+ stmt.close();
+ }
+ refresh();
+ return true;
+ case R.id.share_article:
+ if (true) {
+ int articleId = getArticleIdAtPosition(info.position);
+ m_activity.shareArticle(articleId);
+ }
+ return true;
+ case R.id.catchup_above:
+ if (true) {
+ int articleId = getArticleIdAtPosition(info.position);
+
+ SQLiteStatement stmt = null;
+
+ String updatedOperator = (m_prefs.getBoolean("offline_oldest_first", false)) ? "<" : ">";
+
+ if (m_feedIsCat) {
+ stmt = m_activity.getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 WHERE " +
+ "updated "+updatedOperator+" (SELECT updated FROM articles WHERE " + BaseColumns._ID + " = ?) " +
+ "AND unread = 1 AND feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)");
+ } else {
+ stmt = m_activity.getWritableDb().compileStatement(
+ "UPDATE articles SET modified = 1, unread = 0 WHERE " +
+ "updated "+updatedOperator+" (SELECT updated FROM articles WHERE " + BaseColumns._ID + " = ?) " +
+ "AND unread = 1 AND feed_id = ?");
+ }
+
+ stmt.bindLong(1, articleId);
+ stmt.bindLong(2, m_feedId);
+ stmt.execute();
+ stmt.close();
+ }
+ refresh();
+ 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.headlines_context_menu, menu);
+
+ if (getSelectedArticleCount() > 0) {
+ menu.setHeaderTitle(R.string.headline_context_multiple);
+ menu.setGroupVisible(R.id.menu_group_single_article, false);
+ } else {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ Cursor c = getArticleAtPosition(info.position);
+ menu.setHeaderTitle(c.getString(c.getColumnIndex("title")));
+ //c.close();
+ menu.setGroupVisible(R.id.menu_group_single_article, true);
+
+ menu.findItem(R.id.set_labels).setVisible(false);
+ menu.findItem(R.id.article_set_note).setVisible(false);
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (GlobalState.getInstance().m_selectedArticleId != 0) {
+ m_activeArticleId = GlobalState.getInstance().m_selectedArticleId;
+ GlobalState.getInstance().m_selectedArticleId = 0;
+ }
+
+ if (m_activeArticleId != 0) {
+ setActiveArticleId(m_activeArticleId);
+ }
+
+ refresh();
+
+ m_activity.initMenu();
+ }
+
+ public void refresh() {
+ try {
+ if (!isAdded()) return;
+
+ m_swipeLayout.setRefreshing(true);
+
+ if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
+
+ m_cursor = createCursor();
+
+ if (m_cursor != null && m_adapter != null) {
+ m_adapter.changeCursor(m_cursor);
+ m_adapter.notifyDataSetChanged();
+ }
+
+ m_swipeLayout.setRefreshing(false);
+
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ if (savedInstanceState != null) {
+ m_feedId = savedInstanceState.getInt("feedId");
+ m_activeArticleId = savedInstanceState.getInt("activeArticleId");
+ //m_selectedArticles = savedInstanceState.getParcelableArrayList("selectedArticles");
+ m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery");
+ m_feedIsCat = savedInstanceState.getBoolean("feedIsCat");
+ } else {
+ m_activity.getWritableDb().execSQL("UPDATE articles SET selected = 0 ");
+ }
+
+ View view = inflater.inflate(R.layout.headlines_fragment, container, false);
+
+ m_swipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.headlines_swipe_container);
+
+ m_swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ refresh();
+ }
+ });
+
+ if (!m_activity.isCompatMode()) {
+ m_swipeLayout.setColorScheme(android.R.color.holo_green_dark,
+ android.R.color.holo_red_dark,
+ android.R.color.holo_blue_dark,
+ android.R.color.holo_orange_dark);
+ }
+
+ m_cursor = createCursor();
+
+ ListView list = (ListView)view.findViewById(R.id.headlines);
+ m_adapter = new ArticleListAdapter(getActivity(), R.layout.headlines_row, m_cursor,
+ new String[] { "title" }, new int[] { R.id.title }, 0);
+
+ /* if (!m_activity.isCompatMode()) {
+ AnimationSet set = new AnimationSet(true);
+
+ Animation animation = new AlphaAnimation(0.0f, 1.0f);
+ animation.setDuration(500);
+ set.addAnimation(animation);
+
+ animation = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, 50.0f,Animation.RELATIVE_TO_SELF, 0.0f,
+ Animation.RELATIVE_TO_SELF, 0.0f,Animation.RELATIVE_TO_SELF, 0.0f
+ );
+ animation.setDuration(1000);
+ set.addAnimation(animation);
+
+ LayoutAnimationController controller = new LayoutAnimationController(set, 0.5f);
+
+ list.setLayoutAnimation(controller);
+ } */
+
+ list.setAdapter(m_adapter);
+ list.setOnItemClickListener(this);
+ list.setEmptyView(view.findViewById(R.id.no_headlines));
+ registerForContextMenu(list);
+
+ //if (m_activity.isSmallScreen())
+ // view.findViewById(R.id.headlines_fragment).setPadding(0, 0, 0, 0);
+
+ getActivity().setProgressBarIndeterminateVisibility(false);
+
+ return view;
+ }
+
+ public Cursor createCursor() {
+ String feedClause = null;
+
+ if (m_feedIsCat) {
+ feedClause = "feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)";
+ } else {
+ feedClause = "feed_id = ?";
+ }
+
+ String viewMode = m_activity.getViewMode();
+
+ if ("adaptive".equals(viewMode)) {
+ // TODO: implement adaptive
+ } else if ("marked".equals(viewMode)) {
+ feedClause += "AND (marked = 1)";
+ } else if ("published".equals(viewMode)) {
+ feedClause += "AND (published = 1)";
+ } else if ("unread".equals(viewMode)) {
+ feedClause += "AND (unread = 1)";
+ } else { // all_articles
+ //
+ }
+
+ String orderBy = (m_prefs.getBoolean("offline_oldest_first", false)) ? "updated" : "updated DESC";
+
+ if (m_searchQuery == null || m_searchQuery.equals("")) {
+ return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
+ new String[] { "articles.*", "feeds.title AS feed_title" }, feedClause,
+ new String[] { String.valueOf(m_feedId) }, null, null, orderBy);
+ } else {
+ return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
+ new String[] { "articles.*", "feeds.title AS feed_title" },
+ feedClause + " AND (articles.title LIKE '%' || ? || '%' OR content LIKE '%' || ? || '%')",
+ new String[] { String.valueOf(m_feedId), m_searchQuery, m_searchQuery }, null, null, orderBy);
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ m_listener = (OfflineHeadlinesEventListener) activity;
+ m_activity = (OfflineActivity) activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> av, View view, int position, long id) {
+ ListView list = (ListView)av;
+
+ Log.d(TAG, "onItemClick=" + position);
+
+ if (list != null) {
+ /* Cursor cursor = (Cursor)list.getItemAtPosition(position);
+
+ int articleId = cursor.getInt(0); */
+
+ int articleId = getArticleIdAtPosition(position);
+
+ if (getActivity().findViewById(R.id.article_fragment) != null) {
+ m_activeArticleId = articleId;
+ }
+
+ m_listener.onArticleSelected(articleId);
+
+ refresh();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState (Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("feedId", m_feedId);
+ out.putInt("activeArticleId", m_activeArticleId);
+ //out.putParcelableArrayList("selectedArticles", m_selectedArticles);
+ out.putCharSequence("searchQuery", m_searchQuery);
+ out.putBoolean("feedIsCat", m_feedIsCat);
+ }
+
+ /* public void setLoadingStatus(int status, boolean showProgress) {
+ if (getView() != null) {
+ TextView tv = (TextView)getView().findViewById(R.id.loading_message);
+
+ if (tv != null) {
+ tv.setText(status);
+ }
+ }
+
+ getActivity().setProgressBarIndeterminateVisibility(showProgress);
+ } */
+
+ private class ArticleListAdapter extends SimpleCursorAdapter {
+ public static final int VIEW_NORMAL = 0;
+ public static final int VIEW_UNREAD = 1;
+ public static final int VIEW_SELECTED = 2;
+ public static final int VIEW_SELECTED_UNREAD = 3;
+ public static final int VIEW_LOADMORE = 4;
+
+ public static final int VIEW_COUNT = VIEW_LOADMORE+1;
+
+ private final Integer[] origTitleColors = new Integer[VIEW_COUNT];
+ private final int titleHighScoreUnreadColor;
+
+ public ArticleListAdapter(Context context, int layout, Cursor c,
+ String[] from, int[] to, int flags) {
+ super(context, layout, c, from, to, flags);
+
+ Theme theme = context.getTheme();
+ TypedValue tv = new TypedValue();
+ theme.resolveAttribute(R.attr.headlineTitleHighScoreUnreadTextColor, tv, true);
+ titleHighScoreUnreadColor = tv.data;
+ }
+
+ public int getViewTypeCount() {
+ return VIEW_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Cursor c = (Cursor) getItem(position);
+
+ //Log.d(TAG, "@gIVT " + position + " " + c.getInt(0) + " vs " + m_activeArticleId);
+
+ if (c.getInt(0) == m_activeArticleId && c.getInt(c.getColumnIndex("unread")) == 1) {
+ return VIEW_SELECTED_UNREAD;
+ } else if (c.getInt(0) == m_activeArticleId) {
+ return VIEW_SELECTED;
+ } else if (c.getInt(c.getColumnIndex("unread")) == 1) {
+ return VIEW_UNREAD;
+ } else {
+ return VIEW_NORMAL;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ View v = convertView;
+
+ Cursor article = (Cursor)getItem(position);
+ final int articleId = article.getInt(0);
+
+ int headlineFontSize = Integer.parseInt(m_prefs.getString("headlines_font_size_sp", "13"));
+ int headlineSmallFontSize = Math.max(10, Math.min(18, headlineFontSize - 2));
+
+ if (v == null) {
+ int layoutId = R.layout.headlines_row;
+
+ switch (getItemViewType(position)) {
+ case VIEW_LOADMORE:
+ layoutId = R.layout.headlines_row_loadmore;
+ break;
+ case VIEW_UNREAD:
+ layoutId = R.layout.headlines_row_unread;
+ break;
+ case VIEW_SELECTED_UNREAD:
+ layoutId = R.layout.headlines_row_selected_unread;
+ break;
+ case VIEW_SELECTED:
+ layoutId = R.layout.headlines_row_selected;
+ break;
+ }
+
+ LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(layoutId, null);
+
+ // http://code.google.com/p/android/issues/detail?id=3414
+ ((ViewGroup)v).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ }
+
+ TextView tt = (TextView)v.findViewById(R.id.title);
+
+ if (tt != null) {
+
+ tt.setText(Html.fromHtml(article.getString(article.getColumnIndex("title"))));
+
+ if (m_prefs.getBoolean("enable_condensed_fonts", false)) {
+ Typeface tf = TypefaceCache.get(m_activity, "sans-serif-condensed", article.getInt(article.getColumnIndex("unread")) == 1 ? Typeface.BOLD : Typeface.NORMAL);
+
+ if (tf != null && !tf.equals(tt.getTypeface())) {
+ tt.setTypeface(tf);
+ }
+
+ tt.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, headlineFontSize + 5));
+ } else {
+ tt.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, headlineFontSize + 3));
+ }
+
+ int scoreIndex = article.getColumnIndex("score");
+ if (scoreIndex >= 0)
+ adjustTitleTextView(article.getInt(scoreIndex), tt, position);
+ }
+
+ TextView ft = (TextView)v.findViewById(R.id.feed_title);
+
+ int feedTitleIndex = article.getColumnIndex("feed_title");
+
+ if (ft != null && feedTitleIndex != -1 && m_feedIsCat) {
+ String feedTitle = article.getString(feedTitleIndex);
+
+ if (feedTitle.length() > 20)
+ feedTitle = feedTitle.substring(0, 20) + "...";
+
+ if (feedTitle.length() > 0) {
+ ft.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize);
+ ft.setText(feedTitle);
+ } else {
+ ft.setVisibility(View.GONE);
+ }
+ } else if (ft != null) {
+ ft.setVisibility(View.GONE);
+ }
+
+ ImageView marked = (ImageView)v.findViewById(R.id.marked);
+
+ if (marked != null) {
+ marked.setImageResource(article.getInt(article.getColumnIndex("marked")) == 1 ? R.drawable.ic_star_full : R.drawable.ic_star_empty);
+
+ marked.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ SQLiteStatement stmtUpdate = m_activity.getWritableDb().compileStatement("UPDATE articles SET modified = 1, marked = NOT marked " +
+ "WHERE " + BaseColumns._ID + " = ?");
+
+ stmtUpdate.bindLong(1, articleId);
+ stmtUpdate.execute();
+ stmtUpdate.close();
+
+ refresh();
+ }
+ });
+ }
+
+ ImageView published = (ImageView)v.findViewById(R.id.published);
+
+ if (published != null) {
+ published.setImageResource(article.getInt(article.getColumnIndex("published")) == 1 ? R.drawable.ic_published : R.drawable.ic_unpublished);
+
+ published.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ SQLiteStatement stmtUpdate = m_activity.getWritableDb().compileStatement("UPDATE articles SET modified = 1, published = NOT published " +
+ "WHERE " + BaseColumns._ID + " = ?");
+
+ stmtUpdate.bindLong(1, articleId);
+ stmtUpdate.execute();
+ stmtUpdate.close();
+
+ refresh();
+ }
+ });
+ }
+
+ TextView te = (TextView)v.findViewById(R.id.excerpt);
+
+ if (te != null) {
+ if (!m_prefs.getBoolean("headlines_show_content", true)) {
+ te.setVisibility(View.GONE);
+ } else {
+ String excerpt = Jsoup.parse(article.getString(article.getColumnIndex("content"))).text();
+
+ if (excerpt.length() > CommonActivity.EXCERPT_MAX_SIZE)
+ excerpt = excerpt.substring(0, CommonActivity.EXCERPT_MAX_SIZE) + "...";
+
+ te.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineFontSize);
+ te.setText(excerpt);
+ }
+ }
+
+ TextView ta = (TextView)v.findViewById(R.id.author);
+
+ if (ta != null) {
+ int authorIndex = article.getColumnIndex("author");
+ if (authorIndex >= 0) {
+ String author = article.getString(authorIndex);
+
+ ta.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize);
+
+ if (author != null && author.length() > 0)
+ ta.setText(getString(R.string.author_formatted, author));
+ else
+ ta.setText("");
+ }
+ }
+
+ /* ImageView separator = (ImageView)v.findViewById(R.id.headlines_separator);
+
+ if (separator != null && m_offlineServices.isSmallScreen()) {
+ separator.setVisibility(View.GONE);
+ } */
+
+ TextView dv = (TextView) v.findViewById(R.id.date);
+
+ if (dv != null) {
+ dv.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize);
+
+ Date d = new Date((long)article.getInt(article.getColumnIndex("updated")) * 1000);
+ DateFormat df = new SimpleDateFormat("MMM dd, HH:mm");
+ df.setTimeZone(TimeZone.getDefault());
+ dv.setText(df.format(d));
+ }
+
+ CheckBox cb = (CheckBox) v.findViewById(R.id.selected);
+
+ if (cb != null) {
+ cb.setChecked(article.getInt(article.getColumnIndex("selected")) == 1);
+ cb.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View view) {
+ CheckBox cb = (CheckBox)view;
+
+ SQLiteStatement stmtUpdate = m_activity.getWritableDb().compileStatement("UPDATE articles SET selected = ? " +
+ "WHERE " + BaseColumns._ID + " = ?");
+
+ stmtUpdate.bindLong(1, cb.isChecked() ? 1 : 0);
+ stmtUpdate.bindLong(2, articleId);
+ stmtUpdate.execute();
+ stmtUpdate.close();
+
+ refresh();
+
+ m_activity.initMenu();
+
+ }
+ });
+ }
+
+ ImageView ib = (ImageView) v.findViewById(R.id.article_menu_button);
+
+ if (ib != null) {
+ //if (m_activity.isDarkTheme())
+ // ib.setImageResource(R.drawable.ic_mailbox_collapsed_holo_dark);
+
+ ib.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().openContextMenu(v);
+ }
+ });
+ }
+
+ return v;
+ }
+
+ private void adjustTitleTextView(int score, TextView tv, int position) {
+ int viewType = getItemViewType(position);
+ if (origTitleColors[viewType] == null)
+ // store original color
+ origTitleColors[viewType] = Integer.valueOf(tv.getCurrentTextColor());
+
+ if (score < -500) {
+ tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
+ } else if (score > 500) {
+ tv.setTextColor(titleHighScoreUnreadColor);
+ tv.setPaintFlags(tv.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ } else {
+ tv.setTextColor(origTitleColors[viewType].intValue());
+ tv.setPaintFlags(tv.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ }
+ }
+ }
+
+ public void notifyUpdated() {
+ m_adapter.notifyDataSetChanged();
+ }
+
+ public void setActiveArticleId(int articleId) {
+ m_activeArticleId = articleId;
+ try {
+ m_adapter.notifyDataSetChanged();
+
+ ListView list = (ListView)getView().findViewById(R.id.headlines);
+
+ Log.d(TAG, articleId + " position " + getArticleIdPosition(articleId));
+
+ if (list != null) {
+ list.setSelection(getArticleIdPosition(articleId));
+ }
+ } catch (NullPointerException e) {
+ // invoked before view is created, nvm
+ }
+ }
+
+ public Cursor getArticleAtPosition(int position) {
+ return (Cursor) m_adapter.getItem(position);
+ }
+
+ public int getArticleIdAtPosition(int position) {
+ /*Cursor c = getArticleAtPosition(position);
+
+ if (c != null) {
+ int id = c.getInt(0);
+ return id;
+ } */
+
+ return (int) m_adapter.getItemId(position);
+ }
+
+ public int getActiveArticleId() {
+ return m_activeArticleId;
+ }
+
+ public int getArticleIdPosition(int articleId) {
+ for (int i = 0; i < m_adapter.getCount(); i++) {
+ if (articleId == m_adapter.getItemId(i))
+ return i;
+ }
+
+ return -1;
+ }
+
+ public int getArticleCount() {
+ return m_adapter.getCount();
+ }
+
+ public void setSearchQuery(String query) {
+ if (!m_searchQuery.equals(query)) {
+ m_searchQuery = query;
+ }
+ }
+
+ public int getFeedId() {
+ return m_feedId;
+ }
+
+ public boolean getFeedIsCat() {
+ return m_feedIsCat;
+ }
+
+ public String getSearchQuery() {
+ return m_searchQuery;
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineUploadService.java b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineUploadService.java
new file mode 100644
index 00000000..4c3349d4
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/offline/OfflineUploadService.java
@@ -0,0 +1,286 @@
+package org.fox.ttrss.offline;
+
+import java.util.HashMap;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.OnlineActivity;
+import org.fox.ttrss.R;
+import org.fox.ttrss.util.DatabaseHelper;
+
+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.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.google.gson.JsonElement;
+
+public class OfflineUploadService extends IntentService {
+ private final String TAG = this.getClass().getSimpleName();
+
+ public static final int NOTIFY_UPLOADING = 2;
+ public static final String INTENT_ACTION_SUCCESS = "org.fox.ttrss.intent.action.UploadComplete";
+
+ private SQLiteDatabase m_writableDb;
+ private SQLiteDatabase m_readableDb;
+ private String m_sessionId;
+ private NotificationManager m_nmgr;
+ private boolean m_uploadInProgress = false;
+ private boolean m_batchMode = false;
+
+ public OfflineUploadService() {
+ super("OfflineUploadService");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ m_nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
+ initDatabase();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ m_nmgr.cancel(NOTIFY_UPLOADING);
+ }
+
+ @SuppressWarnings("deprecation")
+ private void updateNotification(String msg) {
+ Notification notification = new Notification(R.drawable.icon,
+ getString(R.string.notify_uploading_title), System.currentTimeMillis());
+
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+ new Intent(this, OnlineActivity.class), 0);
+
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
+
+ notification.setLatestEventInfo(this, getString(R.string.notify_uploading_title), msg, contentIntent);
+
+ m_nmgr.notify(NOTIFY_UPLOADING, notification);
+ }
+
+ private void updateNotification(int msgResId) {
+ updateNotification(getString(msgResId));
+ }
+
+ private void initDatabase() {
+ DatabaseHelper dh = new DatabaseHelper(getApplicationContext());
+ m_writableDb = dh.getWritableDatabase();
+ m_readableDb = dh.getReadableDatabase();
+ }
+
+ private synchronized SQLiteDatabase getReadableDb() {
+ return m_readableDb;
+ }
+
+ private synchronized SQLiteDatabase getWritableDb() {
+ return m_writableDb;
+ }
+
+ private void uploadRead() {
+ Log.d(TAG, "syncing modified offline data... (read)");
+
+ final String ids = getModifiedIds(ModifiedCriteria.READ);
+
+ if (ids.length() > 0) {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ uploadMarked();
+ } else {
+ updateNotification(getErrorMessage());
+ uploadFailed();
+ }
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", m_sessionId);
+ put("op", "updateArticle");
+ put("article_ids", ids);
+ put("mode", "0");
+ put("field", "2");
+ }
+ };
+
+ req.execute(map);
+ } else {
+ uploadMarked();
+ }
+ }
+
+ private enum ModifiedCriteria {
+ READ, MARKED, PUBLISHED
+ };
+
+ private String getModifiedIds(ModifiedCriteria criteria) {
+
+ String criteriaStr = "";
+
+ switch (criteria) {
+ case READ:
+ criteriaStr = "unread = 0";
+ break;
+ case MARKED:
+ criteriaStr = "marked = 1";
+ break;
+ case PUBLISHED:
+ criteriaStr = "published = 1";
+ break;
+ }
+
+ Cursor c = getReadableDb().query("articles", null,
+ "modified = 1 AND " + criteriaStr, null, null, null, null);
+
+ String tmp = "";
+
+ while (c.moveToNext()) {
+ tmp += c.getInt(0) + ",";
+ }
+
+ tmp = tmp.replaceAll(",$", "");
+
+ c.close();
+
+ return tmp;
+ }
+
+ private void uploadMarked() {
+ Log.d(TAG, "syncing modified offline data... (marked)");
+
+ final String ids = getModifiedIds(ModifiedCriteria.MARKED);
+
+ if (ids.length() > 0) {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ uploadPublished();
+ } else {
+ updateNotification(getErrorMessage());
+ uploadFailed();
+ }
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", m_sessionId);
+ put("op", "updateArticle");
+ put("article_ids", ids);
+ put("mode", "1");
+ put("field", "0");
+ }
+ };
+
+ req.execute(map);
+ } else {
+ uploadPublished();
+ }
+ }
+
+ private void uploadFailed() {
+ m_readableDb.close();
+ m_writableDb.close();
+
+ // TODO send notification to activity?
+
+ m_uploadInProgress = false;
+ }
+
+ private void uploadSuccess() {
+ getWritableDb().execSQL("UPDATE articles SET modified = 0");
+
+ if (m_batchMode) {
+
+ SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = localPrefs.edit();
+ editor.putBoolean("offline_mode_active", false);
+ editor.commit();
+
+ } else {
+ Intent intent = new Intent();
+ intent.setAction(INTENT_ACTION_SUCCESS);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ sendBroadcast(intent);
+ }
+
+ m_readableDb.close();
+ m_writableDb.close();
+
+ m_uploadInProgress = false;
+
+ m_nmgr.cancel(NOTIFY_UPLOADING);
+ }
+
+ private void uploadPublished() {
+ Log.d(TAG, "syncing modified offline data... (published)");
+
+ final String ids = getModifiedIds(ModifiedCriteria.MARKED);
+
+ if (ids.length() > 0) {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ uploadSuccess();
+ } else {
+ updateNotification(getErrorMessage());
+ uploadFailed();
+ }
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", m_sessionId);
+ put("op", "updateArticle");
+ put("article_ids", ids);
+ put("mode", "1");
+ put("field", "1");
+ }
+ };
+
+ req.execute(map);
+ } else {
+ uploadSuccess();
+ }
+ }
+
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ try {
+ if (getWritableDb().isDbLockedByCurrentThread() || getWritableDb().isDbLockedByOtherThreads()) {
+ return;
+ }
+
+ m_sessionId = intent.getStringExtra("sessionId");
+ m_batchMode = intent.getBooleanExtra("batchMode", false);
+
+ if (!m_uploadInProgress) {
+ m_uploadInProgress = true;
+
+ updateNotification(R.string.notify_uploading_sending_data);
+
+ uploadRead();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java
new file mode 100644
index 00000000..63458532
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java
@@ -0,0 +1,57 @@
+package org.fox.ttrss.share;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Display;
+import android.widget.Toast;
+
+public class CommonActivity extends Activity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private boolean m_smallScreenMode = true;
+ private boolean m_compatMode = false;
+
+ protected void setSmallScreen(boolean smallScreen) {
+ Log.d(TAG, "m_smallScreenMode=" + smallScreen);
+ m_smallScreenMode = smallScreen;
+ }
+
+ public void toast(int msgId) {
+ Toast toast = Toast.makeText(CommonActivity.this, msgId, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ public void toast(String msg) {
+ Toast toast = Toast.makeText(CommonActivity.this, msg, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_compatMode = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB;
+
+ Log.d(TAG, "m_compatMode=" + m_compatMode);
+
+ super.onCreate(savedInstanceState);
+ }
+
+ public boolean isSmallScreen() {
+ return m_smallScreenMode;
+ }
+
+ public boolean isCompatMode() {
+ return m_compatMode;
+ }
+
+ public boolean isPortrait() {
+ Display display = getWindowManager().getDefaultDisplay();
+
+ int width = display.getWidth();
+ int height = display.getHeight();
+
+ return width < height;
+ }
+
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonShareActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonShareActivity.java
new file mode 100644
index 00000000..165d38f7
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/share/CommonShareActivity.java
@@ -0,0 +1,136 @@
+package org.fox.ttrss.share;
+
+import java.util.HashMap;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.PreferencesActivity;
+import org.fox.ttrss.R;
+import org.fox.ttrss.util.SimpleLoginManager;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+
+public abstract class CommonShareActivity extends CommonActivity {
+ protected SharedPreferences m_prefs;
+ protected String m_sessionId;
+ protected int m_apiLevel = 0;
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ m_sessionId = savedInstanceState.getString("sessionId");
+ m_apiLevel = savedInstanceState.getInt("apiLevel");
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putString("sessionId", m_sessionId);
+ out.putInt("apiLevel", m_apiLevel);
+ }
+
+ protected abstract void onLoggedIn(int requestId);
+
+ protected abstract void onLoggingIn(int requestId);
+
+ public void login(int requestId) {
+
+ if (m_prefs.getString("ttrss_url", "").trim().length() == 0) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.dialog_need_configure_prompt)
+ .setCancelable(false)
+ .setPositiveButton(R.string.dialog_open_preferences, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // launch preferences
+
+ Intent intent = new Intent(CommonShareActivity.this,
+ PreferencesActivity.class);
+ startActivityForResult(intent, 0);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+
+ } else {
+
+ SimpleLoginManager loginManager = new SimpleLoginManager() {
+
+ @Override
+ protected void onLoginSuccess(int requestId, String sessionId, int apiLevel) {
+ m_sessionId = sessionId;
+ m_apiLevel = apiLevel;
+
+ CommonShareActivity.this.onLoggedIn(requestId);
+ }
+
+ @Override
+ protected void onLoginFailed(int requestId, ApiRequest ar) {
+ toast(ar.getErrorMessage());
+ setProgressBarIndeterminateVisibility(false);
+ }
+
+ @Override
+ protected void onLoggingIn(int requestId) {
+ CommonShareActivity.this.onLoggingIn(requestId);
+ }
+ };
+
+ String login = m_prefs.getString("login", "").trim();
+ String password = m_prefs.getString("password", "").trim();
+
+ loginManager.logIn(this, requestId, login, password);
+ }
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.preferences:
+ Intent intent = new Intent(CommonShareActivity.this,
+ PreferencesActivity.class);
+ startActivityForResult(intent, 0);
+ 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.share_menu, menu);
+ return true;
+ }
+
+
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java
new file mode 100644
index 00000000..dff48502
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java
@@ -0,0 +1,146 @@
+package org.fox.ttrss.share;
+
+import java.util.HashMap;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.EditText;
+
+import com.google.gson.JsonElement;
+
+public class ShareActivity extends CommonShareActivity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private Button m_button;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ //setTheme(R.style.DarkTheme);
+
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_LEFT_ICON);
+
+ Intent intent = getIntent();
+
+ String urlValue = intent.getStringExtra(Intent.EXTRA_TEXT);
+ String titleValue = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ String contentValue = "";
+
+ if (savedInstanceState != null) {
+ urlValue = savedInstanceState.getString("url");
+ titleValue = savedInstanceState.getString("title");
+ contentValue = savedInstanceState.getString("content");
+ }
+
+ setContentView(R.layout.share);
+
+ getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.icon);
+
+ setSmallScreen(false);
+
+ EditText url = (EditText) findViewById(R.id.url);
+ url.setText(urlValue);
+
+ EditText title = (EditText) findViewById(R.id.title);
+ title.setText(titleValue);
+
+ EditText content = (EditText) findViewById(R.id.content);
+ content.setText(contentValue);
+
+ m_button = (Button) findViewById(R.id.share_button);
+
+ m_button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ login(0);
+ }
+ });
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ EditText url = (EditText) findViewById(R.id.url);
+
+ if (url != null) {
+ out.putString("url", url.getText().toString());
+ }
+
+ EditText title = (EditText) findViewById(R.id.title);
+
+ if (title != null) {
+ out.putString("title", title.getText().toString());
+ }
+
+ EditText content = (EditText) findViewById(R.id.content);
+
+ if (content != null) {
+ out.putString("content", content.getText().toString());
+ }
+
+ }
+
+ private void postData() {
+ m_button.setEnabled(false);
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ setProgressBarIndeterminateVisibility(false);
+
+ if (m_lastError != ApiError.NO_ERROR) {
+ toast(getErrorMessage());
+ } else {
+ toast(R.string.share_article_posted);
+ finish();
+ }
+
+ m_button.setEnabled(true);
+ }
+ };
+
+ final EditText url = (EditText) findViewById(R.id.url);
+ final EditText title = (EditText) findViewById(R.id.title);
+ final EditText content = (EditText) findViewById(R.id.content);
+
+ if (url != null && title != null && content != null) {
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", m_sessionId);
+ put("op", "shareToPublished");
+ put("title", title.getText().toString());
+ put("url", url.getText().toString());
+ put("content", content.getText().toString());
+ }
+ };
+
+ setProgressBarIndeterminateVisibility(true);
+
+ req.execute(map);
+ }
+ }
+
+
+ @Override
+ public void onLoggingIn(int requestId) {
+ m_button.setEnabled(false);
+ }
+
+ @Override
+ protected void onLoggedIn(int requestId) {
+ m_button.setEnabled(true);
+
+ if (m_apiLevel < 4) {
+ toast(R.string.api_too_low);
+ } else {
+ postData();
+ }
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java
new file mode 100644
index 00000000..bd7e964f
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java
@@ -0,0 +1,321 @@
+package org.fox.ttrss.share;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.R;
+import org.fox.ttrss.types.FeedCategory;
+import org.fox.ttrss.types.FeedCategoryList;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+
+public class SubscribeActivity extends CommonShareActivity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private Button m_postButton;
+ private Button m_catButton;
+ private CatListAdapter m_adapter;
+ private FeedCategoryList m_cats = new FeedCategoryList();
+
+ private static final int REQ_CATS = 1;
+ private static final int REQ_POST = 2;
+
+ class CatTitleComparator implements Comparator<FeedCategory> {
+
+ @Override
+ public int compare(FeedCategory a, FeedCategory b) {
+ if (a.id >= 0 && b.id >= 0)
+ return a.title.compareTo(b.title);
+ else
+ return a.id - b.id;
+ }
+
+ }
+
+ public void sortCats() {
+ Comparator<FeedCategory> cmp = new CatTitleComparator();
+
+ Collections.sort(m_cats, cmp);
+ try {
+ m_adapter.notifyDataSetChanged();
+ } catch (NullPointerException e) {
+ // adapter missing
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_LEFT_ICON);
+
+ String urlValue = getIntent().getDataString();
+
+ if (urlValue == null)
+ urlValue = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+
+ if (savedInstanceState != null) {
+ urlValue = savedInstanceState.getString("url");
+
+ ArrayList<FeedCategory> list = savedInstanceState.getParcelableArrayList("cats");
+
+ for (FeedCategory c : list)
+ m_cats.add(c);
+ }
+
+ setContentView(R.layout.subscribe);
+
+ getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.icon);
+
+ setSmallScreen(false);
+
+ Spinner catList = (Spinner) findViewById(R.id.category_spinner);
+
+ if (m_cats.size() == 0) m_cats.add(new FeedCategory(0, "Uncategorized", 0));
+
+ m_adapter = new CatListAdapter(this, android.R.layout.simple_spinner_dropdown_item, m_cats);
+ catList.setAdapter(m_adapter);
+
+ EditText feedUrl = (EditText) findViewById(R.id.feed_url);
+ feedUrl.setText(urlValue);
+
+ m_postButton = (Button) findViewById(R.id.subscribe_button);
+
+ m_postButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ login(REQ_POST);
+ }
+ });
+
+ m_catButton = (Button) findViewById(R.id.cats_button);
+
+ m_catButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ login(REQ_CATS);
+ }
+ });
+
+ login(REQ_CATS);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ EditText url = (EditText) findViewById(R.id.url);
+
+ if (url != null) {
+ out.putString("url", url.getText().toString());
+ }
+
+ out.putParcelableArrayList("cats", m_cats);
+
+ }
+
+ private void subscribeToFeed() {
+ m_postButton.setEnabled(false);
+
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ setProgressBarIndeterminateVisibility(false);
+
+ if (m_lastError != ApiError.NO_ERROR) {
+ toast(getErrorMessage());
+ } else {
+ try {
+ int rc = -1;
+
+ try {
+ rc = result.getAsJsonObject().get("status").getAsJsonObject().get("code").getAsInt();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ switch (rc) {
+ case -1:
+ toast(R.string.error_api_unknown);
+ //finish();
+ break;
+ case 0:
+ toast(R.string.error_feed_already_exists_);
+ //finish();
+ break;
+ case 1:
+ toast(R.string.subscribed_to_feed);
+ finish();
+ break;
+ case 2:
+ toast(R.string.error_invalid_url);
+ break;
+ case 3:
+ toast(R.string.error_url_is_an_html_page_no_feeds_found);
+ break;
+ case 4:
+ toast(R.string.error_url_contains_multiple_feeds);
+ break;
+ case 5:
+ toast(R.string.error_could_not_download_url);
+ break;
+ }
+
+ } catch (Exception e) {
+ toast(R.string.error_while_subscribing);
+ e.printStackTrace();
+ }
+ }
+
+ m_postButton.setEnabled(true);
+ }
+ };
+
+ Spinner catSpinner = (Spinner) findViewById(R.id.category_spinner);
+
+ final FeedCategory cat = (FeedCategory) m_adapter.getCategory(catSpinner.getSelectedItemPosition());
+ final EditText feedUrl = (EditText) findViewById(R.id.feed_url);
+
+ if (feedUrl != null ) {
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", m_sessionId);
+ put("op", "subscribeToFeed");
+ put("feed_url", feedUrl.getText().toString());
+
+ if (cat != null) {
+ put("category_id", String.valueOf(cat.id));
+ }
+ }
+ };
+
+ setProgressBarIndeterminateVisibility(true);
+
+ req.execute(map);
+ }
+ }
+
+ @Override
+ public void onLoggingIn(int requestId) {
+ switch (requestId) {
+ case REQ_CATS:
+ m_catButton.setEnabled(false);
+ break;
+ case REQ_POST:
+ m_postButton.setEnabled(false);
+ break;
+ }
+ }
+
+ private void updateCats() {
+ ApiRequest req = new ApiRequest(getApplicationContext()) {
+ protected void onPostExecute(JsonElement result) {
+ setProgressBarIndeterminateVisibility(false);
+
+ if (m_lastError != ApiError.NO_ERROR) {
+ toast(getErrorMessage());
+ } else {
+ JsonArray content = result.getAsJsonArray();
+
+ if (content != null) {
+ Type listType = new TypeToken<List<FeedCategory>>() {}.getType();
+ final List<FeedCategory> cats = new Gson().fromJson(content, listType);
+
+ m_cats.clear();
+
+ for (FeedCategory c : cats) {
+ if (c.id > 0)
+ m_cats.add(c);
+ }
+
+ sortCats();
+
+ m_cats.add(0, new FeedCategory(0, "Uncategorized", 0));
+
+ m_adapter.notifyDataSetChanged();
+
+ toast(R.string.category_list_updated);
+ }
+ }
+
+ m_catButton.setEnabled(true);
+ }
+ };
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", m_sessionId);
+ put("op", "getCategories");
+ }
+ };
+
+ setProgressBarIndeterminateVisibility(true);
+
+ req.execute(map);
+ }
+
+ @Override
+ protected void onLoggedIn(int requestId) {
+ switch (requestId) {
+ case REQ_CATS:
+ updateCats();
+ break;
+ case REQ_POST:
+ m_postButton.setEnabled(true);
+ if (m_apiLevel < 5) {
+ toast(R.string.api_too_low);
+ } else {
+ subscribeToFeed();
+ }
+ break;
+ }
+ }
+
+ private class CatListAdapter extends ArrayAdapter<String> {
+ private List<FeedCategory> m_items;
+
+ public CatListAdapter(Context context, int resource,
+ List<FeedCategory> items) {
+ super(context, resource);
+
+ m_items = items;
+ }
+
+ @Override
+ public String getItem(int item) {
+ return m_items.get(item).title;
+ }
+
+ public FeedCategory getCategory(int item) {
+ try {
+ return m_items.get(item);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return m_items.size();
+ }
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerReceiver.java b/orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerReceiver.java
new file mode 100644
index 00000000..1b1351cb
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerReceiver.java
@@ -0,0 +1,93 @@
+package org.fox.ttrss.tasker;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.CommonActivity;
+import org.fox.ttrss.OnlineActivity;
+import org.fox.ttrss.offline.OfflineDownloadService;
+import org.fox.ttrss.offline.OfflineUploadService;
+import org.fox.ttrss.util.SimpleLoginManager;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.widget.Toast;
+
+public class TaskerReceiver extends BroadcastReceiver {
+ private final String TAG = this.getClass().getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Got action: " + intent.getAction());
+
+ final Context fContext = context;
+
+ if (com.twofortyfouram.locale.Intent.ACTION_FIRE_SETTING.equals(intent.getAction())) {
+
+ final Bundle settings = intent.getBundleExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE);
+ final int actionId = settings != null ? settings.getInt("actionId", -1) : -1;
+
+ Log.d(TAG, "received action id=" + actionId);
+
+ SimpleLoginManager loginMgr = new SimpleLoginManager() {
+
+ @Override
+ protected void onLoginSuccess(int requestId, String sessionId, int apiLevel) {
+
+ switch (actionId) {
+ case TaskerSettingsActivity.ACTION_DOWNLOAD:
+ if (true) {
+ Intent intent = new Intent(fContext,
+ OfflineDownloadService.class);
+ intent.putExtra("sessionId", sessionId);
+ intent.putExtra("batchMode", true);
+
+ fContext.startService(intent);
+ }
+ break;
+ case TaskerSettingsActivity.ACTION_UPLOAD:
+ if (true) {
+ Intent intent = new Intent(fContext,
+ OfflineUploadService.class);
+ intent.putExtra("sessionId", sessionId);
+ intent.putExtra("batchMode", true);
+
+ fContext.startService(intent);
+ }
+ break;
+ default:
+ Log.d(TAG, "unknown action id=" + actionId);
+ }
+ }
+
+ @Override
+ protected void onLoginFailed(int requestId, ApiRequest ar) {
+ Toast toast = Toast.makeText(fContext, fContext.getString(ar.getErrorMessage()), Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ @Override
+ protected void onLoggingIn(int requestId) {
+ //
+ }
+ };
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ String login = prefs.getString("login", "").trim();
+ String password = prefs.getString("password", "").trim();
+ String ttrssUrl = prefs.getString("ttrss_url", "").trim();
+
+ if (ttrssUrl.equals("")) {
+ Toast toast = Toast.makeText(fContext, "Could not download articles: not configured?", Toast.LENGTH_SHORT);
+ toast.show();
+ } else {
+ loginMgr.logIn(context, 1, login, password);
+ }
+ }
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerSettingsActivity.java b/orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerSettingsActivity.java
new file mode 100644
index 00000000..0770fc00
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/tasker/TaskerSettingsActivity.java
@@ -0,0 +1,96 @@
+package org.fox.ttrss.tasker;
+
+import org.fox.ttrss.R;
+import org.fox.ttrss.offline.OfflineDownloadService;
+import org.fox.ttrss.offline.OfflineUploadService;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.RadioGroup;
+import android.widget.RadioGroup.OnCheckedChangeListener;
+
+public class TaskerSettingsActivity extends Activity {
+ protected static final int ACTION_DOWNLOAD = 0;
+ protected static final int ACTION_UPLOAD = 1;
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ protected Bundle m_settings = new Bundle();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Bundle settings = getIntent().getBundleExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE);
+
+ int actionId = settings != null ? settings.getInt("actionId", -1) : -1;
+
+ setContentView(R.layout.tasker_settings);
+
+ RadioGroup radioGroup = (RadioGroup) findViewById(R.id.taskerActions);
+
+ switch (actionId) {
+ case TaskerSettingsActivity.ACTION_DOWNLOAD:
+ radioGroup.check(R.id.actionDownload);
+ break;
+ case TaskerSettingsActivity.ACTION_UPLOAD:
+ radioGroup.check(R.id.actionUpload);
+ break;
+ default:
+ Log.d(TAG, "unknown action id=" + actionId);
+ }
+
+ radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ switch (checkedId) {
+ case R.id.actionDownload:
+ m_settings.putInt("actionId", ACTION_DOWNLOAD);
+ break;
+ case R.id.actionUpload:
+ m_settings.putInt("actionId", ACTION_UPLOAD);
+ break;
+ }
+ }
+ });
+
+ Button button = (Button)findViewById(R.id.close_button);
+
+ button.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void finish() {
+ final Intent intent = new Intent();
+
+ intent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, m_settings);
+
+ String blurb = "?";
+
+ switch (m_settings.getInt("actionId")) {
+ case TaskerSettingsActivity.ACTION_DOWNLOAD:
+ blurb = getString(R.string.download_articles_and_go_offline);
+ break;
+ case TaskerSettingsActivity.ACTION_UPLOAD:
+ blurb = getString(R.string.synchronize_read_articles_and_go_online);
+ break;
+ }
+
+ intent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BLURB, blurb);
+
+ setResult(RESULT_OK, intent);
+
+ super.finish();
+
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/Article.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Article.java
new file mode 100644
index 00000000..9beea81a
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Article.java
@@ -0,0 +1,115 @@
+package org.fox.ttrss.types;
+import java.util.ArrayList;
+import java.util.List;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+// TODO: serialize Labels
+public class Article implements Parcelable {
+ public int id;
+ public boolean unread;
+ public boolean marked;
+ public boolean published;
+ public int score;
+ public int updated;
+ public boolean is_updated;
+ public String title;
+ public String link;
+ public int feed_id;
+ public List<String> tags;
+ public List<Attachment> attachments;
+ public String content;
+ public List<List<String>> labels;
+ public String feed_title;
+ public int comments_count;
+ public String comments_link;
+ public boolean always_display_attachments;
+ public String author;
+ public String note;
+
+ public Article(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public Article() {
+
+ }
+
+ public Article(int id) {
+ this.id = id;
+ this.title = "";
+ this.link = "";
+ this.tags = new ArrayList<String>();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(id);
+ out.writeInt(unread ? 1 : 0);
+ out.writeInt(marked ? 1 : 0);
+ out.writeInt(published ? 1 : 0);
+ out.writeInt(score);
+ out.writeInt(updated);
+ out.writeInt(is_updated ? 1 : 0);
+ out.writeString(title);
+ out.writeString(link);
+ out.writeInt(feed_id);
+ out.writeStringList(tags);
+ out.writeString(content);
+ out.writeList(attachments);
+ out.writeString(feed_title);
+ out.writeInt(comments_count);
+ out.writeString(comments_link);
+ out.writeInt(always_display_attachments ? 1 : 0);
+ out.writeString(author);
+ out.writeString(note);
+ }
+
+ public void readFromParcel(Parcel in) {
+ id = in.readInt();
+ unread = in.readInt() == 1;
+ marked = in.readInt() == 1;
+ published = in.readInt() == 1;
+ score = in.readInt();
+ updated = in.readInt();
+ is_updated = in.readInt() == 1;
+ title = in.readString();
+ link = in.readString();
+ feed_id = in.readInt();
+
+ if (tags == null) tags = new ArrayList<String>();
+ in.readStringList(tags);
+
+ content = in.readString();
+
+ attachments = new ArrayList<Attachment>();
+ in.readList(attachments, Attachment.class.getClassLoader());
+
+ feed_title = in.readString();
+
+ comments_count = in.readInt();
+ comments_link = in.readString();
+ always_display_attachments = in.readInt() == 1;
+ author = in.readString();
+ note = in.readString();
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public Article createFromParcel(Parcel in) {
+ return new Article(in);
+ }
+
+ public Article[] newArray(int size) {
+ return new Article[size];
+ }
+ };
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/ArticleList.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/ArticleList.java
new file mode 100644
index 00000000..c9b491ee
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/ArticleList.java
@@ -0,0 +1,59 @@
+package org.fox.ttrss.types;
+
+import java.util.ArrayList;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+@SuppressWarnings("serial")
+public class ArticleList extends ArrayList<Article> implements Parcelable {
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeList(this);
+ }
+
+ public Article findById(int id) {
+ for (Article a : this) {
+ if (a.id == id)
+ return a;
+ }
+ return null;
+ }
+
+ public void readFromParcel(Parcel in) {
+ in.readList(this, getClass().getClassLoader());
+ }
+
+ public ArticleList() { }
+
+ public ArticleList(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public boolean containsId(int id) {
+ for (Article a : this) {
+ if (a.id == id)
+ return true;
+ }
+ return false;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public ArticleList createFromParcel(Parcel in) {
+ return new ArticleList(in);
+ }
+
+ public ArticleList[] newArray(int size) {
+ return new ArticleList[size];
+ }
+ };
+} \ No newline at end of file
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/Attachment.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Attachment.java
new file mode 100644
index 00000000..9e363dba
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Attachment.java
@@ -0,0 +1,75 @@
+package org.fox.ttrss.types;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Attachment implements Parcelable {
+ public int id;
+ public String content_url;
+ public String content_type;
+ public String title;
+ public String duration;
+ public int post_id;
+
+ public Attachment(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public Attachment() {
+
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(id);
+ out.writeString(content_url);
+ out.writeString(content_type);
+ out.writeString(title);
+ out.writeString(duration);
+ out.writeInt(post_id);
+ }
+
+ public String toString() {
+ if (title != null && title.length() > 0) {
+ return title;
+ } else {
+ try {
+ URL url = new URL(content_url.trim());
+ return new File(url.getFile()).getName();
+ } catch (MalformedURLException e) {
+ return content_url;
+ }
+ }
+ }
+
+ public void readFromParcel(Parcel in) {
+ id = in.readInt();
+ content_url = in.readString();
+ content_type = in.readString();
+ title = in.readString();
+ duration = in.readString();
+ post_id = in.readInt();
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public Attachment createFromParcel(Parcel in) {
+ return new Attachment(in);
+ }
+
+ public Attachment[] newArray(int size) {
+ return new Attachment[size];
+ }
+ };
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/Feed.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Feed.java
new file mode 100644
index 00000000..6cf4a1b1
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Feed.java
@@ -0,0 +1,90 @@
+package org.fox.ttrss.types;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Feed implements Comparable<Feed>, Parcelable {
+ public String feed_url;
+ public String title;
+ public int id;
+ public int unread;
+ public boolean has_icon;
+ public int cat_id;
+ public int last_updated;
+ public int order_id;
+ public boolean is_cat;
+
+ public Feed(int id, String title, boolean is_cat) {
+ this.id = id;
+ this.title = title;
+ this.is_cat = is_cat;
+ }
+
+ public Feed(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public Feed() {
+
+ }
+
+ public boolean equals(Feed feed) {
+ if (feed == this)
+ return true;
+
+ if (feed == null)
+ return false;
+
+ return feed.id == this.id && (this.title == null || this.title.equals(feed.title)) && this.is_cat == feed.is_cat;
+ }
+
+ @Override
+ public int compareTo(Feed feed) {
+ if (feed.unread != this.unread)
+ return feed.unread - this.unread;
+ else
+ return this.title.compareTo(feed.title);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(feed_url);
+ out.writeString(title);
+ out.writeInt(id);
+ out.writeInt(unread);
+ out.writeInt(has_icon ? 1 : 0);
+ out.writeInt(cat_id);
+ out.writeInt(last_updated);
+ out.writeInt(is_cat ? 1 : 0);
+ out.writeInt(order_id);
+ }
+
+ public void readFromParcel(Parcel in) {
+ feed_url = in.readString();
+ title = in.readString();
+ id = in.readInt();
+ unread = in.readInt();
+ has_icon = in.readInt() == 1;
+ cat_id = in.readInt();
+ last_updated = in.readInt();
+ is_cat = in.readInt() == 1;
+ order_id = in.readInt();
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public Feed createFromParcel(Parcel in) {
+ return new Feed(in);
+ }
+
+ public Feed[] newArray(int size) {
+ return new Feed[size];
+ }
+ };
+} \ No newline at end of file
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategory.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategory.java
new file mode 100644
index 00000000..c8193f94
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategory.java
@@ -0,0 +1,58 @@
+package org.fox.ttrss.types;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class FeedCategory implements Parcelable {
+ public int id;
+ public String title;
+ public int unread;
+ public int order_id;
+
+ public FeedCategory(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public FeedCategory(int id, String title, int unread) {
+ this.id = id;
+ this.title = title;
+ this.unread = unread;
+ this.order_id = 0;
+ }
+
+ public FeedCategory() {
+
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(id);
+ out.writeString(title);
+ out.writeInt(unread);
+ out.writeInt(order_id);
+ }
+
+ public void readFromParcel(Parcel in) {
+ id = in.readInt();
+ title = in.readString();
+ unread = in.readInt();
+ order_id = in.readInt();
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public FeedCategory createFromParcel(Parcel in) {
+ return new FeedCategory(in);
+ }
+
+ public FeedCategory[] newArray(int size) {
+ return new FeedCategory[size];
+ }
+ };
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategoryList.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategoryList.java
new file mode 100644
index 00000000..eb5331bc
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedCategoryList.java
@@ -0,0 +1,43 @@
+package org.fox.ttrss.types;
+
+import java.util.ArrayList;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+@SuppressWarnings("serial")
+public class FeedCategoryList extends ArrayList<FeedCategory> implements Parcelable {
+
+ public FeedCategoryList() { }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeList(this);
+ }
+
+ public void readFromParcel(Parcel in) {
+ in.readList(this, getClass().getClassLoader());
+ }
+
+ public FeedCategoryList(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public FeedCategoryList createFromParcel(Parcel in) {
+ return new FeedCategoryList(in);
+ }
+
+ public FeedCategoryList[] newArray(int size) {
+ return new FeedCategoryList[size];
+ }
+ };
+ }
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedList.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedList.java
new file mode 100644
index 00000000..350f45ad
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/FeedList.java
@@ -0,0 +1,43 @@
+package org.fox.ttrss.types;
+
+import java.util.ArrayList;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+@SuppressWarnings("serial")
+public class FeedList extends ArrayList<Feed> implements Parcelable {
+
+ public FeedList() { }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeList(this);
+ }
+
+ public void readFromParcel(Parcel in) {
+ in.readList(this, getClass().getClassLoader());
+ }
+
+ public FeedList(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public FeedList createFromParcel(Parcel in) {
+ return new FeedList(in);
+ }
+
+ public FeedList[] newArray(int size) {
+ return new FeedList[size];
+ }
+ };
+ }
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/types/Label.java b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Label.java
new file mode 100644
index 00000000..0d4f3699
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/types/Label.java
@@ -0,0 +1,13 @@
+package org.fox.ttrss.types;
+
+public class Label {
+ public int id;
+ public String caption;
+ public String fg_color;
+ public String bg_color;
+ public boolean checked;
+
+ public Label() {
+
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/AppRater.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/AppRater.java
new file mode 100644
index 00000000..21dccdff
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/AppRater.java
@@ -0,0 +1,105 @@
+package org.fox.ttrss.util;
+
+// From http://androidsnippets.com/prompt-engaged-users-to-rate-your-app-in-the-android-market-appirater
+
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class AppRater {
+ private final static String APP_TITLE = "Tiny Tiny RSS";
+ private final static String APP_PNAME = "org.fox.ttrss";
+
+ private final static int DAYS_UNTIL_PROMPT = 3;
+ private final static int LAUNCHES_UNTIL_PROMPT = 7;
+
+ public static void appLaunched(Context mContext) {
+ SharedPreferences prefs = mContext.getSharedPreferences("apprater", 0);
+
+ if (prefs.getBoolean("dontshowagain", false)) { return ; }
+
+ SharedPreferences.Editor editor = prefs.edit();
+
+ // Increment launch counter
+ long launch_count = prefs.getLong("launch_count", 0) + 1;
+ editor.putLong("launch_count", launch_count);
+
+ // Get date of first launch
+ Long date_firstLaunch = prefs.getLong("date_firstlaunch", 0);
+ if (date_firstLaunch == 0) {
+ date_firstLaunch = System.currentTimeMillis();
+ editor.putLong("date_firstlaunch", date_firstLaunch);
+ }
+
+ // Wait at least n days before opening
+ if (launch_count >= LAUNCHES_UNTIL_PROMPT &&
+ !prefs.getBoolean("dontshowagain", false) &&
+ System.currentTimeMillis() >= date_firstLaunch + (DAYS_UNTIL_PROMPT * 24 * 60 * 60 * 1000)) {
+
+ showRateDialog(mContext, editor);
+ }
+
+ editor.commit();
+ }
+
+ public static void showRateDialog(final Context mContext, final SharedPreferences.Editor editor) {
+ final Dialog dialog = new Dialog(mContext);
+ dialog.setTitle("Rate " + APP_TITLE);
+
+ LinearLayout ll = new LinearLayout(mContext);
+ ll.setOrientation(LinearLayout.VERTICAL);
+
+ TextView tv = new TextView(mContext);
+ tv.setText("If you enjoy using " + APP_TITLE + ", please take a moment to rate it. Thanks for your support!");
+ tv.setWidth(240);
+ tv.setPadding(4, 0, 4, 10);
+ ll.addView(tv);
+
+ Button b1 = new Button(mContext);
+ b1.setText("Rate " + APP_TITLE);
+ b1.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ try {
+ mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + APP_PNAME)));
+ } catch (ActivityNotFoundException e) {
+ mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + APP_PNAME)));
+ }
+ dialog.dismiss();
+ }
+ });
+ ll.addView(b1);
+
+ Button b2 = new Button(mContext);
+ b2.setText("Remind me later");
+ b2.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+ ll.addView(b2);
+
+ Button b3 = new Button(mContext);
+ b3.setText("No, thanks");
+ b3.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if (editor != null) {
+ editor.putBoolean("dontshowagain", true);
+ editor.commit();
+ }
+ dialog.dismiss();
+ }
+ });
+ ll.addView(b3);
+
+ dialog.setContentView(ll);
+ dialog.show();
+ }
+} \ No newline at end of file
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/DatabaseHelper.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/DatabaseHelper.java
new file mode 100644
index 00000000..572ff62e
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/DatabaseHelper.java
@@ -0,0 +1,89 @@
+package org.fox.ttrss.util;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.BaseColumns;
+
+
+public class DatabaseHelper extends SQLiteOpenHelper {
+
+ @SuppressWarnings("unused")
+ private final String TAG = this.getClass().getSimpleName();
+ public static final String DATABASE_NAME = "OfflineStorage.db";
+ public static final int DATABASE_VERSION = 4;
+
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("DROP VIEW IF EXISTS cats_unread;");
+ db.execSQL("DROP VIEW IF EXISTS feeds_unread;");
+ db.execSQL("DROP TRIGGER IF EXISTS articles_set_modified;");
+ db.execSQL("DROP TABLE IF EXISTS categories;");
+ db.execSQL("DROP TABLE IF EXISTS feeds;");
+ db.execSQL("DROP TABLE IF EXISTS articles;");
+
+ db.execSQL("CREATE TABLE IF NOT EXISTS feeds (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ "feed_url TEXT, " +
+ "title TEXT, " +
+ "has_icon BOOLEAN, " +
+ "cat_id INTEGER" +
+ ");");
+
+ db.execSQL("CREATE TABLE IF NOT EXISTS categories (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ "title TEXT" +
+ ");");
+
+ db.execSQL("CREATE TABLE IF NOT EXISTS articles (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ "unread BOOLEAN, " +
+ "marked BOOLEAN, " +
+ "published BOOLEAN, " +
+ "score INTEGER, " +
+ "updated INTEGER, " +
+ "is_updated BOOLEAN, " +
+ "title TEXT, " +
+ "link TEXT, " +
+ "feed_id INTEGER, " +
+ "tags TEXT, " +
+ "content TEXT, " +
+ "author TEXT, " +
+ "selected BOOLEAN, " +
+ "modified BOOLEAN" +
+ ");");
+
+ db.execSQL("CREATE TRIGGER articles_set_modified UPDATE OF marked, published, unread ON articles " +
+ "BEGIN " +
+ " UPDATE articles SET modified = 1 WHERE " + BaseColumns._ID + " = " + "OLD." + BaseColumns._ID + "; " +
+ "END;");
+
+ db.execSQL("CREATE VIEW feeds_unread AS SELECT feeds."+BaseColumns._ID+" AS "+BaseColumns._ID+", " +
+ "feeds.title AS title, " +
+ "cat_id, " +
+ "SUM(articles.unread) AS unread FROM feeds " +
+ "LEFT JOIN articles ON (articles.feed_id = feeds."+BaseColumns._ID+") " +
+ "GROUP BY feeds."+BaseColumns._ID+", feeds.title;");
+
+ //sqlite> select categories._id,categories.title,sum(articles.unread) from categories left j
+ //oin feeds on (feeds.cat_id = categories._id) left join articles on (articles.feed_id = fee
+ //ds._id) group by categories._id;
+
+ db.execSQL("CREATE VIEW cats_unread AS SELECT categories."+BaseColumns._ID+" AS "+BaseColumns._ID+", " +
+ "categories.title AS title, " +
+ "SUM(articles.unread) AS unread FROM categories " +
+ "LEFT JOIN feeds ON (feeds.cat_id = categories."+BaseColumns._ID+") "+
+ "LEFT JOIN articles ON (articles.feed_id = feeds."+BaseColumns._ID+") " +
+ "GROUP BY categories."+BaseColumns._ID+", categories.title;");
+
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ onCreate(db);
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/EnlargingImageView.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/EnlargingImageView.java
new file mode 100644
index 00000000..e3f1e6f6
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/EnlargingImageView.java
@@ -0,0 +1,252 @@
+package org.fox.ttrss.util;
+
+/*
+ * Copyright (C) 2013 Tomáš Procházka
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.lang.reflect.Field;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Special version of ImageView which allow enlarge width of image if android:adjustViewBounds is true.
+ *
+ * <p>It simulate HTML behaviour &lt;img src="" widh="100" /&gt;</p>
+ * <p><a href="http://stackoverflow.com/questions/6202000/imageview-one-dimension-to-fit-free-space-and-second-evaluate-to-keep-aspect-rati">Stackoverflow question link</a></p>
+ *
+ * <p>It also allow set related view which will be used as reference for size measure.</p>
+ *
+ * @author Tomáš Procházka &lt;<a href="mailto:[email protected]">[email protected]</a>&gt;
+ * @version $Revision: 0$ ($Date: 6.6.2011 18:16:52$)
+ */
+public class EnlargingImageView extends android.widget.ImageView {
+
+ private int mDrawableWidth;
+ private int mDrawableHeight;
+ private boolean mAdjustViewBoundsL;
+ private int mMaxWidthL = Integer.MAX_VALUE;
+ private int mMaxHeightL = Integer.MAX_VALUE;
+ private View relatedView;
+
+ public EnlargingImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ // hack for acces some private field of parent :-(
+ Field f;
+ try {
+ f = android.widget.ImageView.class.getDeclaredField("mAdjustViewBounds");
+ f.setAccessible(true);
+ setAdjustViewBounds((Boolean) f.get(this));
+
+ f = android.widget.ImageView.class.getDeclaredField("mMaxWidth");
+ f.setAccessible(true);
+ setMaxWidth((Integer) f.get(this));
+
+ f = android.widget.ImageView.class.getDeclaredField("mMaxHeight");
+ f.setAccessible(true);
+ setMaxHeight((Integer) f.get(this));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public EnlargingImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public EnlargingImageView(Context context) {
+ super(context);
+ }
+
+ public void setAdjustViewBounds(boolean adjustViewBounds) {
+ super.setAdjustViewBounds(adjustViewBounds);
+ mAdjustViewBoundsL = adjustViewBounds;
+ }
+
+ public void setMaxWidth(int maxWidth) {
+ super.setMaxWidth(maxWidth);
+ mMaxWidthL = maxWidth;
+ }
+
+ public void setMaxHeight(int maxHeight) {
+ super.setMaxHeight(maxHeight);
+ mMaxHeightL = maxHeight;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (getDrawable() == null) {
+ setMeasuredDimension(0, 0);
+ return;
+ }
+
+ mDrawableWidth = getDrawable().getIntrinsicWidth();
+ mDrawableHeight = getDrawable().getIntrinsicHeight();
+
+ int w = 0;
+ int h = 0;
+
+ // Desired aspect ratio of the view's contents (not including padding)
+ float desiredAspect = 0.0f;
+
+ // We are allowed to change the view's width
+ boolean resizeWidth = false;
+
+ // We are allowed to change the view's height
+ boolean resizeHeight = false;
+
+ if (mDrawableWidth > 0) {
+ w = mDrawableWidth;
+ h = mDrawableHeight;
+ if (w <= 0) w = 1;
+ if (h <= 0) h = 1;
+
+ // We are supposed to adjust view bounds to match the aspect
+ // ratio of our drawable. See if that is possible.
+ if (mAdjustViewBoundsL) {
+
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
+ resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
+
+ desiredAspect = (float) w / (float) h;
+ }
+ }
+
+ int pleft = getPaddingLeft();
+ int pright = getPaddingRight();
+ int ptop = getPaddingTop();
+ int pbottom = getPaddingBottom();
+
+ int widthSize;
+ int heightSize;
+
+ if (resizeWidth || resizeHeight) {
+ /* If we get here, it means we want to resize to match the
+ drawables aspect ratio, and we have the freedom to change at
+ least one dimension.
+ */
+
+ // Get the max possible width given our constraints
+ widthSize = resolveAdjustedSize(w + pleft + pright,
+ mMaxWidthL, widthMeasureSpec);
+
+ // Get the max possible height given our constraints
+ heightSize = resolveAdjustedSize(h + ptop + pbottom,
+ mMaxHeightL, heightMeasureSpec);
+
+ if (desiredAspect != 0.0f) {
+ // See what our actual aspect ratio is
+ float actualAspect = (float) (widthSize - pleft - pright) /
+ (heightSize - ptop - pbottom);
+
+ if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
+
+ // Try adjusting width to be proportional to height
+ if (resizeWidth) {
+ int newWidth = (int) (desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright;
+ if (/*newWidth <= widthSize &&*/newWidth > 0) {
+ widthSize = Math.min(newWidth, mMaxWidthL);
+ heightSize = (int) ((widthSize - pleft - pright) / desiredAspect) + ptop + pbottom;
+ }
+ }
+
+ // Try adjusting height to be proportional to width
+ if (resizeHeight) {
+ int newHeight = (int) ((widthSize - pleft - pright) / desiredAspect) + ptop + pbottom;
+ if (/*newHeight <= heightSize && */newHeight > 0) {
+ heightSize = Math.min(newHeight, mMaxHeightL);
+ widthSize = (int) (desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright;
+ }
+ }
+ }
+ }
+ } else {
+ /* We are either don't want to preserve the drawables aspect ratio,
+ or we are not allowed to change view dimensions. Just measure in
+ the normal way.
+ */
+ w += pleft + pright;
+ h += ptop + pbottom;
+
+ w = Math.max(w, getSuggestedMinimumWidth());
+ h = Math.max(h, getSuggestedMinimumHeight());
+
+ widthSize = resolveSize(w, widthMeasureSpec);
+ heightSize = resolveSize(h, heightMeasureSpec);
+ }
+
+ //Log.d(Constants.LOGTAG, mDrawableWidth + ":" + mDrawableHeight + " to " + widthSize + ":" + heightSize);
+
+ setMeasuredDimension(widthSize, heightSize);
+
+ if (relatedView != null) {
+ //Log.i(Constants.LOGTAG, getTag() + " onMeasure:" + widthSize + ", " + heightSize + " update size of related view!");
+ relatedView.getLayoutParams().width = widthSize;
+ relatedView.getLayoutParams().height = heightSize;
+ }
+
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ //Log.d(Constants.LOGTAG, getTag() + " onLayout:" + left + ", " + top + ", " + right + ", " + bottom);
+ }
+
+ /**
+ * Experimental. This view will be set to the same size as this image.
+ */
+ public void setRelatedView(View view) {
+ relatedView = view;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ //Log.d(Constants.LOGTAG, getTag() + " onSizeChanged:" + w + ", " + h + ", " + oldw + ", " + oldh);
+ }
+
+ private int resolveAdjustedSize(int desiredSize, int maxSize, int measureSpec) {
+ int result = desiredSize;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ /* Parent says we can be as big as we want. Just don't be larger
+ than max size imposed on ourselves.
+ */
+ result = Math.min(desiredSize, maxSize);
+ break;
+ case MeasureSpec.AT_MOST:
+ // Parent says we can be as big as we want, up to specSize.
+ // Don't be larger than specSize, and don't be larger than
+ // the max size imposed on ourselves.
+ result = Math.min(Math.min(desiredSize, specSize), maxSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ // No choice. Do what we are told.
+ result = specSize;
+ break;
+ }
+ return result;
+ }
+} \ No newline at end of file
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/FontSizeDialogPreference.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/FontSizeDialogPreference.java
new file mode 100644
index 00000000..ec7af2e5
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/FontSizeDialogPreference.java
@@ -0,0 +1,224 @@
+package org.fox.ttrss.util;
+
+// http://www.lukehorvat.com/blog/android-seekbardialogpreference/
+
+import org.fox.ttrss.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+/**
+ * A {@link DialogPreference} that provides a user with the means to select an
+ * integer from a {@link SeekBar}, and persist it.
+ *
+ * @author lukehorvat
+ *
+ */
+public class FontSizeDialogPreference extends DialogPreference {
+ private static final int DEFAULT_MIN_PROGRESS = 9;
+ private static final int DEFAULT_MAX_PROGRESS = 24;
+ private static final String DEFAULT_PROGRESS = "0";
+
+ private int mMinProgress = DEFAULT_MIN_PROGRESS;
+ private int mMaxProgress = DEFAULT_MAX_PROGRESS;
+ private int mProgress;
+ private CharSequence mProgressTextSuffix;
+ private TextView mProgressText;
+ private SeekBar mSeekBar;
+
+ public FontSizeDialogPreference(Context context) {
+ this(context, null);
+ }
+
+ public FontSizeDialogPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setProgressTextSuffix(" " + context.getString(R.string.font_size_dialog_suffix));
+
+ // set layout
+ setDialogLayoutResource(R.layout.select_font_size_dialog);
+ setPositiveButtonText(android.R.string.ok);
+ setNegativeButtonText(android.R.string.cancel);
+ setDialogIcon(null);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restore, Object defaultValue) {
+ setProgress(restore ? Integer.valueOf(getPersistedString(DEFAULT_PROGRESS))
+ : Integer.valueOf((String)defaultValue));
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+ super.onBindDialogView(view);
+
+ mProgressText = (TextView) view.findViewById(R.id.text_progress);
+
+ mSeekBar = (SeekBar) view.findViewById(R.id.seek_bar);
+ mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress,
+ boolean fromUser) {
+ // update text that displays the current SeekBar progress value
+ // note: this does not persist the progress value. that is only
+ // ever done in setProgress()
+ String progressStr = String.valueOf(progress + mMinProgress);
+ mProgressText.setText(mProgressTextSuffix == null ? progressStr
+ : progressStr.concat(mProgressTextSuffix.toString()));
+ mProgressText.setTextSize(TypedValue.COMPLEX_UNIT_SP, progress + mMinProgress);
+ }
+ });
+
+ mSeekBar.setMax(mMaxProgress - mMinProgress);
+ mSeekBar.setProgress(mProgress - mMinProgress);
+ }
+
+ public int getMinProgress() {
+ return mMinProgress;
+ }
+
+ public void setMinProgress(int minProgress) {
+ mMinProgress = minProgress;
+ setProgress(Math.max(mProgress, mMinProgress));
+ }
+
+ public int getMaxProgress() {
+ return mMaxProgress;
+ }
+
+ public void setMaxProgress(int maxProgress) {
+ mMaxProgress = maxProgress;
+ setProgress(Math.min(mProgress, mMaxProgress));
+ }
+
+ public int getProgress() {
+ return mProgress;
+ }
+
+ public void setProgress(int progress) {
+ progress = Math.max(Math.min(progress, mMaxProgress), mMinProgress);
+
+ if (progress != mProgress) {
+ mProgress = progress;
+ persistString(String.valueOf(progress));
+ notifyChanged();
+ }
+ }
+
+ public CharSequence getProgressTextSuffix() {
+ return mProgressTextSuffix;
+ }
+
+ public void setProgressTextSuffix(CharSequence progressTextSuffix) {
+ mProgressTextSuffix = progressTextSuffix;
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ // when the user selects "OK", persist the new value
+ if (positiveResult) {
+ int seekBarProgress = mSeekBar.getProgress() + mMinProgress;
+ if (callChangeListener(seekBarProgress)) {
+ setProgress(seekBarProgress);
+ }
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ // save the instance state so that it will survive screen orientation
+ // changes and other events that may temporarily destroy it
+ final Parcelable superState = super.onSaveInstanceState();
+
+ // set the state's value with the class member that holds current
+ // setting value
+ final SavedState myState = new SavedState(superState);
+ myState.minProgress = getMinProgress();
+ myState.maxProgress = getMaxProgress();
+ myState.progress = getProgress();
+
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ // check whether we saved the state in onSaveInstanceState()
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // didn't save the state, so call superclass
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ // restore the state
+ SavedState myState = (SavedState) state;
+ setMinProgress(myState.minProgress);
+ setMaxProgress(myState.maxProgress);
+ setProgress(myState.progress);
+
+ super.onRestoreInstanceState(myState.getSuperState());
+ }
+
+ private static class SavedState extends BaseSavedState {
+ int minProgress;
+ int maxProgress;
+ int progress;
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public SavedState(Parcel source) {
+ super(source);
+
+ minProgress = source.readInt();
+ maxProgress = source.readInt();
+ progress = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+
+ dest.writeInt(minProgress);
+ dest.writeInt(maxProgress);
+ dest.writeInt(progress);
+ }
+
+ @SuppressWarnings("unused")
+ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java
new file mode 100644
index 00000000..551c0add
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java
@@ -0,0 +1,101 @@
+package org.fox.ttrss.util;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.GlobalState;
+import org.fox.ttrss.OnlineActivity;
+import org.fox.ttrss.R;
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.ArticleList;
+import org.fox.ttrss.types.Feed;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+
+public class HeadlinesRequest extends ApiRequest {
+ public static final int HEADLINES_REQUEST_SIZE = 30;
+ public static final int HEADLINES_BUFFER_MAX = 1500;
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ private int m_offset = 0;
+ private OnlineActivity m_activity;
+ private ArticleList m_articles = GlobalState.getInstance().m_loadedArticles;
+ private Feed m_feed;
+
+ public HeadlinesRequest(Context context, OnlineActivity activity, final Feed feed) {
+ super(context);
+
+ m_activity = activity;
+ m_feed = feed;
+ }
+
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ try {
+
+ // check if we are returning results for correct feed
+ if (GlobalState.getInstance().m_activeFeed != null && !m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
+ Log.d(TAG, "received results for wrong feed, bailing out.");
+ return;
+ }
+
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
+ Type listType = new TypeToken<List<Article>>() {}.getType();
+ final List<Article> articles = new Gson().fromJson(content, listType);
+
+ if (m_offset == 0) {
+ m_articles.clear();
+ } else {
+ while (m_articles.size() > HEADLINES_BUFFER_MAX) {
+ m_articles.remove(0);
+ }
+
+ if (m_articles.get(m_articles.size()-1).id == -1) {
+ m_articles.remove(m_articles.size()-1); // remove previous placeholder
+ }
+
+ }
+
+ for (Article f : articles)
+ if (!m_articles.containsId(f.id))
+ 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;
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/ImageCacheService.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/ImageCacheService.java
new file mode 100644
index 00000000..5b029fc6
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/ImageCacheService.java
@@ -0,0 +1,212 @@
+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.OnlineActivity;
+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.Context;
+import android.content.Intent;
+import android.os.Environment;
+
+public class ImageCacheService extends IntentService {
+
+ @SuppressWarnings("unused")
+ private final String TAG = this.getClass().getSimpleName();
+
+ public static final int NOTIFY_DOWNLOADING = 1;
+
+ private static final String CACHE_PATH = "/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(Context context, String url) {
+ String hashedUrl = md5(url);
+
+ File storage = context.getExternalCacheDir();
+
+ File file = new File(storage.getAbsolutePath() + CACHE_PATH + "/" + hashedUrl + ".png");
+
+ return file.exists();
+ }
+
+ public static String getCacheFileName(Context context, String url) {
+ String hashedUrl = md5(url);
+
+ File storage = context.getExternalCacheDir();
+
+ File file = new File(storage.getAbsolutePath() + CACHE_PATH + "/" + hashedUrl + ".png");
+
+ return file.getAbsolutePath();
+ }
+
+ public static void cleanupCache(Context context, boolean deleteAll) {
+ if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
+ File storage = context.getExternalCacheDir();
+ 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);
+ urlConnection.setReadTimeout(5*1000);
+ return urlConnection.getInputStream();
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void updateNotification(String msg) {
+ Notification notification = new Notification(R.drawable.icon,
+ getString(R.string.notify_downloading_title), System.currentTimeMillis());
+
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+ new Intent(this, OnlineActivity.class), 0);
+
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
+
+ notification.setLatestEventInfo(this, getString(R.string.notify_downloading_title), msg, contentIntent);
+
+ m_nmgr.notify(NOTIFY_DOWNLOADING, notification);
+ }
+
+ /* private void updateNotification(int msgResId) {
+ updateNotification(getString(msgResId));
+ } */
+
+ @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 = getExternalCacheDir();
+ 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++;
+
+ updateNotification(getString(R.string.notify_downloading_images, 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/orgfoxttrss/src/main/java/org/fox/ttrss/util/LessBrokenWebView.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/LessBrokenWebView.java
new file mode 100644
index 00000000..4a3ea826
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/LessBrokenWebView.java
@@ -0,0 +1,37 @@
+package org.fox.ttrss.util;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.webkit.WebView;
+
+public class LessBrokenWebView extends WebView {
+
+ public LessBrokenWebView(Context context) {
+ super(context);
+ // TODO Auto-generated constructor stub
+ }
+
+ public LessBrokenWebView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // TODO Auto-generated constructor stub
+ }
+
+ public LessBrokenWebView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ // TODO Auto-generated constructor stub
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ int temp_ScrollY = getScrollY();
+ scrollTo(getScrollX(), getScrollY() + 1);
+ scrollTo(getScrollX(), temp_ScrollY);
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/NoChildFocusScrollView.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/NoChildFocusScrollView.java
new file mode 100644
index 00000000..b5ec23c5
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/NoChildFocusScrollView.java
@@ -0,0 +1,34 @@
+package org.fox.ttrss.util;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.webkit.WebView;
+import android.widget.ScrollView;
+
+public class NoChildFocusScrollView extends ScrollView {
+
+ public NoChildFocusScrollView(Context context) {
+ super(context);
+ // TODO Auto-generated constructor stub
+ }
+
+
+ public NoChildFocusScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // TODO Auto-generated constructor stub
+ }
+
+ public NoChildFocusScrollView(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ // TODO Auto-generated constructor stub
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (focused instanceof WebView )
+ return;
+ super.requestChildFocus(child, focused);
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/PrefsBackupAgent.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/PrefsBackupAgent.java
new file mode 100644
index 00000000..2b33615f
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/PrefsBackupAgent.java
@@ -0,0 +1,19 @@
+package org.fox.ttrss.util;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+
+public class PrefsBackupAgent extends BackupAgentHelper {
+ // The name of the SharedPreferences file
+ static final String PREFS = "org.fox.ttrss_preferences";
+
+ // A key to uniquely identify the set of backup data
+ static final String PREFS_BACKUP_KEY = "prefs";
+
+ // Allocate a helper and add it to the backup agent
+ @Override
+ public void onCreate() {
+ SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, PREFS);
+ addHelper(PREFS_BACKUP_KEY, helper);
+ }
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/SimpleLoginManager.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/SimpleLoginManager.java
new file mode 100644
index 00000000..e11e574d
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/SimpleLoginManager.java
@@ -0,0 +1,105 @@
+package org.fox.ttrss.util;
+
+import java.util.HashMap;
+
+import org.fox.ttrss.ApiRequest;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+public abstract class SimpleLoginManager {
+ private final String TAG = this.getClass().getSimpleName();
+
+ protected class LoginRequest extends ApiRequest {
+ private int m_requestId;
+ protected String m_sessionId;
+ protected int m_apiLevel;
+ protected Context m_context;
+
+ public LoginRequest(Context context, int requestId) {
+ super(context);
+ m_context = context;
+ m_requestId = requestId;
+ }
+
+ protected void onPostExecute(JsonElement result) {
+ Log.d(TAG, "onPostExecute");
+
+ if (result != null) {
+ try {
+ JsonObject content = result.getAsJsonObject();
+ if (content != null) {
+ m_sessionId = content.get("session_id").getAsString();
+
+ Log.d(TAG, "[SLM] Authenticated!");
+
+ ApiRequest req = new ApiRequest(m_context) {
+ protected void onPostExecute(JsonElement result) {
+ m_apiLevel = 0;
+
+ if (result != null) {
+ try {
+ m_apiLevel = result.getAsJsonObject()
+ .get("level").getAsInt();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ Log.d(TAG, "[SLM] Received API level: " + m_apiLevel);
+
+ onLoginSuccess(m_requestId, m_sessionId, m_apiLevel);
+ }
+ };
+
+ @SuppressWarnings("serial")
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("sid", m_sessionId);
+ put("op", "getApiLevel");
+ }
+ };
+
+ req.execute(map);
+
+ return;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ m_sessionId = null;
+
+ onLoginFailed(m_requestId, this);
+ }
+
+ }
+
+ public void logIn(Context context, int requestId, final String login, final String password) {
+ LoginRequest ar = new LoginRequest(context, requestId);
+
+ HashMap<String, String> map = new HashMap<String, String>() {
+ {
+ put("op", "login");
+ put("user", login.trim());
+ put("password", password.trim());
+ }
+ };
+
+ onLoggingIn(requestId);
+
+ ar.execute(map);
+ }
+
+ protected abstract void onLoggingIn(int requestId);
+
+ protected abstract void onLoginSuccess(int requestId, String sessionId, int apiLevel);
+
+ protected abstract void onLoginFailed(int requestId, ApiRequest ar);
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/TitleWebView.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/TitleWebView.java
new file mode 100644
index 00000000..4d97918e
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/TitleWebView.java
@@ -0,0 +1,91 @@
+package org.fox.ttrss.util;
+
+// http://www.techques.com/question/1-9718245/Webview-in-Scrollview
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.webkit.WebView;
+
+public class TitleWebView extends WebView{
+
+ public TitleWebView(Context context, AttributeSet attrs){
+ super(context, attrs);
+ }
+
+ private int titleHeight;
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // determine height of title bar
+ View title = getChildAt(0);
+ titleHeight = title==null ? 0 : title.getMeasuredHeight();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev){
+ return true; // don't pass our touch events to children (title bar), we send these in dispatchTouchEvent
+ }
+
+ private boolean touchInTitleBar;
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent me){
+
+ boolean wasInTitle = false;
+ switch(me.getActionMasked()){
+ case MotionEvent.ACTION_DOWN:
+ touchInTitleBar = (me.getY() <= visibleTitleHeight());
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ wasInTitle = touchInTitleBar;
+ touchInTitleBar = false;
+ break;
+ }
+ if(touchInTitleBar || wasInTitle) {
+ View title = getChildAt(0);
+ if(title!=null) {
+ // this touch belongs to title bar, dispatch it here
+ me.offsetLocation(0, getScrollY());
+ return title.dispatchTouchEvent(me);
+ }
+ }
+ // this is our touch, offset and process
+ me.offsetLocation(0, -titleHeight);
+ return super.dispatchTouchEvent(me);
+ }
+
+ /**
+ * @return visible height of title (may return negative values)
+ */
+ private int visibleTitleHeight(){
+ return titleHeight-getScrollY();
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt){
+ super.onScrollChanged(l, t, oldl, oldt);
+ View title = getChildAt(0);
+ if(title!=null) // undo horizontal scroll, so that title scrolls only vertically
+ title.offsetLeftAndRight(l - title.getLeft());
+ }
+
+ @Override
+ protected void onDraw(Canvas c){
+
+ c.save();
+ int tH = visibleTitleHeight();
+ if(tH>0) {
+ // clip so that it doesn't clear background under title bar
+ int sx = getScrollX(), sy = getScrollY();
+ c.clipRect(sx, sy+tH, sx+getWidth(), sy+getHeight());
+ }
+ c.translate(0, titleHeight);
+ super.onDraw(c);
+ c.restore();
+ }
+ }
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/util/TypefaceCache.java b/orgfoxttrss/src/main/java/org/fox/ttrss/util/TypefaceCache.java
new file mode 100644
index 00000000..752304ca
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/util/TypefaceCache.java
@@ -0,0 +1,29 @@
+package org.fox.ttrss.util;
+
+import java.util.Hashtable;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.util.Log;
+
+public class TypefaceCache {
+ private static final String TAG = "TypefaceCache";
+ private static final Hashtable<String, Typeface> cache = new Hashtable<String, Typeface>();
+
+ public static Typeface get(Context c, String typefaceName, int style) {
+ synchronized (cache) {
+ String key = typefaceName + ":" + style;
+
+ if (!cache.containsKey(key)) {
+ try {
+ Typeface t = Typeface.create(typefaceName, style);
+ cache.put(key, t);
+ } catch (Exception e) {
+ Log.e(TAG, "Could not get typeface '" + typefaceName + "' because " + e.getMessage());
+ return null;
+ }
+ }
+ return cache.get(key);
+ }
+ }
+} \ No newline at end of file
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/widget/SmallWidgetProvider.java b/orgfoxttrss/src/main/java/org/fox/ttrss/widget/SmallWidgetProvider.java
new file mode 100644
index 00000000..6162abab
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/widget/SmallWidgetProvider.java
@@ -0,0 +1,65 @@
+package org.fox.ttrss.widget;
+
+import org.fox.ttrss.R;
+
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.app.Service;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+public class SmallWidgetProvider extends AppWidgetProvider {
+ private final String TAG = this.getClass().getSimpleName();
+
+ public static final String FORCE_UPDATE_ACTION = "org.fox.ttrss.WIDGET_FORCE_UPDATE";
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ //RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_small);
+
+ final int N = appWidgetIds.length;
+
+ for (int i=0; i < N; i++) {
+ int appWidgetId = appWidgetIds[i];
+
+ Intent updateIntent = new Intent(context, org.fox.ttrss.widget.WidgetUpdateService.class);
+ PendingIntent updatePendingIntent = PendingIntent.getService(context, 0, updateIntent, 0);
+
+ Intent intent = new Intent(context, org.fox.ttrss.OnlineActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_small);
+ views.setOnClickPendingIntent(R.id.widget_main, pendingIntent);
+
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+
+ try {
+ updatePendingIntent.send();
+ } catch (CanceledException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ super.onReceive(context, intent);
+
+ if (FORCE_UPDATE_ACTION.equals(intent.getAction())) {
+
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ ComponentName thisAppWidget = new ComponentName(context.getPackageName(), SmallWidgetProvider.class.getName());
+ int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget);
+
+ onUpdate(context, appWidgetManager, appWidgetIds);
+ }
+ }
+
+}
diff --git a/orgfoxttrss/src/main/java/org/fox/ttrss/widget/WidgetUpdateService.java b/orgfoxttrss/src/main/java/org/fox/ttrss/widget/WidgetUpdateService.java
new file mode 100644
index 00000000..e45bd301
--- /dev/null
+++ b/orgfoxttrss/src/main/java/org/fox/ttrss/widget/WidgetUpdateService.java
@@ -0,0 +1,141 @@
+package org.fox.ttrss.widget;
+
+import java.util.HashMap;
+
+import org.fox.ttrss.ApiRequest;
+import org.fox.ttrss.R;
+import org.fox.ttrss.util.SimpleLoginManager;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import android.app.Service;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+
+public class WidgetUpdateService extends Service {
+ private final String TAG = this.getClass().getSimpleName();
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.d(TAG, "onBind");
+
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ /* @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.d(TAG, "onStartCommand");
+
+ return super.onStartCommand(intent, flags, startId);
+ } */
+
+ public void update() {
+
+
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ final RemoteViews view = new RemoteViews(getPackageName(), R.layout.widget_small);
+
+ final ComponentName thisWidget = new ComponentName(this, SmallWidgetProvider.class);
+ final AppWidgetManager manager = AppWidgetManager.getInstance(this);
+
+ try {
+ view.setTextViewText(R.id.counter, String.valueOf(""));
+ view.setViewVisibility(R.id.progress, View.VISIBLE);
+
+ manager.updateAppWidget(thisWidget, view);
+
+ final SharedPreferences m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ if (m_prefs.getString("ttrss_url", "").trim().length() == 0) {
+
+ // Toast: need configure
+
+ } else {
+
+ SimpleLoginManager loginManager = new SimpleLoginManager() {
+
+ @Override
+ protected void onLoginSuccess(int requestId, String sessionId, int apiLevel) {
+
+ ApiRequest aru = new ApiRequest(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(JsonElement result) {
+ if (result != null) {
+ try {
+ JsonObject content = result.getAsJsonObject();
+
+ if (content != null) {
+ int unread = content.get("unread").getAsInt();
+
+ view.setViewVisibility(R.id.progress, View.GONE);
+ view.setTextViewText(R.id.counter, String.valueOf(unread));
+ manager.updateAppWidget(thisWidget, view);
+
+ return;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ view.setViewVisibility(R.id.progress, View.GONE);
+ view.setTextViewText(R.id.counter, "?");
+ manager.updateAppWidget(thisWidget, view);
+ }
+ };
+
+ final String fSessionId = sessionId;
+
+ HashMap<String, String> umap = new HashMap<String, String>() {
+ {
+ put("op", "getUnread");
+ put("sid", fSessionId);
+ }
+ };
+
+ aru.execute(umap);
+ }
+
+ @Override
+ protected void onLoginFailed(int requestId, ApiRequest ar) {
+
+ view.setViewVisibility(R.id.progress, View.GONE);
+ view.setTextViewText(R.id.counter, "?");
+ manager.updateAppWidget(thisWidget, view);
+ }
+
+ @Override
+ protected void onLoggingIn(int requestId) {
+
+
+ }
+ };
+
+ String login = m_prefs.getString("login", "").trim();
+ String password = m_prefs.getString("password", "").trim();
+
+ loginManager.logIn(getApplicationContext(), 1, login, password);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+
+ view.setViewVisibility(R.id.progress, View.GONE);
+ view.setTextViewText(R.id.counter, getString(R.string.app_name));
+ manager.updateAppWidget(thisWidget, view);
+
+ }
+ }
+}