diff options
author | Andrew Dolgov <[email protected]> | 2014-10-17 00:17:34 +0400 |
---|---|---|
committer | Andrew Dolgov <[email protected]> | 2014-10-17 00:17:34 +0400 |
commit | 8f6ca0eec07c077269d97edaf9c89f2eaf3fc115 (patch) | |
tree | b2b744b1f11435820f9be54e78b290f810ffd8ff /org.fox.ttrss/src/main/java | |
parent | 97cc96839d31b6cce59ec29a6681c6fe802552ee (diff) |
rename main module
Diffstat (limited to 'org.fox.ttrss/src/main/java')
56 files changed, 14025 insertions, 0 deletions
diff --git a/org.fox.ttrss/src/main/java/android/support/v4/app/ClassloaderWorkaroundFragmentStatePagerAdapter.java b/org.fox.ttrss/src/main/java/android/support/v4/app/ClassloaderWorkaroundFragmentStatePagerAdapter.java new file mode 100644 index 00000000..dd67d599 --- /dev/null +++ b/org.fox.ttrss/src/main/java/android/support/v4/app/ClassloaderWorkaroundFragmentStatePagerAdapter.java @@ -0,0 +1,39 @@ +package android.support.v4.app; + +// http://code.google.com/p/android/issues/detail?id=37484 +// Thanks for your amazing code quality, Google. + +import android.os.Bundle; +import android.view.ViewGroup; + +public class ClassloaderWorkaroundFragmentStatePagerAdapter extends + FragmentStatePagerAdapter { + + public ClassloaderWorkaroundFragmentStatePagerAdapter(FragmentManager fm) { + super(fm); + // TODO Auto-generated constructor stub + } + + @Override + public Fragment getItem(int arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + Fragment f = (Fragment) super.instantiateItem(container, position); + Bundle savedFragmentState = f.mSavedFragmentState; + if (savedFragmentState != null) { + savedFragmentState.setClassLoader(f.getClass().getClassLoader()); + } + return f; + } + + @Override + public int getCount() { + // TODO Auto-generated method stub + return 0; + } + +} diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiRequest.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiRequest.java new file mode 100644 index 00000000..65e97e8e --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleFragment.java new file mode 100644 index 00000000..4a568d3c --- /dev/null +++ b/org.fox.ttrss/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> </p><p> </p><p> </p><p> </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/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticlePager.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticlePager.java new file mode 100644 index 00000000..ee940e79 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java new file mode 100644 index 00000000..5a64ae57 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/DashClock.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/DashClock.java new file mode 100644 index 00000000..b3491972 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/DummyFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/DummyFragment.java new file mode 100644 index 00000000..7bf799a9 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java new file mode 100644 index 00000000..4439a943 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsActivity.java new file mode 100644 index 00000000..90e2c118 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsFragment.java new file mode 100644 index 00000000..7c974809 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/GlobalState.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/GlobalState.java new file mode 100644 index 00000000..6f81fc5d --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesActivity.java new file mode 100644 index 00000000..c4e0f0cc --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java new file mode 100644 index 00000000..5494bb2b --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java new file mode 100644 index 00000000..32670252 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/LoadingFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/LoadingFragment.java new file mode 100644 index 00000000..f0802b0a --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java new file mode 100644 index 00000000..e33a02b7 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/PreferencesActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/PreferencesActivity.java new file mode 100644 index 00000000..f42154a7 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineActivity.java new file mode 100644 index 00000000..fd05b596 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineArticleFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineArticleFragment.java new file mode 100644 index 00000000..985150a9 --- /dev/null +++ b/org.fox.ttrss/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> </p><p> </p><p> </p><p> </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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineArticlePager.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineArticlePager.java new file mode 100644 index 00000000..510ab97b --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineDownloadService.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineDownloadService.java new file mode 100644 index 00000000..2d65c890 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedCategoriesFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedCategoriesFragment.java new file mode 100644 index 00000000..8fde8176 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsActivity.java new file mode 100644 index 00000000..f7263fe0 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineFeedsFragment.java new file mode 100644 index 00000000..8c04d0cd --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesActivity.java new file mode 100644 index 00000000..de57c985 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesEventListener.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesEventListener.java new file mode 100644 index 00000000..0818a66b --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineHeadlinesFragment.java new file mode 100644 index 00000000..7f9d73f7 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineUploadService.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineUploadService.java new file mode 100644 index 00000000..4c3349d4 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java new file mode 100644 index 00000000..63458532 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonShareActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonShareActivity.java new file mode 100644 index 00000000..165d38f7 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java new file mode 100644 index 00000000..dff48502 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java new file mode 100644 index 00000000..bd7e964f --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/tasker/TaskerReceiver.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/tasker/TaskerReceiver.java new file mode 100644 index 00000000..1b1351cb --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/tasker/TaskerSettingsActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/tasker/TaskerSettingsActivity.java new file mode 100644 index 00000000..0770fc00 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Article.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Article.java new file mode 100644 index 00000000..9beea81a --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/ArticleList.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/ArticleList.java new file mode 100644 index 00000000..c9b491ee --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Attachment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Attachment.java new file mode 100644 index 00000000..9e363dba --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Feed.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Feed.java new file mode 100644 index 00000000..6cf4a1b1 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/FeedCategory.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/FeedCategory.java new file mode 100644 index 00000000..c8193f94 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/FeedCategoryList.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/FeedCategoryList.java new file mode 100644 index 00000000..eb5331bc --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/FeedList.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/FeedList.java new file mode 100644 index 00000000..350f45ad --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Label.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Label.java new file mode 100644 index 00000000..0d4f3699 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/AppRater.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/AppRater.java new file mode 100644 index 00000000..21dccdff --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/DatabaseHelper.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/DatabaseHelper.java new file mode 100644 index 00000000..572ff62e --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/EnlargingImageView.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/EnlargingImageView.java new file mode 100644 index 00000000..e3f1e6f6 --- /dev/null +++ b/org.fox.ttrss/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 <img src="" widh="100" /></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 <<a href="mailto:[email protected]">[email protected]</a>> + * @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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/FontSizeDialogPreference.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/FontSizeDialogPreference.java new file mode 100644 index 00000000..ec7af2e5 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java new file mode 100644 index 00000000..551c0add --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ImageCacheService.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ImageCacheService.java new file mode 100644 index 00000000..5b029fc6 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/LessBrokenWebView.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/LessBrokenWebView.java new file mode 100644 index 00000000..4a3ea826 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/NoChildFocusScrollView.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/NoChildFocusScrollView.java new file mode 100644 index 00000000..b5ec23c5 --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/PrefsBackupAgent.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/PrefsBackupAgent.java new file mode 100644 index 00000000..2b33615f --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/SimpleLoginManager.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/SimpleLoginManager.java new file mode 100644 index 00000000..e11e574d --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/TitleWebView.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/TitleWebView.java new file mode 100644 index 00000000..4d97918e --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/util/TypefaceCache.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/TypefaceCache.java new file mode 100644 index 00000000..752304ca --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/widget/SmallWidgetProvider.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/widget/SmallWidgetProvider.java new file mode 100644 index 00000000..6162abab --- /dev/null +++ b/org.fox.ttrss/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/org.fox.ttrss/src/main/java/org/fox/ttrss/widget/WidgetUpdateService.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/widget/WidgetUpdateService.java new file mode 100644 index 00000000..e45bd301 --- /dev/null +++ b/org.fox.ttrss/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); + + } + } +} |