/*
* Copyright (c) 2015, Nils Braden
*
* This file is part of ttrss-reader-fork. This program is free software; you
* can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation;
* either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details. You should have received a
* copy of the GNU General Public License along with this program; If
* not, see http://www.gnu.org/licenses/.
*/
package org.ttrssreader.net;
import android.content.Context;
import android.os.Build;
import android.util.Base64;
import android.util.Log;
import com.google.gson.JsonObject;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.MalformedJsonException;
import org.json.JSONException;
import org.json.JSONObject;
import org.ttrssreader.MyApplication;
import org.ttrssreader.R;
import org.ttrssreader.controllers.Controller;
import org.ttrssreader.controllers.Data;
import org.ttrssreader.model.pojos.Article;
import org.ttrssreader.model.pojos.Category;
import org.ttrssreader.model.pojos.Feed;
import org.ttrssreader.model.pojos.Label;
import org.ttrssreader.utils.StringSupport;
import org.ttrssreader.utils.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.SocketException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
public class JSONConnector {
private static final String TAG = JSONConnector.class.getSimpleName();
private static String lastError = "";
private static boolean hasLastError = false;
private static final String PARAM_OP = "op";
private static final String PARAM_USER = "user";
private static final String PARAM_PW = "password";
private static final String PARAM_CAT_ID = "cat_id";
private static final String PARAM_CATEGORY_ID = "category_id";
private static final String PARAM_FEED_ID = "feed_id";
private static final String PARAM_FEED_URL = "feed_url";
private static final String PARAM_ARTICLE_IDS = "article_ids";
private static final String PARAM_LIMIT = "limit";
private static final int PARAM_LIMIT_API_5 = 60;
private static final String PARAM_VIEWMODE = "view_mode";
private static final String PARAM_SHOW_CONTENT = "show_content";
// include_attachments available since 1.5.3 but is ignored on older versions
private static final String PARAM_INC_ATTACHMENTS = "include_attachments";
private static final String PARAM_SINCE_ID = "since_id";
private static final String PARAM_SEARCH = "search";
private static final String PARAM_SKIP = "skip";
private static final String PARAM_MODE = "mode";
// 0-starred, 1-published, 2-unread, 3-article note (since api level 1)
private static final String PARAM_FIELD = "field";
// optional data parameter when setting note field
private static final String PARAM_DATA = "data";
private static final String PARAM_IS_CAT = "is_cat";
private static final String PARAM_PREF = "pref_name";
private static final String VALUE_LOGIN = "login";
private static final String VALUE_GET_CATEGORIES = "getCategories";
private static final String VALUE_GET_FEEDS = "getFeeds";
private static final String VALUE_GET_HEADLINES = "getHeadlines";
private static final String VALUE_UPDATE_ARTICLE = "updateArticle";
private static final String VALUE_CATCHUP = "catchupFeed";
private static final String VALUE_UPDATE_FEED = "updateFeed";
private static final String VALUE_GET_PREF = "getPref";
private static final String VALUE_SET_LABELS = "setArticleLabel";
private static final String VALUE_SHARE_TO_PUBLISHED = "shareToPublished";
private static final String VALUE_FEED_SUBSCRIBE = "subscribeToFeed";
private static final String VALUE_FEED_UNSUBSCRIBE = "unsubscribeFeed";
private static final String VALUE_LABEL_ID = "label_id";
private static final String VALUE_ASSIGN = "assign";
private static final String ERROR = "error";
private static final String LOGIN_ERROR = "LOGIN_ERROR";
private static final String NOT_LOGGED_IN = "NOT_LOGGED_IN";
private static final String UNKNOWN_METHOD = "UNKNOWN_METHOD";
private static final String API_DISABLED = "API_DISABLED";
private static final String INCORRECT_USAGE = "INCORRECT_USAGE";
private static final String STATUS = "status";
private static final String API_LEVEL = "api_level";
// session id as an OUT parameter
private static final String SESSION_ID = "session_id";
private static final String ID = "id";
private static final String TITLE = "title";
private static final String UNREAD = "unread";
private static final String CAT_ID = "cat_id";
private static final String CONTENT = "content";
private static final String URL_SHARE = "url";
private static final String FEED_URL = "feed_url";
private static final String CONTENT_URL = "content_url";
private static final String VALUE = "value";
private static final int MAX_ID_LIST_LENGTH = 100;
// session id as an IN parameter
private static final String SID = "sid";
private boolean httpAuth = false;
private String base64NameAndPw = null;
private String sessionId = null;
private final Object lock = new Object();
private int apiLevel = -1;
public static final int PARAM_LIMIT_MAX_VALUE = 200;
private InputStream doRequest(Map<String, String> params) {
try {
if (sessionId != null) params.put(SID, sessionId);
JSONObject json = new JSONObject(params);
byte[] outputBytes = json.toString().getBytes("UTF-8");
logRequest(json);
URL url = Controller.getInstance().url();
HttpURLConnection con = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
con.setDoInput(true);
con.setDoOutput(true);
con.setUseCaches(false);
// Content
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("Accept", "application/json");
con.setRequestProperty("Content-Length", Integer.toString(outputBytes.length));
// Timeouts
long timeoutSocket = (Controller.getInstance().lazyServer()) ? 15 * Utils.MINUTE : 10 * Utils.SECOND;
con.setReadTimeout((int) timeoutSocket);
con.setConnectTimeout((int) (8 * Utils.SECOND));
// HTTP-Basic Authentication
if (base64NameAndPw != null)
con.setRequestProperty("Authorization", "Basic " + base64NameAndPw);
// Add POST data
con.getOutputStream().write(outputBytes);
// Try to check for HTTP Status codes
int code = con.getResponseCode();
if (code >= 400 && code < 600) {
hasLastError = true;
lastError = "Server returned status: " + code + " (Message: " + con.getResponseMessage() + ")";
return null;
}
// Everything is fine!
return con.getInputStream();
} catch (SSLPeerUnverifiedException e) {
// Probably related: http://stackoverflow.com/questions/6035171/no-peer-cert-not-sure-which-route-to-take
// Not doing anything here since this error should happen only when no certificate is received from the
// server.
Log.w(TAG, "SSLPeerUnverifiedException in doRequest(): " + formatException(e));
} catch (SSLException e) {
if ("No peer certificate".equals(e.getMessage())) {
// Handle this by ignoring it, this occurrs very often when the connection is instable.
Log.w(TAG, "SSLException in doRequest(): " + formatException(e));
} else {
hasLastError = true;
lastError = "SSLException in doRequest(): " + formatException(e);
}
} catch (InterruptedIOException e) {
Log.w(TAG, "InterruptedIOException in doRequest(): " + formatException(e));
} catch (SocketException e) {
// http://stackoverflow.com/questions/693997/how-to-set-httpresponse-timeout-for-android-in-java/1565243
// #1565243
Log.w(TAG, "SocketException in doRequest(): " + formatException(e));
} catch (Exception e) {
hasLastError = true;
lastError = "Exception in doRequest(): " + formatException(e);
}
return null;
}
public void init() {
if (Controller.getInstance().useHttpAuth()) {
this.httpAuth = true;
String creds = Controller.getInstance().httpUsername() + ":" + Controller.getInstance().httpPassword();
this.base64NameAndPw = encodeBase64ToString(creds);
} else {
this.httpAuth = false;
this.base64NameAndPw = null;
}
}
private void logRequest(final JSONObject json) throws JSONException {
// Filter password and session-id
Object paramPw = json.remove(PARAM_PW);
Object paramSID = json.remove(SID);
Log.i(TAG, json.toString());
json.put(PARAM_PW, paramPw);
json.put(SID, paramSID);
}
private String readResult(Map<String, String> params, boolean login) throws IOException {
return readResult(params, login, true);
}
private String readResult(Map<String, String> params, boolean login, boolean retry) throws IOException {
InputStream in = doRequest(params);
if (in == null) return null;
JsonReader reader = null;
String ret = "";
try {
reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
// Check if content contains array or object, array indicates login-response or error, object is content
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (!name.equals("content")) {
reader.skipValue();
continue;
}
JsonToken t = reader.peek();
if (!t.equals(JsonToken.BEGIN_OBJECT)) continue;
JsonObject object = new JsonObject();
reader.beginObject();
while (reader.hasNext()) {
object.addProperty(reader.nextName(), reader.nextString());
}
reader.endObject();
if (object.get(SESSION_ID) != null) {
ret = object.get(SESSION_ID).getAsString();
}
if (object.get(STATUS) != null) {
ret = object.get(STATUS).getAsString();
}
if (this.apiLevel == -1 && object.get(API_LEVEL) != null) {
this.apiLevel = object.get(API_LEVEL).getAsInt();
}
if (object.get(VALUE) != null) {
ret = object.get(VALUE).getAsString();
}
if (object.get(ERROR) != null) {
String message = object.get(ERROR).getAsString();
Context ctx = MyApplication.context();
switch (message) {
case API_DISABLED:
lastError = ctx.getString(R.string.Error_ApiDisabled, Controller.getInstance().username());
break;
case NOT_LOGGED_IN:
case LOGIN_ERROR:
if (!login && retry && login())
return readResult(params, false, false); // Just do the same request again
else lastError = ctx.getString(R.string.Error_LoginFailed);
break;
case INCORRECT_USAGE:
lastError = ctx.getString(R.string.Error_ApiIncorrectUsage);
break;
case UNKNOWN_METHOD:
lastError = ctx.getString(R.string.Error_ApiUnknownMethod);
break;
default:
lastError = ctx.getString(R.string.Error_ApiUnknownError);
break;
}
hasLastError = true;
Log.e(TAG, message);
return null;
}
}
} finally {
if (reader != null) reader.close();
}
if (ret.startsWith("\"")) ret = ret.substring(1, ret.length());
if (ret.endsWith("\"")) ret = ret.substring(0, ret.length() - 1);
return ret;
}
private JsonReader prepareReader(Map<String, String> params) throws IOException {
return prepareReader(params, true);
}
private JsonReader prepareReader(Map<String, String> params, boolean firstCall) throws IOException {
InputStream in = doRequest(params);
if (in == null) return null;
JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
// Check if content contains array or object, array indicates login-response or error, object is content
try {
reader.beginObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("content")) {
JsonToken t = reader.peek();
if (t.equals(JsonToken.BEGIN_ARRAY)) {
return reader;
} else if (t.equals(JsonToken.BEGIN_OBJECT)) {
JsonObject object = new JsonObject();
reader.beginObject();
String nextName = reader.nextName();
// We have a BEGIN_OBJECT here but its just the response to call "subscribeToFeed"
if ("status".equals(nextName)) return reader;
// Handle error
while (reader.hasNext()) {
if (nextName != null) {
object.addProperty(nextName, reader.nextString());
nextName = null;
} else {
object.addProperty(reader.nextName(), reader.nextString());
}
}
reader.endObject();
if (object.get(ERROR) != null) {
String message = object.get(ERROR).toString();
if (message.contains(NOT_LOGGED_IN)) {
lastError = NOT_LOGGED_IN;
if (firstCall && login() && !hasLastError)
return prepareReader(params, false); // Just do the same request again
else return null;
}
if (message.contains(API_DISABLED)) {
hasLastError = true;
lastError = MyApplication.context()
.getString(R.string.Error_ApiDisabled, Controller.getInstance().username());
return null;
}
// Any other error
hasLastError = true;
lastError = message;
}
}
} else {
reader.skipValue();
}
}
return null;
}
private boolean sessionNotAlive() {
// Make sure we are logged in
if (sessionId == null || lastError.equals(NOT_LOGGED_IN)) if (!login()) return true;
return hasLastError;
}
/**
* Does an API-Call and ignores the result.
*
* @return true if the call was successful.
*/
private boolean doRequestNoAnswer(Map<String, String> params) {
if (sessionNotAlive()) return false;
try {
String result = readResult(params, false);
// Reset error, this is only for an api-bug which returns an empty result for updateFeed
if (result == null) pullLastError();
return "OK".equals(result);
} catch (MalformedJsonException mje) {
// Reset error, this is only for an api-bug which returns an empty result for updateFeed
pullLastError();
} catch (IOException e) {
e.printStackTrace();
if (!hasLastError) {
hasLastError = true;
lastError = formatException(e);
}
}
return false;
}
/**
* At this time, we can only offer a best guess: if http authentication is on, and
* the user has no tt-rss username and password, then it's likely.
* In the future, it may be a better option to have an explicit setting for this.
* The problem with that is that many people may not know what single-user mode is,
* as it can only be set in server config files.
*
* @return true if the configured tt-rss instance is configured for single user
* (ie. no username, no password, no user management)
*/
private boolean isSingleUser() {
return httpAuth
&& StringSupport.isEmpty(Controller.getInstance().username())
&& StringSupport.isEmpty(Controller.getInstance().password());
}
/**
* Returns a base64 encoded representation of the input string and considers the current android version to access the appropriate API.
*
* @param input the string to be encoded
* @return the base64 encoded representation of the string
*/
private static String encodeBase64ToString(String input) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
try {
return Base64.encodeToString(input.getBytes("UTF-8"), Base64.NO_WRAP);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncodingException: UTF-8 not supported. Should never happen.");
}
} else {
return Base64.encodeToString(input.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
}
}
/**
* Tries to login to the ttrss-server with the base64-encoded password.
*
* @return true on success, false otherwise
*/
private boolean login() {
long time = System.currentTimeMillis();
// Just login once, check if already logged in after acquiring the lock on mSessionId
if (sessionId != null && !lastError.equals(NOT_LOGGED_IN)) return true;
synchronized (lock) {
if (sessionId != null && !lastError.equals(NOT_LOGGED_IN))
return true; // Login done while we were waiting for the lock
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_LOGIN);
if (!isSingleUser()) {
params.put(PARAM_USER, Controller.getInstance().username());
String pass = encodeBase64ToString(Controller.getInstance().password());
params.put(PARAM_PW, pass);
}
try {
sessionId = readResult(params, true, false);
if (sessionId != null) {
Log.d(TAG, "login: " + (System.currentTimeMillis() - time) + "ms");
return true;
}
} catch (IOException e) {
if (!hasLastError) {
hasLastError = true;
lastError = formatException(e);
}
}
if (!hasLastError) {
// Login didnt succeed, write message
hasLastError = true;
lastError = MyApplication.context().getString(R.string.Error_NotLoggedIn);
}
return false;
}
}
// ***************** Helper-Methods **************************************************
private Set<String> parseAttachments(JsonReader reader) throws IOException {
Set<String> ret = new HashSet<>();
reader.beginArray();
while (reader.hasNext()) {
String attId = null;
String attUrl = null;
reader.beginObject();
while (reader.hasNext()) {
try {
switch (reader.nextName()) {
case CONTENT_URL:
attUrl = reader.nextString();
// Some URLs may start with // to indicate that both, http and https can be used
if (attUrl.startsWith("//")) attUrl = "https:" + attUrl;
break;
case ID:
attId = reader.nextString();
break;
default:
reader.skipValue();
break;
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
reader.skipValue();
}
}
reader.endObject();
if (attId != null && attUrl != null) ret.add(attUrl);
}
reader.endArray();
return ret;
}
/**
* parse articles from JSON-reader
*
* @param articles container, where parsed articles will be stored
* @param reader JSON-reader, containing articles (received from server)
* @param filter filter for articles, defining which articles should be omitted while parsing (may be {@code
* null})
* @return amount of processed articles
*/
private int parseArticleArray(final Set<Article> articles, JsonReader reader, IArticleOmitter filter) {
long time = System.currentTimeMillis();
int count = 0;
try {
reader.beginArray();
while (reader.hasNext()) {
Article article = new Article();
reader.beginObject();
boolean skipObject = parseArticle(article, reader, filter);
reader.endObject();
if (!skipObject && article.id != -1 && article.title != null) articles.add(article);
count++;
}
reader.endArray();
} catch (OutOfMemoryError e) {
Controller.getInstance().lowMemory(true); // Low memory detected
} catch (Exception e) {
Log.e(TAG, "Input data could not be read: " + e.getMessage() + " (" + e.getCause() + ")", e);
}
Log.d(TAG, String.format("parseArticleArray: parsing %s articles took %s ms", count,
(System.currentTimeMillis() - time)));
return count;
}
private boolean parseArticle(final Article a, final JsonReader reader, final IArticleOmitter filter)
throws IOException {
boolean skipObject = false;
while (reader.hasNext() && reader.peek().equals(JsonToken.NAME)) {
if (skipObject) {
// field name
reader.skipValue();
// field value
reader.skipValue();
continue;
}
String name = reader.nextName();
try {
Article.ArticleField field = Article.ArticleField.valueOf(name);
switch (field) {
case id:
a.id = reader.nextInt();
break;
case guid:
a.guid = reader.nextString();
break;
case title:
a.title = reader.nextString();
break;
case unread:
a.isUnread = reader.nextBoolean();
break;
case updated:
a.updated = new Date(reader.nextLong() * 1000);
break;
case feed_id:
if (reader.peek() == JsonToken.NULL) reader.nextNull();
else a.feedId = reader.nextInt();
break;
case content:
a.content = reader.nextString()
.replaceAll("(<(?:img|video)[^>]+?src=[\"'])//([^\"']*)", "$1https://$2");
break;
case link:
a.url = reader.nextString();
// Some URLs may start with // to indicate that both, http and https can be used
if (a.url.startsWith("//")) a.url = "https:" + a.url;
break;
case comments:
a.commentUrl = reader.nextString();
// Some URLs may start with // to indicate that both, http and https can be used
if (a.commentUrl.startsWith("//")) a.commentUrl = "https:" + a.commentUrl;
break;
case attachments:
a.attachments = parseAttachments(reader);
break;
case marked:
a.isStarred = reader.nextBoolean();
break;
case published:
a.isPublished = reader.nextBoolean();
break;
case labels:
a.labels = parseLabels(reader);
break;
case author:
a.author = reader.nextString();
break;
case note:
if (reader.peek() == JsonToken.NULL) reader.nextNull();
else a.note = reader.nextString();
break;
default:
reader.skipValue();
continue;
}
if (filter != null) skipObject = filter.omitArticle(field, a);
} catch (IllegalArgumentException | IOException e) {
Log.w(TAG, "Result contained illegal value for entry \"" + name + "\".");
reader.skipValue();
}
}
return skipObject;
}
private Set<Label> parseLabels(final JsonReader reader) throws IOException {
Set<Label> ret = new HashSet<>();
if (reader.peek().equals(JsonToken.BEGIN_ARRAY)) {
reader.beginArray();
} else {
reader.skipValue();
return ret;
}
try {
while (reader.hasNext()) {
Label label = new Label();
reader.beginArray();
try {
label.id = Integer.parseInt(reader.nextString());
label.caption = reader.nextString();
label.foregroundColor = reader.nextString();
label.backgroundColor = reader.nextString();
label.checked = true;
} catch (IllegalArgumentException e) {
e.printStackTrace();
reader.skipValue();
continue;
}
ret.add(label);
reader.endArray();
}
reader.endArray();
} catch (Exception e) {
// Ignore exceptions here
try {
if (reader.peek().equals(JsonToken.END_ARRAY)) reader.endArray();
} catch (Exception ee) {
// Empty!
}
}
return ret;
}
// ***************** Retrieve-Data-Methods **************************************************
/**
* Retrieves all categories.
*
* @return a list of categories.
*/
public Set<Category> getCategories() {
long time = System.currentTimeMillis();
Set<Category> ret = new LinkedHashSet<>();
if (sessionNotAlive()) return ret;
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_GET_CATEGORIES);
JsonReader reader = null;
try {
reader = prepareReader(params);
if (reader == null) return ret;
reader.beginArray();
while (reader.hasNext()) {
int id = -1;
String title = null;
int unread = 0;
reader.beginObject();
while (reader.hasNext()) {
try {
switch (reader.nextName()) {
case ID:
id = reader.nextInt();
break;
case TITLE:
title = reader.nextString();
break;
case UNREAD:
unread = reader.nextInt();
break;
default:
reader.skipValue();
break;
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
reader.skipValue();
}
}
reader.endObject();
// Don't handle categories with an id below 1, we already have them in the DB from
// Data.updateVirtualCategories()
if (id > 0 && title != null) ret.add(new Category(id, title, unread));
}
reader.endArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) try {
reader.close();
} catch (IOException e1) {
// Empty!
}
}
Log.d(TAG, "getCategories: " + (System.currentTimeMillis() - time) + "ms");
return ret;
}
/**
* get current feeds from server
*
* @param tolerateWrongUnreadInformation if set to {@code false}, then
* lazy server will be updated before
* @return set of actual feeds on server
*/
private Set<Feed> getFeeds(boolean tolerateWrongUnreadInformation) {
long time = System.currentTimeMillis();
Set<Feed> ret = new LinkedHashSet<>();
if (sessionNotAlive()) return ret;
if (!tolerateWrongUnreadInformation) {
makeLazyServerWork();
}
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_GET_FEEDS);
params.put(PARAM_CAT_ID, Data.VCAT_ALL + ""); // Hardcoded -4 fetches all feeds. See
// http://tt-rss.org/redmine/wiki/tt-rss/JsonApiReference#getFeeds
JsonReader reader = null;
try {
reader = prepareReader(params);
if (reader == null) return ret;
reader.beginArray();
while (reader.hasNext()) {
int categoryId = -1;
int id = 0;
String title = null;
String feedUrl = null;
int unread = 0;
reader.beginObject();
while (reader.hasNext()) {
try {
switch (reader.nextName()) {
case ID:
id = reader.nextInt();
break;
case CAT_ID:
categoryId = reader.nextInt();
break;
case TITLE:
title = reader.nextString();
break;
case FEED_URL:
feedUrl = reader.nextString();
break;
case UNREAD:
unread = reader.nextInt();
break;
default:
reader.skipValue();
break;
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
reader.skipValue();
}
}
reader.endObject();
if (id != -1 || categoryId == -2) { // normal feed (>0) or label (-2)
if (title != null) {
Feed f = new Feed();
f.id = id;
f.categoryId = categoryId;
f.title = title;
f.url = feedUrl;
f.unread = unread;
ret.add(f);
}
}
}
reader.endArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) try {
reader.close();
} catch (IOException e1) {
// Empty!
}
}
Log.d(TAG, "getFeeds: " + (System.currentTimeMillis() - time) + "ms");
return ret;
}
/**
* Retrieves all feeds from server.
*
* @return a set of all feeds on server.
*/
public Set<Feed> getFeeds() {
return getFeeds(false);
}
private boolean makeLazyServerWork(Integer feedId) {
if (Controller.getInstance().lazyServer()) {
Map<String, String> taskParams = new HashMap<>();
taskParams.put(PARAM_OP, VALUE_UPDATE_FEED);
taskParams.put(PARAM_FEED_ID, String.valueOf(feedId));
return doRequestNoAnswer(taskParams);
}
return true;
}
private long noTaskUntil = 0;
final static private long minTaskIntervall = 10 * Utils.MINUTE;
private void makeLazyServerWork() {
final long time = System.currentTimeMillis();
if (Controller.getInstance().lazyServer() && (noTaskUntil < time)) {
noTaskUntil = time + minTaskIntervall;
for (Feed feed : getFeeds(true)) {
makeLazyServerWork(feed.id);
}
}
}
/**
* Retrieves the specified articles.
*
* @param articles container for retrieved articles
* @param id the id of the feed/category
* @param limit the maximum number of articles to be fetched
* @param viewMode indicates wether only unread articles should be included (Possible values: all_articles,
* unread,
* adaptive, marked, updated)
* @param isCategory indicates if we are dealing with a category or a feed
* @param sinceId the first ArticleId which is to be retrieved.
* @param search search query
* @param filter filter for articles, defining which articles should be omitted while parsing (may be
* {@code
* null})
*/
public void getHeadlines(final Set<Article> articles, Integer id, int limit, String viewMode, boolean isCategory,
Integer sinceId, String search, IArticleOmitter filter) {
long time = System.currentTimeMillis();
int offset = 0;
int count;
int maxSize = articles.size() + limit;
if (sessionNotAlive()) return;
int limitParam = Math.min((apiLevel < 6) ? PARAM_LIMIT_API_5 : PARAM_LIMIT_MAX_VALUE, limit);
makeLazyServerWork(id);
while (articles.size() < maxSize) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_GET_HEADLINES);
params.put(PARAM_FEED_ID, id + "");
params.put(PARAM_LIMIT, limitParam + "");
params.put(PARAM_SKIP, offset + "");
params.put(PARAM_VIEWMODE, viewMode);
params.put(PARAM_IS_CAT, (isCategory ? "1" : "0"));
params.put(PARAM_SHOW_CONTENT, "1");
params.put(PARAM_INC_ATTACHMENTS, "1");
if (sinceId > 0) params.put(PARAM_SINCE_ID, sinceId + "");
if (search != null) params.put(PARAM_SEARCH, search);
JsonReader reader = null;
try {
reader = prepareReader(params);
if (hasLastError) return;
if (reader == null) continue;
count = parseArticleArray(articles, reader, filter);
if (count < limitParam) break;
else offset += count;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {
// Empty!
}
}
}
}
Log.d(TAG, "getHeadlines: " + (System.currentTimeMillis() - time) + "ms");
}
/**
* Marks the given list of article-Ids as read/unread depending on int articleState.
*
* @param articlesIds a list of article-ids.
* @param articleState the new state of the article (0 -> mark as read; 1 -> mark as unread).
*/
public boolean setArticleRead(Set<Integer> articlesIds, int articleState) {
boolean ret = true;
if (articlesIds.isEmpty()) return true;
for (String idList : StringSupport.convertListToString(articlesIds, MAX_ID_LIST_LENGTH)) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_UPDATE_ARTICLE);
params.put(PARAM_ARTICLE_IDS, idList);
params.put(PARAM_MODE, articleState + "");
params.put(PARAM_FIELD, "2");
ret = ret && doRequestNoAnswer(params);
}
return ret;
}
/**
* Marks the given Article as "starred"/"not starred" depending on int articleState.
*
* @param ids a list of article-ids.
* @param articleState the new state of the article (0 -> not starred; 1 -> starred; 2 -> toggle).
* @return true if the operation succeeded.
*/
public boolean setArticleStarred(Set<Integer> ids, int articleState) {
boolean ret = true;
if (ids.size() == 0) return true;
for (String idList : StringSupport.convertListToString(ids, MAX_ID_LIST_LENGTH)) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_UPDATE_ARTICLE);
params.put(PARAM_ARTICLE_IDS, idList);
params.put(PARAM_MODE, articleState + "");
params.put(PARAM_FIELD, "0");
ret = ret && doRequestNoAnswer(params);
}
return ret;
}
/**
* Marks a feed or a category with all its feeds as read.
*
* @param id the feed-id/category-id.
* @param isCategory indicates whether id refers to a feed or a category.
* @return true if the operation succeeded.
*/
public boolean setRead(int id, boolean isCategory) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_CATCHUP);
params.put(PARAM_FEED_ID, id + "");
params.put(PARAM_IS_CAT, (isCategory ? "1" : "0"));
return doRequestNoAnswer(params);
}
/**
* Marks the given Articles as "published"/"not published" depending on articleState.
*
* @param ids a list of article-ids.
* @param articleState the new state of the articles (0 -> not published; 1 -> published; 2 -> toggle).
* @return true if the operation succeeded.
*/
public boolean setArticlePublished(Set<Integer> ids, int articleState) {
if (ids.size() == 0) return true;
boolean ret = true;
for (String idList : StringSupport.convertListToString(ids, MAX_ID_LIST_LENGTH)) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_UPDATE_ARTICLE);
params.put(PARAM_ARTICLE_IDS, idList);
params.put(PARAM_MODE, articleState + "");
params.put(PARAM_FIELD, "1");
ret = ret && doRequestNoAnswer(params);
}
return ret;
}
/**
* Adds a note to the given articles
*
* @param ids a list of article-ids with corresponding notes (may be null).
* @return true if the operation succeeded.
*/
public boolean setArticleNote(Map<Integer, String> ids) {
if (ids.size() == 0) return true;
boolean ret = true;
for (Integer id : ids.keySet()) {
String note = ids.get(id);
if (note == null) continue;
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_UPDATE_ARTICLE);
params.put(PARAM_ARTICLE_IDS, id + "");
params.put(PARAM_FIELD, "3"); // Field 3 is the "Add note" field
params.put(PARAM_DATA, note);
ret = ret && doRequestNoAnswer(params);
}
return ret;
}
public boolean feedUnsubscribe(int feed_id) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_FEED_UNSUBSCRIBE);
params.put(PARAM_FEED_ID, feed_id + "");
return doRequestNoAnswer(params);
}
/**
* Returns the value for the given preference-name as a string.
*
* @param pref the preferences name
* @return the value of the preference or null if it ist not set or unknown
*/
public String getPref(String pref) {
if (sessionNotAlive()) return null;
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_GET_PREF);
params.put(PARAM_PREF, pref);
try {
return readResult(params, false);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public boolean setArticleLabel(Set<Integer> articleIds, int labelId, boolean assign) {
boolean ret = true;
if (articleIds.size() == 0) return true;
for (String idList : StringSupport.convertListToString(articleIds, MAX_ID_LIST_LENGTH)) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_SET_LABELS);
params.put(PARAM_ARTICLE_IDS, idList);
params.put(VALUE_LABEL_ID, labelId + "");
params.put(VALUE_ASSIGN, (assign ? "1" : "0"));
ret = ret && doRequestNoAnswer(params);
}
return ret;
}
public boolean shareToPublished(String title, String url, String content) {
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_SHARE_TO_PUBLISHED);
params.put(TITLE, title);
params.put(URL_SHARE, url);
params.put(CONTENT, content);
return doRequestNoAnswer(params);
}
public class SubscriptionResponse {
public int code = -1;
public String message = null;
}
public SubscriptionResponse feedSubscribe(String feed_url, int category_id) {
SubscriptionResponse ret = new SubscriptionResponse();
if (sessionNotAlive()) return ret;
Map<String, String> params = new HashMap<>();
params.put(PARAM_OP, VALUE_FEED_SUBSCRIBE);
params.put(PARAM_FEED_URL, feed_url);
params.put(PARAM_CATEGORY_ID, category_id + "");
String code = "";
String message = null;
JsonReader reader = null;
try {
reader = prepareReader(params);
if (reader == null) return ret;
reader.beginObject();
while (reader.hasNext()) {
switch (reader.nextName()) {
case "code":
code = reader.nextString();
break;
case "message":
message = reader.nextString();
break;
default:
reader.skipValue();
break;
}
}
if (!code.contains(UNKNOWN_METHOD)) {
ret.code = Integer.parseInt(code);
ret.message = message;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) try {
reader.close();
} catch (IOException e1) {
// Empty!
}
}
return ret;
}
/**
* Returns true if there was an error.
*
* @return true if there was an error.
*/
public boolean hasLastError() {
return hasLastError;
}
/**
* Returns the last error-message and resets the error-state of the connector.
*
* @return a string with the last error-message.
*/
public String pullLastError() {
@SuppressWarnings("RedundantStringConstructorCall") String ret = new String(lastError);
lastError = "";
hasLastError = false;
return ret;
}
/**
* Formats an exception message and cause, both only if available in the given exception
*
* @param e the exception
* @return a string representing message and cause of the exception
*/
private static String formatException(Exception e) {
if (e == null) return "";
String msg = e.getMessage() != null ? "Exception-Message: " + e.getMessage() + " " : "No Exception-Message available. ";
String cause = e.getCause() != null ? "Exception-Cause: " + e.getCause() : "No Exception-Cause available.";
return msg + cause;
}
}