"));
+ context.startActivity(Intent.createChooser(intent, context.getString(R.string.TxtSendTo)));
+ }
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ public static void copyToClipboard(final Context context, final Item item) {
+ final String text = Html.fromHtml(item.getTitle()) + ", " + item.getHref() + " ("
+ + context.getString(R.string.TxtViaEasyRSS) + ")";
+ final ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setText(text);
+ Toast.makeText(context, R.string.MsgCopiedToClipboard, Toast.LENGTH_LONG).show();
+ }
+
+ public static void sendTo(final Context context, final Item item) {
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_SUBJECT, Html.fromHtml(item.getTitle()).toString());
+ intent.putExtra(
+ Intent.EXTRA_TEXT,
+ Html.fromHtml(item.getTitle()) + ", " + item.getHref() + " ("
+ + context.getString(R.string.TxtViaEasyRSS) + ")");
+ context.startActivity(Intent.createChooser(intent, context.getString(R.string.TxtSendTo)));
+ }
+
+ public static void streamTransfer(final InputStream in, final OutputStream out) {
+ final byte[] buffer = new byte[8192];
+ int read;
+ try {
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ public static void writeItemToFile(final Item item) throws IOException {
+ final File fdir = new File(item.getStoragePath());
+ fdir.mkdirs();
+ final String content = item.getContent();
+ final HtmlCleaner cleaner = new HtmlCleaner();
+ final TagNode node = cleaner.clean((content == null) ? "" : content);
+ final List imgList = new ArrayList();
+ final Queue nodes = new LinkedList();
+ nodes.add(node);
+ while (!nodes.isEmpty()) {
+ final TagNode tag = nodes.poll();
+ final String tagName = tag.getName().toLowerCase();
+ if ("frame".equals(tagName) || "iframe".equals(tagName) || "script".equals(tagName)) {
+ tag.removeFromTree();
+ } else if ("img".equals(tagName)) {
+ tag.removeAttribute("style");
+ final String src = tag.getAttributeByName("src");
+ if (src != null && (src.startsWith("http://") || src.startsWith("https://"))) {
+ tag.removeAttribute("width");
+ tag.removeAttribute("height");
+ imgList.add(tag);
+ } else {
+ tag.removeFromTree();
+ }
+ } else if (tag.hasChildren()) {
+ nodes.addAll(tag.getChildTagList());
+ }
+ }
+ final CleanerProperties prop = cleaner.getProperties();
+ prop.setOmitXmlDeclaration(false);
+ final FastHtmlSerializer serializer = new FastHtmlSerializer(prop);
+ {
+ final OutputStream out = new FileOutputStream(new File(item.getOriginalContentStoragePath()));
+ serializer.writeToStream(node, out);
+ out.close();
+ }
+ for (final TagNode tag : imgList) {
+ tag.removeFromTree();
+ }
+ {
+ final OutputStream out = new FileOutputStream(new File(item.getStrippedContentStoragePath()));
+ serializer.writeToStream(node, out);
+ out.close();
+ }
+ }
+
+ public static void writeToFile(final String str, final File file) {
+ try {
+ final BufferedWriter output = new BufferedWriter(new FileWriter(file), 8192);
+ output.write(str);
+ output.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ private DataUtils() {
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/Entity.java b/src/com/pursuer/reader/easyrss/data/Entity.java
new file mode 100644
index 0000000..2c96009
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/Entity.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import android.content.ContentValues;
+
+public interface Entity {
+ void clear();
+
+ ContentValues toContentValues();
+
+ ContentValues toUpdateContentValues();
+}
diff --git a/src/com/pursuer/reader/easyrss/data/GoogleAnalyticsMgr.java b/src/com/pursuer/reader/easyrss/data/GoogleAnalyticsMgr.java
new file mode 100644
index 0000000..a6e1dc6
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/GoogleAnalyticsMgr.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import com.google.android.apps.analytics.GoogleAnalyticsTracker;
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.readersetting.SettingBrowserChoice;
+import com.pursuer.reader.easyrss.data.readersetting.SettingDescendingItemsOrdering;
+import com.pursuer.reader.easyrss.data.readersetting.SettingFontSize;
+import com.pursuer.reader.easyrss.data.readersetting.SettingHttpsConnection;
+import com.pursuer.reader.easyrss.data.readersetting.SettingImageFetching;
+import com.pursuer.reader.easyrss.data.readersetting.SettingImagePrefetching;
+import com.pursuer.reader.easyrss.data.readersetting.SettingImmediateStateSyncing;
+import com.pursuer.reader.easyrss.data.readersetting.SettingMarkAllAsReadConfirmation;
+import com.pursuer.reader.easyrss.data.readersetting.SettingMaxItems;
+import com.pursuer.reader.easyrss.data.readersetting.SettingNotificationOn;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncMethod;
+import com.pursuer.reader.easyrss.data.readersetting.SettingTheme;
+import com.pursuer.reader.easyrss.data.readersetting.SettingVolumeKeySwitching;
+
+import android.content.Context;
+
+public class GoogleAnalyticsMgr {
+ final static private String UUID = "UA-25717510-1";
+ final static public int SCOPE_VISITOR_LEVEL = 1;
+ final static public int SCOPE_SESSION_LEVEL = 2;
+ final static public int SCOPE_PAGE_LEVEL = 3;
+
+ final static public String ACTION_SYNCING_TAGS = "syncingTags";
+ final static public String ACTION_SYNCING_SUBSCRIPTIONS = "syncingSubscriptions";
+ final static public String ACTION_SYNCING_UNREADCOUNTS = "syncingUnreadCounts";
+ final static public String CATEGORY_SYNCING = "syncing";
+ final static public String CUSTOM_VAR_VERSION = "version";
+ final static public String CUSTOM_VAR_READING_SETTING = "readingSettings";
+ final static public String CUSTOM_VAR_STORAGE_SETTING = "storageSettings";
+ final static public String CUSTOM_VAR_SYNCING_SETTING = "syncingSettings";
+
+ private static GoogleAnalyticsMgr instance = null;
+
+ public static GoogleAnalyticsMgr getInstance() {
+ return instance;
+ }
+
+ public static synchronized void init(final Context context) {
+ if (instance == null) {
+ instance = new GoogleAnalyticsMgr(context);
+ }
+ }
+
+ final private GoogleAnalyticsTracker tracker;
+ final private Context context;
+
+ private GoogleAnalyticsMgr(final Context context) {
+ this.tracker = GoogleAnalyticsTracker.getInstance();
+ this.context = context;
+ }
+
+ public void dispatch() {
+ tracker.dispatch();
+ }
+
+ public void setCustomVar(final int index, final String name, final String value, final int scope) {
+ tracker.setCustomVar(index, name, value, scope);
+ }
+
+ public void startTracking() {
+ tracker.startNewSession(UUID, context);
+ tracker.setCustomVar(1, CUSTOM_VAR_VERSION, context.getString(R.string.Version));
+ final DataMgr dataMgr = DataMgr.getInstance();
+ {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append(new SettingSyncMethod(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingImageFetching(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingImagePrefetching(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingImmediateStateSyncing(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingHttpsConnection(dataMgr).getData());
+ tracker.setCustomVar(2, CUSTOM_VAR_SYNCING_SETTING, buffer.toString());
+ }
+ {
+ tracker.setCustomVar(3, CUSTOM_VAR_STORAGE_SETTING, String.valueOf(new SettingMaxItems(dataMgr)));
+ }
+ {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append(new SettingFontSize(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingTheme(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingDescendingItemsOrdering(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingNotificationOn(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingMarkAllAsReadConfirmation(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingBrowserChoice(dataMgr).getData());
+ buffer.append('_');
+ buffer.append(new SettingVolumeKeySwitching(dataMgr).getData());
+ tracker.setCustomVar(4, CUSTOM_VAR_READING_SETTING, buffer.toString());
+ }
+ }
+
+ public void trackEvent(final String category, final String action, final String label, final int value) {
+ tracker.trackEvent(category, action, label, value);
+ }
+
+ public void trackPageView(final String page) {
+ tracker.trackPageView(page);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/Item.java b/src/com/pursuer/reader/easyrss/data/Item.java
new file mode 100644
index 0000000..0e9743c
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/Item.java
@@ -0,0 +1,251 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import java.io.File;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class Item implements Entity {
+ public static final String TABLE_NAME = "items";
+ public static final Uri CONTENT_URI = Uri.parse(DataProvider.ITEM_CONTENT_URI);
+
+ public static final String _AUTHOR = "author";
+ public static final String _UID = "uid";
+ public static final String _HREF = "href";
+ public static final String _SOURCEURI = "sourceUri";
+ public static final String _SOURCETITLE = "sourceTitle";
+ public static final String _TITLE = "title";
+ public static final String _TIMESTAMP = "timestamp";
+ public static final String _UPDATETIME = "updateTime";
+
+ public static final String[] OWN_COLUMNS = { _UID, _AUTHOR, _HREF, _SOURCEURI, _SOURCETITLE, _TITLE, _TIMESTAMP,
+ _UPDATETIME };
+ public static final String[] OWN_COLUMNS_TYPE = { "TEXT PRIMARY KEY", "TEXT", "TEXT", "TEXT", "TEXT", "TEXT",
+ "INTEGER", "INTEGER" };
+ public static final String[] COLUMNS = Utils.arrayMerge(OWN_COLUMNS, ItemState.OWN_COLUMNS);
+ public static final String[] COLUMNS_TYPE = Utils.arrayMerge(OWN_COLUMNS_TYPE, ItemState.OWN_COLUMN_TYPE);
+
+ public static final String[][] INDEX_COLUMNS = { { _UID }, { _UPDATETIME }, { _SOURCEURI },
+ { _SOURCEURI, ItemState._ISREAD }, { _SOURCEURI, ItemState._ISSTARRED }, { _TIMESTAMP },
+ { _TIMESTAMP, ItemState._ISREAD }, { _TIMESTAMP, ItemState._ISSTARRED } };
+
+ public static final String UID_PREFIX = "tag:google.com,2005:reader/item/";
+
+ public static Item fromCursor(final Cursor cur) {
+ final ItemState state = ItemState.fromCursor(cur);
+ return new Item(Utils.getStringFromCursor(cur, Item._AUTHOR), Utils.getStringFromCursor(cur, Item._UID),
+ Utils.getStringFromCursor(cur, Item._HREF), Utils.getStringFromCursor(cur, Item._SOURCEURI),
+ Utils.getStringFromCursor(cur, Item._SOURCETITLE), Utils.getStringFromCursor(cur, Item._TITLE),
+ Utils.getLongFromCursor(cur, Item._UPDATETIME), Utils.getLongFromCursor(cur, Item._TIMESTAMP), state);
+ }
+
+ public static String getFullUid(final String uid) {
+ return UID_PREFIX + uid;
+ }
+
+ public static String getStoragePathByUid(final String uid) {
+ return DataUtils.getAppFolderPath() + File.separator + uid;
+ }
+
+ private String author;
+ private String uid;
+ private String content;
+ private String href;
+ private String sourceUri;
+ private String sourceTitle;
+ private String title;
+ private List tags;
+ private long updateTime;
+ private long timestamp;
+ private ItemState state;
+
+ public Item() {
+ init(null, null, null, null, null, null, null, null, null, null, null);
+ }
+
+ public Item(final String author, final String uid, final String href, final String sourceUri,
+ final String sourceTitle, final String title, final List tags, final long updateTime,
+ final long timestamp, final ItemState state) {
+ init(author, uid, null, href, sourceUri, sourceTitle, title, tags, updateTime, timestamp, state);
+ }
+
+ public Item(final String author, final String uid, final String href, final String sourceUri,
+ final String sourceTitle, final String title, final long updateTime, final long timestamp,
+ final ItemState state) {
+ init(author, uid, null, href, sourceUri, sourceTitle, title, null, updateTime, timestamp, state);
+ }
+
+ public void addTag(final String tag) {
+ tags.add(tag);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null, null, null, null, null, null, null, null, null, null);
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public String getFullContentStoragePath() {
+ return getStoragePath() + File.separator + uid + ".full";
+ }
+
+ public String getFullUid() {
+ return getFullUid(uid);
+ }
+
+ public String getHref() {
+ return href;
+ }
+
+ public String getImageStoragePath(final int picId) {
+ return getStoragePath() + File.separator + picId + ".erss";
+ }
+
+ public String getOriginalContentStoragePath() {
+ return getStoragePath() + File.separator + uid + ".original";
+ }
+
+ public String getSourceTitle() {
+ return sourceTitle;
+ }
+
+ public String getSourceUri() {
+ return sourceUri;
+ }
+
+ public ItemState getState() {
+ return state;
+ }
+
+ public String getStoragePath() {
+ return getStoragePathByUid(uid);
+ }
+
+ public String getStrippedContentStoragePath() {
+ return getStoragePath() + File.separator + uid + ".stripped";
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ public long getUpdateTime() {
+ return updateTime;
+ }
+
+ private void init(final String author, final String uid, final String content, final String href,
+ final String sourceUri, final String sourceTitle, final String title, final List tags,
+ final Long updateTime, final Long timestamp, final ItemState state) {
+ this.author = (author == null) ? "" : author;
+ this.uid = uid;
+ this.content = (content == null) ? "" : content;
+ this.href = (href == null) ? "" : href;
+ this.sourceUri = (sourceUri == null) ? "" : sourceUri;
+ this.sourceTitle = (sourceTitle == null) ? "" : sourceTitle;
+ this.title = (title == null) ? "" : title;
+ this.tags = (tags == null) ? new LinkedList() : tags;
+ this.updateTime = (updateTime == null) ? System.currentTimeMillis() : updateTime;
+ this.timestamp = (timestamp == null) ? 0 : timestamp;
+ this.state = (state == null) ? new ItemState() : state;
+ }
+
+ public void setAuthor(final String author) {
+ this.author = author;
+ }
+
+ public void setContent(final String content) {
+ this.content = content;
+ }
+
+ public void setHref(final String href) {
+ this.href = href;
+ }
+
+ public void setSourceTitle(final String sourceTitle) {
+ this.sourceTitle = sourceTitle;
+ }
+
+ public void setSourceUri(final String sourceUri) {
+ this.sourceUri = sourceUri;
+ }
+
+ public void setState(final ItemState state) {
+ this.state = state;
+ }
+
+ public void setTags(final List tags) {
+ this.tags = tags;
+ }
+
+ public void setTimestamp(final long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ }
+
+ public void setUpdateTime(final long updateTime) {
+ this.updateTime = updateTime;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ final ContentValues ret = state.toContentValues();
+ ret.put(_AUTHOR, author);
+ ret.put(_UID, uid);
+ ret.put(_HREF, href);
+ ret.put(_SOURCEURI, sourceUri);
+ ret.put(_SOURCETITLE, sourceTitle);
+ ret.put(_TITLE, title);
+ ret.put(_UPDATETIME, updateTime);
+ ret.put(_TIMESTAMP, timestamp);
+ return ret;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ final ContentValues ret = state.toUpdateContentValues();
+ ret.put(_UPDATETIME, updateTime);
+ return ret;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/ItemId.java b/src/com/pursuer/reader/easyrss/data/ItemId.java
new file mode 100644
index 0000000..6ecbff7
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/ItemId.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import android.content.ContentValues;
+
+public class ItemId implements Entity {
+ private String uid;
+ private long timestamp;
+
+ public ItemId() {
+ init(null, null);
+ }
+
+ public ItemId(final String uid, final long timestamp) {
+ init(uid, timestamp);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null);
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ private void init(final String uid, final Long timestamp) {
+ this.uid = uid;
+ this.setTimestamp((timestamp == null) ? 0 : timestamp);
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ return null;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ return null;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/ItemState.java b/src/com/pursuer/reader/easyrss/data/ItemState.java
new file mode 100644
index 0000000..37c9bc0
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/ItemState.java
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class ItemState implements Entity {
+ public static final String _ISCACHED = "isCached";
+ public static final String _ISREAD = "isRead";
+ public static final String _ISSTARRED = "isStarred";
+
+ public static final String[] OWN_COLUMNS = { _ISCACHED, _ISREAD, _ISSTARRED };
+ public static final String[] OWN_COLUMN_TYPE = { "INT NOT NULL DEFAULT 0", "INT NOT NULL DEFAULT 0",
+ "INT NOT NULL DEFAULT 0" };
+
+ public static ItemState fromCursor(final Cursor cur) {
+ return new ItemState(Utils.toBoolean(Utils.getIntFromCursor(cur, _ISCACHED)), Utils.toBoolean(Utils
+ .getIntFromCursor(cur, _ISREAD)), Utils.toBoolean(Utils.getIntFromCursor(cur, _ISSTARRED)));
+ }
+
+ private boolean isCached;
+ private boolean isRead;
+ private boolean isStarred;
+
+ public ItemState() {
+ init(null, null, null);
+ }
+
+ public ItemState(final boolean isCached, final boolean isRead, final boolean isStarred) {
+ init(isCached, isRead, isStarred);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null, null);
+ }
+
+ private void init(final Boolean isCached, final Boolean isRead, final Boolean isStarred) {
+ this.isCached = (isCached == null) ? false : isCached;
+ this.isRead = (isRead == null) ? false : isRead;
+ this.isStarred = (isStarred == null) ? false : isStarred;
+ }
+
+ public boolean isCached() {
+ return isCached;
+ }
+
+ public boolean isRead() {
+ return isRead;
+ }
+
+ public boolean isStarred() {
+ return isStarred;
+ }
+
+ public void setCached(final boolean isCached) {
+ this.isCached = isCached;
+ }
+
+ public void setRead(final boolean isRead) {
+ this.isRead = isRead;
+ }
+
+ public void setStarred(final boolean isStarred) {
+ this.isStarred = isStarred;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(_ISREAD, isRead);
+ values.put(_ISSTARRED, isStarred);
+ return values;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(_ISREAD, isRead);
+ values.put(_ISSTARRED, isStarred);
+ return values;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/ItemTag.java b/src/com/pursuer/reader/easyrss/data/ItemTag.java
new file mode 100644
index 0000000..d1be034
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/ItemTag.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class ItemTag implements Entity {
+ public static final String TABLE_NAME = "itemTags";
+
+ public static final Uri CONTENT_URI = Uri.parse(DataProvider.ITEMTAG_CONTENT_URI);
+
+ public static final String _ITEMUID = "itemUid";
+ public static final String _TAGUID = "tagUid";
+ public static final String[] COLUMNS = { _ITEMUID, _TAGUID };
+
+ public static final String SQL_CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + _ITEMUID
+ + " TEXT, " + _TAGUID + " TEXT, PRIMARY KEY (" + _ITEMUID + "," + _TAGUID + "))";
+
+ public static final String[][] INDEX_COLUMNS = { { _ITEMUID }, { _TAGUID } };
+
+ public static ItemTag fromCursor(final Cursor cur) {
+ return new ItemTag(Utils.getStringFromCursor(cur, ItemTag._ITEMUID), Utils.getStringFromCursor(cur,
+ ItemTag._TAGUID));
+ }
+
+ private String itemUid;
+ private String tagUid;
+
+ public ItemTag() {
+ init(null, null);
+ }
+
+ public ItemTag(final String itemUid, final String tagUid) {
+ init(itemUid, tagUid);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null);
+ }
+
+ public String getItemUid() {
+ return itemUid;
+ }
+
+ public String getTagUid() {
+ return tagUid;
+ }
+
+ private void init(final String itemUid, final String tagUid) {
+ this.itemUid = itemUid;
+ this.tagUid = tagUid;
+ }
+
+ public void setItemUid(final String itemUid) {
+ this.itemUid = itemUid;
+ }
+
+ public void setTagUid(final String tagUid) {
+ this.tagUid = tagUid;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ final ContentValues ret = new ContentValues(2);
+ ret.put(_ITEMUID, itemUid);
+ ret.put(_TAGUID, tagUid);
+ return ret;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ return new ContentValues();
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/OnItemUpdatedListener.java b/src/com/pursuer/reader/easyrss/data/OnItemUpdatedListener.java
new file mode 100644
index 0000000..9ce54ae
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/OnItemUpdatedListener.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+public interface OnItemUpdatedListener {
+ void onItemUpdated(Item item);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/OnSettingUpdatedListener.java b/src/com/pursuer/reader/easyrss/data/OnSettingUpdatedListener.java
new file mode 100644
index 0000000..1883b2b
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/OnSettingUpdatedListener.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+public interface OnSettingUpdatedListener {
+ void onSettingUpdated(String name);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/OnSubscriptionUpdatedListener.java b/src/com/pursuer/reader/easyrss/data/OnSubscriptionUpdatedListener.java
new file mode 100644
index 0000000..951c13f
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/OnSubscriptionUpdatedListener.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+public interface OnSubscriptionUpdatedListener {
+ void onSubscriptionUpdated(Subscription sub);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/OnTagUpdatedListener.java b/src/com/pursuer/reader/easyrss/data/OnTagUpdatedListener.java
new file mode 100644
index 0000000..42a966e
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/OnTagUpdatedListener.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+public interface OnTagUpdatedListener {
+ void onTagUpdated(Tag tag);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/SQLConstants.java b/src/com/pursuer/reader/easyrss/data/SQLConstants.java
new file mode 100644
index 0000000..fc1eee1
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/SQLConstants.java
@@ -0,0 +1,272 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+final public class SQLConstants {
+ final public static String CREATE_TRIGGER_DELETE_REDUNDENT_TRANSACTION;
+ final public static String CREATE_TRIGGER_MARK_ITEM_AS_READ;
+ final public static String DROP_TRIGGER_MARK_ITEM_AS_READ;
+ final public static String INCREASE_TAG_UNREAD_COUNT;
+ final public static String INSERT_ITEM_TAG;
+ final public static String INSERT_OR_REPLACE_SETTING;
+ final public static String INSERT_SUBSCRIPTION_TAG;
+ final public static String MARK_ITEM_AS_READ;
+ final public static String SELECT_ITEM_TAGS;
+ final public static String SELECT_ITEM_TAGS_UID;
+ final public static String UPGRADE_ITEM_TAGS_ITEM_UID;
+ final public static String UPGRADE_ITEMS_UID;
+
+ // CREATE TRIGGER IF NOT EXISTS delete_redundent_transactions INSERT ON
+ // transactions BEGIN
+ // DELETE FROM transactions WHERE uid=new.uid AND type=new.type;
+ // END;
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("CREATE TRIGGER IF NOT EXISTS delete_redundent_transactions INSERT ON ");
+ buffer.append(Transaction.TABLE_NAME);
+ buffer.append(" BEGIN DELETE FROM ");
+ buffer.append(Transaction.TABLE_NAME);
+ buffer.append(" WHERE ");
+ buffer.append(Transaction._UID);
+ buffer.append("=new.");
+ buffer.append(Transaction._UID);
+ buffer.append(" AND ");
+ buffer.append(Transaction._TYPE);
+ buffer.append("=new.");
+ buffer.append(Transaction._TYPE);
+ buffer.append(";END;");
+ CREATE_TRIGGER_DELETE_REDUNDENT_TRANSACTION = buffer.toString();
+ }
+
+ // CREATE TRIGGER IF NOT EXISTS mark_item_as_read UPDATE OF isRead ON items
+ // FOR EACH ROW WHEN new.isRead=1 AND old.isRead=0 BEGIN
+ // UPDATE tags SET unreadCount=unreadCount-1 WHERE uid IN (SELECT tagUid
+ // FROM itemTags WHERE itemUid=new.uid) AND unreadCount<1000 AND
+ // unreadCount>0;
+ // UPDATE subscriptions SET unreadCount=unreadCount-1 WHERE
+ // uid=new.sourceUri AND unreadCount<1000 AND unreadCount>0;
+ // UPDATE settings SET value=value-1 WHERE name='globalItemUnreadCount' AND
+ // CAST(value AS INT)>0 AND CAST(value AS INT)<1000;
+ // INSERT INTO transactions (uid,type) VALUES (new.uid,0);
+ // END;
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ // Declare the trigger
+ buffer.append("CREATE TRIGGER IF NOT EXISTS mark_item_as_read UPDATE OF ");
+ buffer.append(ItemState._ISREAD);
+ buffer.append(" ON ");
+ buffer.append(Item.TABLE_NAME);
+ buffer.append(" FOR EACH ROW WHEN new.");
+ buffer.append(ItemState._ISREAD);
+ buffer.append("=1 AND old.");
+ buffer.append(ItemState._ISREAD);
+ buffer.append("=0 BEGIN\n");
+ // Update unreadCount of tags
+ buffer.append("UPDATE ");
+ buffer.append(Tag.TABLE_NAME);
+ buffer.append(" SET ");
+ buffer.append(Tag._UNREADCOUNT);
+ buffer.append("=");
+ buffer.append(Tag._UNREADCOUNT);
+ buffer.append("-1 WHERE ");
+ buffer.append(Tag._UID);
+ buffer.append(" IN (SELECT ");
+ buffer.append(ItemTag._TAGUID);
+ buffer.append(" FROM ");
+ buffer.append(ItemTag.TABLE_NAME);
+ buffer.append(" WHERE ");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append("=new.");
+ buffer.append(Item._UID);
+ buffer.append(") AND ");
+ buffer.append(Tag._UNREADCOUNT);
+ buffer.append("<1000 AND ");
+ buffer.append(Tag._UNREADCOUNT);
+ buffer.append(">0;\n");
+ // Update unreadCount of subscription
+ buffer.append("UPDATE ");
+ buffer.append(Subscription.TABLE_NAME);
+ buffer.append(" SET ");
+ buffer.append(Subscription._UNREADCOUNT);
+ buffer.append("=");
+ buffer.append(Subscription._UNREADCOUNT);
+ buffer.append("-1 WHERE ");
+ buffer.append(Subscription._UID);
+ buffer.append("=new.");
+ buffer.append(Item._SOURCEURI);
+ buffer.append(" AND ");
+ buffer.append(Subscription._UNREADCOUNT);
+ buffer.append("<1000 AND ");
+ buffer.append(Subscription._UNREADCOUNT);
+ buffer.append(">0;\n");
+ // Update Setting globalItemUnreadCount
+ buffer.append("UPDATE ");
+ buffer.append(Setting.TABLE_NAME);
+ buffer.append(" SET ");
+ buffer.append(Setting._VALUE);
+ buffer.append("=");
+ buffer.append(Setting._VALUE);
+ buffer.append("-1 WHERE ");
+ buffer.append(Setting._NAME);
+ buffer.append("='");
+ buffer.append(Setting.SETTING_GLOBAL_ITEM_UNREAD_COUNT);
+ buffer.append("' AND CAST(");
+ buffer.append(Setting._VALUE);
+ buffer.append(" AS INT)>0 AND CAST(");
+ buffer.append(Setting._VALUE);
+ buffer.append(" AS INT)<1000;\n");
+ // End
+ buffer.append("END;");
+ CREATE_TRIGGER_MARK_ITEM_AS_READ = buffer.toString();
+ }
+
+ static {
+ DROP_TRIGGER_MARK_ITEM_AS_READ = "DROP TRIGGER IF EXISTS mark_item_as_read";
+ }
+
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("UPDATE ");
+ buffer.append(Tag.TABLE_NAME);
+ buffer.append(" SET ");
+ buffer.append(Tag._UNREADCOUNT);
+ buffer.append("=");
+ buffer.append(Tag._UNREADCOUNT);
+ buffer.append("+1 WHERE ");
+ buffer.append(Tag._UID);
+ buffer.append(" IN(SELECT ");
+ buffer.append(ItemTag._TAGUID);
+ buffer.append(" FROM ");
+ buffer.append(ItemTag.TABLE_NAME);
+ buffer.append(" WHERE ");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append("=?) AND ");
+ buffer.append(Tag._UNREADCOUNT);
+ buffer.append("<1000");
+ INCREASE_TAG_UNREAD_COUNT = buffer.toString();
+ }
+
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("INSERT INTO ");
+ buffer.append(ItemTag.TABLE_NAME);
+ buffer.append("(");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append(",");
+ buffer.append(ItemTag._TAGUID);
+ buffer.append(")VALUES(?,?)");
+ INSERT_ITEM_TAG = buffer.toString();
+ }
+
+ static {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("INSERT OR REPLACE INTO ");
+ builder.append(Setting.TABLE_NAME);
+ builder.append('(');
+ builder.append(Setting._NAME);
+ builder.append(',');
+ builder.append(Setting._VALUE);
+ builder.append(") VALUES (?,?)");
+ INSERT_OR_REPLACE_SETTING = builder.toString();
+ }
+
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("INSERT INTO ");
+ buffer.append(SubscriptionTag.TABLE_NAME);
+ buffer.append("(");
+ buffer.append(SubscriptionTag._SUBSCRIPTIONUID);
+ buffer.append(",");
+ buffer.append(SubscriptionTag._TAGUID);
+ buffer.append(")VALUES(?,?)");
+ INSERT_SUBSCRIPTION_TAG = buffer.toString();
+ }
+
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("UPDATE ");
+ buffer.append(Item.TABLE_NAME);
+ buffer.append(" SET ");
+ buffer.append(ItemState._ISREAD);
+ buffer.append("=1 WHERE ");
+ buffer.append(Item._UID);
+ buffer.append("=?");
+ MARK_ITEM_AS_READ = buffer.toString();
+ }
+
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("SELECT * FROM ");
+ buffer.append(Tag.TABLE_NAME);
+ buffer.append(" WHERE ");
+ buffer.append(Tag._UID);
+ buffer.append(" IN(SELECT ");
+ buffer.append(ItemTag._TAGUID);
+ buffer.append(" FROM ");
+ buffer.append(ItemTag.TABLE_NAME);
+ buffer.append(" WHERE ");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append("=?)");
+ SELECT_ITEM_TAGS = buffer.toString();
+ }
+
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("SELECT ");
+ buffer.append(ItemTag._TAGUID);
+ buffer.append(" FROM ");
+ buffer.append(ItemTag.TABLE_NAME);
+ buffer.append(" WHERE ");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append("=?");
+ SELECT_ITEM_TAGS_UID = buffer.toString();
+ }
+
+ /*
+ * UPDATE itemTags SET itemUid = SUBSTR(itemUid, LENGTH(RTRIM(itemUid,
+ * '0123456789abcdef')) + 1);
+ */
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("UPDATE ");
+ buffer.append(ItemTag.TABLE_NAME);
+ buffer.append(" SET ");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append("=SUBSTR(");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append(",LENGTH(RTRIM(");
+ buffer.append(ItemTag._ITEMUID);
+ buffer.append(",'0123456789abcdef'))+1)");
+ UPGRADE_ITEM_TAGS_ITEM_UID = buffer.toString();
+ }
+
+ /*
+ * UPDATE items SET uid = SUBSTR(uid, LENGTH(RTRIM(uid, '0123456789abcdef'))
+ * + 1);
+ */
+ static {
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("UPDATE ");
+ buffer.append(Item.TABLE_NAME);
+ buffer.append(" SET ");
+ buffer.append(Item._UID);
+ buffer.append("=SUBSTR(");
+ buffer.append(Item._UID);
+ buffer.append(",LENGTH(RTRIM(");
+ buffer.append(Item._UID);
+ buffer.append(",'0123456789abcdef'))+1)");
+ UPGRADE_ITEMS_UID = buffer.toString();
+ }
+
+ private SQLConstants() {
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/Setting.java b/src/com/pursuer/reader/easyrss/data/Setting.java
new file mode 100644
index 0000000..4713508
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/Setting.java
@@ -0,0 +1,125 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class Setting implements Entity {
+ public static final String TABLE_NAME = "settings";
+ public static final Uri CONTENT_URI = Uri.parse(DataProvider.SETTING_CONTENT_URI);
+
+ public static final String _NAME = "name";
+ public static final String _VALUE = "value";
+ public static final String[] COLUMNS = { _NAME, _VALUE };
+
+ public static final String SQL_CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + _NAME
+ + " PRIMARY KEY ," + _VALUE + " TEXT NOT NULL)";
+ public static final String[][] INDEX_COLUMNS = { { _NAME } };
+
+ public static final String SETTING_AUTH = "auth";
+ public static final String SETTING_TOKEN = "token";
+ public static final String SETTING_USERNAME = "username";
+ public static final String SETTING_PASSWORD = "password";
+ public static final String SETTING_SHOW_HELP = "showHelp";
+ public static final String SETTING_IS_CLIENT_LOGIN = "isClientLogin";
+ public static final String SETTING_FONT_SIZE = "fontSize";
+ public static final String SETTING_TOKEN_EXPIRE_TIME = "tokenExpireTime";
+ public static final String SETTING_ITEM_LIST_EXPIRE_TIME = "itemListExpireTime";
+ public static final String SETTING_SUBSCRIPTION_LIST_EXPIRE_TIME = "subscriptionListExpireTime";
+ public static final String SETTING_SYNC_INTERVAL = "syncInterval";
+ public static final String SETTING_SYNC_METHOD = "syncMethod";
+ public static final String SETTING_IMAGE_PREFETCHING = "imagePrefetching";
+ public static final String SETTING_IMAGE_FETCHING = "imageFetching";
+ public static final String SETTING_DESCENDING_ITEMS_ORDERING = "decendingItemsOrdering";
+ public static final String SETTING_NOTIFICATION_ON = "notificationOn";
+ public static final String SETTING_HTTPS_CONECTION = "httpsConnection";
+ public static final String SETTING_IMMEDIATE_STATE_SYNCING = "immediateStateSyncing";
+ public static final String SETTING_MARK_ALL_AS_READ_CONFIRMATION = "markAllAsReadConfirmation";
+ public static final String SETTING_MAX_ITEMS = "maxItems";
+ public static final String SETTING_THEME = "theme";
+ public static final String SETTING_TAG_LIST_EXPIRE_TIME = "tagListExpireTime";
+ public static final String SETTING_GLOBAL_VIEW_TYPE = "globalViewType";
+ public static final String SETTING_GLOBAL_NEWEST_ITEM_TIMESTAMP = "globalNewestItemTimestamp";
+ public static final String SETTING_GLOBAL_ITEM_UPDATE_TIME = "globalItemUpdateTime";
+ public static final String SETTING_GLOBAL_ITEM_UNREAD_COUNT = "globalItemUnreadCount";
+ public static final String SETTING_BROWSER_CHOICE = "browserChoice";
+ public static final String SETTING_VOLUMN_KEY_SWITCHING = "volumnKeySwitching";
+
+ public static Setting fromCursor(final Cursor cur) {
+ return new Setting(Utils.getStringFromCursor(cur, Setting._NAME),
+ Utils.getStringFromCursor(cur, Setting._VALUE));
+ }
+
+ private String name;
+ private String value;
+
+ public Setting() {
+ init(null, null);
+ }
+
+ public Setting(final String name, final Integer value) {
+ init(name, String.valueOf(value));
+ }
+
+ public Setting(final String name, final Long value) {
+ init(name, String.valueOf(value));
+ }
+
+ public Setting(final String name, final String value) {
+ init(name, value);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ private void init(final String name, final String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ final ContentValues ret = new ContentValues(2);
+ ret.put(_NAME, name);
+ ret.put(_VALUE, value);
+ return ret;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ final ContentValues ret = new ContentValues(1);
+ ret.put(_VALUE, value);
+ return ret;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/Subscription.java b/src/com/pursuer/reader/easyrss/data/Subscription.java
new file mode 100644
index 0000000..d6c67c6
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/Subscription.java
@@ -0,0 +1,202 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+
+public class Subscription implements Entity {
+ public static final String TABLE_NAME = "subscriptions";
+
+ public static final Uri CONTENT_URI = Uri.parse(DataProvider.SUBSCRIPTION_CONTENT_URI);
+
+ public static final String _UID = "uid";
+ public static final String _URL = "url";
+ public static final String _TITLE = "title";
+ public static final String _ICON = "icon";
+ public static final String _UNREADCOUNT = "unreadCount";
+ public static final String _UPDATETIME = "updateTime";
+ public static final String _SORTID = "sortid";
+ public static final String _FIRSTITEMMSEC = "firstItemMsec";
+
+ public static final String[] COLUMNS = { _UID, _URL, _TITLE, _ICON, _UNREADCOUNT, _UPDATETIME, _SORTID,
+ _FIRSTITEMMSEC };
+ public static final String[] COLUMNS_TYPE = { "TEXT PRIMARY KEY", "TEXT", "TEXT NOT NULL", "BOLB",
+ "INTEGER NOT NULL DEFAULT 0", "INTEGER NOT NULL DEFAULT 0", "TEXT", "INTEGER NOT NULL DEFAULT 0" };
+ public static final String[][] INDEX_COLUMNS = { { _UID }, { _UPDATETIME } };
+
+ public static Subscription fromCursor(final Cursor cur) {
+ return new Subscription(Utils.getStringFromCursor(cur, _UID), Utils.getStringFromCursor(cur, _URL),
+ Utils.getStringFromCursor(cur, _TITLE), Utils.getBolbFromCursor(cur, _ICON), Utils.getIntFromCursor(
+ cur, _UNREADCOUNT), Utils.getLongFromCursor(cur, _UPDATETIME), Utils.getStringFromCursor(cur,
+ _SORTID), Utils.getLongFromCursor(cur, _FIRSTITEMMSEC));
+ }
+
+ private String uid;
+ private String title;
+ private String url;
+ private String sortId;
+ private Bitmap icon;
+ private List tags;
+ private int unreadCount;
+ private long updateTime;
+ private long firstItemMsec;
+
+ public Subscription() {
+ init(null, null, null, null, null, null, null, null, null);
+ }
+
+ public Subscription(final String uid, final String url, final String title, final byte[] icon,
+ final int unreadCount, final long updateTime, final String sortId, final long firstItemMsec) {
+ initFromByteArray(uid, url, title, icon, null, unreadCount, updateTime, sortId, firstItemMsec);
+ }
+
+ public Subscription(final String uid, final String url, final String title, final byte[] icon,
+ final List tags, final int unreadCount, final long updateTime, final String sortId,
+ final long firstItemMsec) {
+ initFromByteArray(uid, url, title, icon, tags, unreadCount, updateTime, sortId, firstItemMsec);
+ }
+
+ public void addTag(final String tag) {
+ tags.add(tag);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null, null, null, null, null, null, null, null);
+ }
+
+ public long getFirstItemMsec() {
+ return firstItemMsec;
+ }
+
+ public Bitmap getIcon() {
+ return icon;
+ }
+
+ public String getSortId() {
+ return sortId;
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getUid() {
+ return this.uid;
+ }
+
+ public int getUnreadCount() {
+ return unreadCount;
+ }
+
+ public long getUpdateTime() {
+ return this.updateTime;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ private void init(final String uid, final String url, final String title, final Bitmap icon,
+ final List tags, final Integer unreadCount, final Long updateTime, final String sortId,
+ final Long firstItemMsec) {
+ this.uid = uid;
+ this.url = url;
+ this.title = title;
+ this.icon = icon;
+ this.tags = (tags == null) ? new LinkedList() : tags;
+ this.unreadCount = (unreadCount == null) ? 0 : unreadCount;
+ this.updateTime = (updateTime == null) ? System.currentTimeMillis() : updateTime;
+ this.sortId = sortId;
+ this.firstItemMsec = (firstItemMsec == null) ? 0 : firstItemMsec;
+ }
+
+ private void initFromByteArray(final String uid, final String url, final String title, final byte[] icon,
+ final List tags, final Integer unreadCount, final Long updateTime, final String sortId,
+ final Long firstItemMsec) {
+ final Bitmap bitmapIcon = (icon == null) ? null : BitmapFactory.decodeByteArray(icon, 0, icon.length);
+ init(uid, url, title, bitmapIcon, tags, unreadCount, updateTime, sortId, firstItemMsec);
+ }
+
+ public void setFirstItemMsec(final long firstItemMsec) {
+ this.firstItemMsec = firstItemMsec;
+ }
+
+ public void setIcon(final Bitmap icon) {
+ this.icon = icon;
+ }
+
+ public void setIcon(final byte[] icon) {
+ this.icon = (icon == null) ? null : BitmapFactory.decodeByteArray(icon, 0, icon.length);
+ }
+
+ public void setSortId(final String sortId) {
+ this.sortId = sortId;
+ }
+
+ public void setTags(final List tags) {
+ this.tags = tags;
+ }
+
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ }
+
+ public void setUnreadCount(final int unreadCount) {
+ this.unreadCount = unreadCount;
+ }
+
+ public void setUpdateTime(final long syncTime) {
+ this.updateTime = syncTime;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public ContentValues toContentValues() {
+ final ContentValues ret = new ContentValues(6);
+ ret.put(_UID, uid);
+ ret.put(_URL, url);
+ ret.put(_TITLE, title);
+ ret.put(_UPDATETIME, updateTime);
+ ret.put(_SORTID, sortId);
+ ret.put(_FIRSTITEMMSEC, firstItemMsec);
+ return ret;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ final ContentValues ret = new ContentValues(3);
+ ret.put(_UPDATETIME, updateTime);
+ ret.put(_SORTID, sortId);
+ ret.put(_FIRSTITEMMSEC, firstItemMsec);
+ return ret;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/SubscriptionTag.java b/src/com/pursuer/reader/easyrss/data/SubscriptionTag.java
new file mode 100644
index 0000000..2026809
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/SubscriptionTag.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class SubscriptionTag implements Entity {
+ public static final String TABLE_NAME = "subscriptionTags";
+ public static final Uri CONTENT_URI = Uri.parse(DataProvider.SUBSCRIPTIONTAG_CONTENT_URI);
+
+ public static final String _SUBSCRIPTIONUID = "subscriptionUid";
+ public static final String _TAGUID = "tagUid";
+ public static final String[] COLUMNS = { _SUBSCRIPTIONUID, _TAGUID };
+
+ public static final String SQL_CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + _SUBSCRIPTIONUID
+ + " TEXT," + _TAGUID + " TEXT, PRIMARY KEY (" + _SUBSCRIPTIONUID + "," + _TAGUID + "))";
+ public static final String[][] INDEX_COLUMNS = { { _SUBSCRIPTIONUID }, { _TAGUID } };
+
+ public static SubscriptionTag fromCursor(final Cursor cur) {
+ return new SubscriptionTag(Utils.getStringFromCursor(cur, SubscriptionTag._SUBSCRIPTIONUID),
+ Utils.getStringFromCursor(cur, SubscriptionTag._TAGUID));
+ }
+
+ private String subscriptionUid;
+ private String tagUid;
+
+ public SubscriptionTag() {
+ init(null, null);
+ }
+
+ public SubscriptionTag(final String subscriptionUid, final String tagUid) {
+ init(subscriptionUid, tagUid);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null);
+ }
+
+ public String getSubscriptionUid() {
+ return subscriptionUid;
+ }
+
+ public String getTagUid() {
+ return tagUid;
+ }
+
+ private void init(final String subscriptionUid, final String tagUid) {
+ this.subscriptionUid = subscriptionUid;
+ this.tagUid = tagUid;
+ }
+
+ public void setSubscriptionUid(final String subscriptionUid) {
+ this.subscriptionUid = subscriptionUid;
+ }
+
+ public void setTagUid(final String tagUid) {
+ this.tagUid = tagUid;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ final ContentValues ret = new ContentValues(2);
+ ret.put(_SUBSCRIPTIONUID, subscriptionUid);
+ ret.put(_TAGUID, tagUid);
+ return ret;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ final ContentValues ret = new ContentValues();
+ return ret;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/Tag.java b/src/com/pursuer/reader/easyrss/data/Tag.java
new file mode 100644
index 0000000..0c15943
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/Tag.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.Html;
+
+public class Tag implements Entity {
+ public static final String TABLE_NAME = "tags";
+ public static final Uri CONTENT_URI = Uri.parse(DataProvider.TAG_CONTENT_URI);
+
+ public static final String _UID = "uid";
+ public static final String _UNREADCOUNT = "unreadCount";
+ public static final String _UPDATETIME = "updateTime";
+ public static final String _SORTID = "sortId";
+
+ public static final String[] COLUMNS = { _UID, _UNREADCOUNT, _UPDATETIME, _SORTID };
+ public static final String[] COLUMNS_TYPE = { "TEXT PRIMARY KEY", "INTEGER NOT NULL DEFAULT 0",
+ "INTEGER NOT NULL DEFAULT 0", "TEXT", "INTEGER NOT NULL DEFAULT 0" };
+ public static final String[][] INDEX_COLUMNS = { { _UID }, { _UPDATETIME } };
+
+ public static Tag fromCursor(final Cursor cur) {
+ return new Tag(Utils.getStringFromCursor(cur, _UID), Utils.getIntFromCursor(cur, _UNREADCOUNT),
+ Utils.getLongFromCursor(cur, _UPDATETIME), Utils.getStringFromCursor(cur, _SORTID));
+ }
+
+ private String uid;
+ private String sortId;
+ private int unreadCount;
+ private long updateTime;
+
+ public Tag() {
+ init(null, null, null, null);
+ }
+
+ public Tag(final String uid, final int unreadCount, final long updateTime, final String sortId) {
+ init(uid, unreadCount, updateTime, sortId);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null, null, null);
+ }
+
+ public String getSortId() {
+ return sortId;
+ }
+
+ public String getTitle() {
+ return Html.fromHtml(uid.substring(uid.lastIndexOf('/') + 1)).toString();
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ public int getUnreadCount() {
+ return unreadCount;
+ }
+
+ public long getUpdateTime() {
+ return updateTime;
+ }
+
+ private void init(final String uid, final Integer unreadCount, final Long updateTime, final String sortId) {
+ this.uid = uid;
+ this.unreadCount = (unreadCount == null) ? 0 : unreadCount;
+ this.updateTime = (updateTime == null) ? System.currentTimeMillis() : updateTime;
+ this.sortId = sortId;
+ }
+
+ public void setSortId(String sortId) {
+ this.sortId = sortId;
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ }
+
+ public void setUnreadCount(final int unreadCount) {
+ this.unreadCount = unreadCount;
+ }
+
+ public void setUpdateTime(final long updateTime) {
+ this.updateTime = updateTime;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ final ContentValues ret = new ContentValues(4);
+ ret.put(_UID, uid);
+ ret.put(_UPDATETIME, updateTime);
+ ret.put(_SORTID, sortId);
+ return ret;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ final ContentValues ret = new ContentValues(3);
+ ret.put(_UPDATETIME, updateTime);
+ ret.put(_SORTID, sortId);
+ return ret;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/Transaction.java b/src/com/pursuer/reader/easyrss/data/Transaction.java
new file mode 100644
index 0000000..e72b467
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/Transaction.java
@@ -0,0 +1,126 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import com.pursuer.reader.easyrss.Utils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class Transaction implements Entity {
+ public static final String TABLE_NAME = "transactions";
+ public static final Uri CONTENT_URI = Uri.parse(DataProvider.TRANSACTION_CONTENT_URI);
+
+ public static final String _ID = "id";
+ public static final String _UID = "uid";
+ public static final String _TYPE = "type";
+ public static final String _CONTENT = "note";
+
+ public static final int TYPE_UNKNOWN = -1;
+ public static final int TYPE_SET_READ = 0;
+ public static final int TYPE_REMOVE_READ = 1;
+ public static final int TYPE_SET_STARRED = 2;
+ public static final int TYPE_REMOVE_STARRED = 3;
+
+ public static final String[] COLUMNS = { _ID, _UID, _TYPE, _CONTENT };
+ public static final String[] COLUMNS_TYPE = { "INTEGER PRIMARY KEY AUTOINCREMENT", "TEXT NOT NULL",
+ "INTEGER NOT NULL", "TEXT" };
+ public static final String[][] INDEX_COLUMNS = { { _UID }, { _UID, _TYPE } };
+
+ public static Transaction fromCursor(final Cursor cur) {
+ return new Transaction(Utils.getLongFromCursor(cur, _ID), Utils.getStringFromCursor(cur, _UID),
+ Utils.getStringFromCursor(cur, _CONTENT), Utils.getIntFromCursor(cur, _TYPE));
+ }
+
+ private long id;
+ private String uid;
+ private String content;
+ private int type;
+
+ public Transaction() {
+ init(null, null, null, null);
+ }
+
+ public Transaction(final long id, final String uid, final String content, final int type) {
+ init(id, uid, content, type);
+ }
+
+ public Transaction(final String uid, final String content, final int type) {
+ init(null, uid, content, type);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null, null, null);
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ private void init(final Long id, final String uid, final String content, final Integer type) {
+ this.id = (id == null) ? 0 : id;
+ this.uid = uid;
+ this.setContent(content);
+ this.type = (type == null) ? TYPE_UNKNOWN : type;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public void setId(final long id) {
+ this.id = id;
+ }
+
+ public void setType(final int type) {
+ this.type = type;
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ final ContentValues ret = new ContentValues(3);
+ if (id > 0) {
+ ret.put(_ID, id);
+ }
+ ret.put(_UID, uid);
+ ret.put(_TYPE, type);
+ ret.put(_CONTENT, content);
+ return ret;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ final ContentValues ret = new ContentValues(2);
+ ret.put(_UID, uid);
+ ret.put(_TYPE, type);
+ ret.put(_CONTENT, content);
+ return ret;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/UnreadCount.java b/src/com/pursuer/reader/easyrss/data/UnreadCount.java
new file mode 100644
index 0000000..a21c59a
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/UnreadCount.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data;
+
+import android.content.ContentValues;
+
+public class UnreadCount implements Entity {
+ private String uid;
+ private int count;
+ private long newestItemTime;
+
+ public UnreadCount() {
+ init(null, null, null);
+ }
+
+ public UnreadCount(final String uid, final int count, final long newestItemTime) {
+ init(uid, count, newestItemTime);
+ }
+
+ @Override
+ public void clear() {
+ init(null, null, null);
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public long getNewestItemTime() {
+ return newestItemTime;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ private void init(final String uid, final Integer count, final Long newestItemTime) {
+ this.uid = uid;
+ this.count = (count == null) ? 0 : count;
+ this.newestItemTime = (newestItemTime == null) ? 0 : newestItemTime;
+ }
+
+ public void setCount(final int count) {
+ this.count = count;
+ }
+
+ public void setNewestItemTime(final long newestItemTime) {
+ this.newestItemTime = newestItemTime;
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ }
+
+ @Override
+ public ContentValues toContentValues() {
+ return null;
+ }
+
+ @Override
+ public ContentValues toUpdateContentValues() {
+ return null;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/ItemIdJSONParser.java b/src/com/pursuer/reader/easyrss/data/parser/ItemIdJSONParser.java
new file mode 100644
index 0000000..40469ae
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/ItemIdJSONParser.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.pursuer.reader.easyrss.data.ItemId;
+
+public class ItemIdJSONParser {
+ final private JsonParser parser;
+ private OnItemIdRetrievedListener listener;
+
+ public ItemIdJSONParser(final byte[] input) throws JsonParseException, IOException {
+ final JsonFactory factory = new JsonFactory();
+ this.parser = factory.createJsonParser(input);
+ }
+
+ public ItemIdJSONParser(final InputStream input) throws JsonParseException, IOException {
+ final JsonFactory factory = new JsonFactory();
+ this.parser = factory.createJsonParser(input);
+ }
+
+ public OnItemIdRetrievedListener getListener() {
+ return listener;
+ }
+
+ public void parse() throws JsonParseException, IOException {
+ ItemId itemId = new ItemId();
+ int level = 0;
+ while (parser.nextToken() != null) {
+ final String name = parser.getCurrentName();
+ switch (parser.getCurrentToken()) {
+ case START_OBJECT:
+ case START_ARRAY:
+ level++;
+ break;
+ case END_OBJECT:
+ case END_ARRAY:
+ level--;
+ break;
+ case VALUE_STRING:
+ if (level == 3) {
+ if ("id".equals(name)) {
+ itemId.setUid(Long.toHexString(Long.valueOf(parser.getText())));
+ } else if ("timestampUsec".equals(name)) {
+ itemId.setTimestamp(Long.valueOf(parser.getText()));
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ if (level == 2) {
+ if (itemId.getUid() != null && listener != null) {
+ listener.onItemIdRetrieved(itemId);
+ }
+ itemId = new ItemId();
+ }
+ }
+ parser.close();
+ }
+
+ public void parse(final OnItemIdRetrievedListener listener) throws JsonParseException, IOException {
+ setListener(listener);
+ parse();
+ }
+
+ public void setListener(final OnItemIdRetrievedListener listener) {
+ this.listener = listener;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/ItemJSONParser.java b/src/com/pursuer/reader/easyrss/data/parser/ItemJSONParser.java
new file mode 100644
index 0000000..f853d0e
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/ItemJSONParser.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import android.text.Html;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.pursuer.reader.easyrss.data.DataUtils;
+import com.pursuer.reader.easyrss.data.Item;
+
+public class ItemJSONParser {
+ final private JsonParser parser;
+ private OnItemRetrievedListener listener;
+
+ public ItemJSONParser(final byte[] input) throws JsonParseException, IOException {
+ final JsonFactory factory = new JsonFactory();
+ this.parser = factory.createJsonParser(input);
+ }
+
+ public ItemJSONParser(final InputStream input) throws JsonParseException, IOException {
+ final JsonFactory factory = new JsonFactory();
+ this.parser = factory.createJsonParser(input);
+ }
+
+ public OnItemRetrievedListener getListener() {
+ return listener;
+ }
+
+ public void parse() throws JsonParseException, IOException {
+ Item item = new Item();
+ int level = 0;
+ boolean found = false;
+ while (parser.nextToken() != null) {
+ final String name = parser.getCurrentName();
+ switch (parser.getCurrentToken()) {
+ case START_OBJECT:
+ case START_ARRAY:
+ level++;
+ break;
+ case END_OBJECT:
+ case END_ARRAY:
+ level--;
+ break;
+ case VALUE_STRING:
+ if (level == 1 && "continuation".equals(name)) {
+ if (listener != null) {
+ listener.onListContinuationRetrieved(parser.getText());
+ }
+ } else if (level == 3) {
+ if ("id".equals(name)) {
+ final String text = parser.getText();
+ item.setUid(text.substring(text.lastIndexOf('/') + 1));
+ } else if ("title".equals(name)) {
+ item.setTitle(Html.fromHtml(parser.getText()).toString());
+ } else if ("timestampUsec".equals(name)) {
+ item.setTimestamp(Long.valueOf(parser.getText()));
+ } else if ("author".equals(name)) {
+ item.setAuthor(Html.fromHtml(parser.getText()).toString());
+ }
+ } else if (level == 4) {
+ if ("content".equals(name)) {
+ item.setContent(parser.getText());
+ } else if ("streamId".equals(name)) {
+ item.setSourceUri(parser.getText());
+ } else if ("title".equals(name)) {
+ item.setSourceTitle(Html.fromHtml(parser.getText()).toString());
+ }
+ } else if (level == 5 && "href".equals(name)) {
+ item.setHref(parser.getText());
+ }
+ break;
+ case FIELD_NAME:
+ if (level == 1 && "items".equals(name)) {
+ found = true;
+ } else if (level == 3 && "categories".equals(name)) {
+ parser.nextToken();
+ if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
+ while (parser.nextToken() != JsonToken.END_ARRAY) {
+ if (parser.getCurrentToken() == JsonToken.VALUE_STRING) {
+ final String category = parser.getText();
+ if (DataUtils.isReadUid(category)) {
+ item.getState().setRead(true);
+ } else if (DataUtils.isStarredUid(category)) {
+ item.getState().setStarred(true);
+ } else if (DataUtils.isTagUid(category)) {
+ item.addTag(category);
+ }
+ }
+ }
+ }
+ } else if (level == 3 && "enclosure".equals(name)) {
+ parser.nextToken();
+ if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
+ while (parser.nextToken() != JsonToken.END_ARRAY) {
+ }
+ }
+ }
+ default:
+ break;
+ }
+ if (level == 2) {
+ if (item.getUid() != null && listener != null) {
+ listener.onItemRetrieved(item);
+ }
+ item = new Item();
+ }
+ }
+ parser.close();
+ if (!found) {
+ throw new IllegalStateException("Invalid JSON input");
+ }
+ }
+
+ public void parse(final OnItemRetrievedListener listener) throws JsonParseException, IOException {
+ setListener(listener);
+ parse();
+ }
+
+ public void setListener(final OnItemRetrievedListener listener) {
+ this.listener = listener;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/OnItemIdRetrievedListener.java b/src/com/pursuer/reader/easyrss/data/parser/OnItemIdRetrievedListener.java
new file mode 100644
index 0000000..962aa07
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/OnItemIdRetrievedListener.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import java.io.IOException;
+
+import com.pursuer.reader.easyrss.data.ItemId;
+
+public interface OnItemIdRetrievedListener {
+ void onItemIdRetrieved(ItemId itemId) throws IOException;
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/OnItemRetrievedListener.java b/src/com/pursuer/reader/easyrss/data/parser/OnItemRetrievedListener.java
new file mode 100644
index 0000000..e1e8c0a
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/OnItemRetrievedListener.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import java.io.IOException;
+
+import com.pursuer.reader.easyrss.data.Item;
+
+public interface OnItemRetrievedListener {
+ void onItemRetrieved(Item item) throws IOException;
+
+ void onListContinuationRetrieved(String continuation);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/OnSubscriptionRetrievedListener.java b/src/com/pursuer/reader/easyrss/data/parser/OnSubscriptionRetrievedListener.java
new file mode 100644
index 0000000..a04b289
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/OnSubscriptionRetrievedListener.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import com.pursuer.reader.easyrss.data.Subscription;
+
+public interface OnSubscriptionRetrievedListener {
+ void onSubscriptionRetrieved(Subscription sub);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/OnTagRetrievedListener.java b/src/com/pursuer/reader/easyrss/data/parser/OnTagRetrievedListener.java
new file mode 100644
index 0000000..fd18059
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/OnTagRetrievedListener.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import com.pursuer.reader.easyrss.data.Tag;
+
+public interface OnTagRetrievedListener {
+ void onTagRetrieved(Tag tag);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/OnUnreadCountRetrievedListener.java b/src/com/pursuer/reader/easyrss/data/parser/OnUnreadCountRetrievedListener.java
new file mode 100644
index 0000000..93c49c2
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/OnUnreadCountRetrievedListener.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import com.pursuer.reader.easyrss.data.UnreadCount;
+
+public interface OnUnreadCountRetrievedListener {
+ void onUnreadCountRetrieved(UnreadCount count);
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/SubscriptionJSONParser.java b/src/com/pursuer/reader/easyrss/data/parser/SubscriptionJSONParser.java
new file mode 100644
index 0000000..040ec28
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/SubscriptionJSONParser.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import android.text.Html;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.pursuer.reader.easyrss.data.Subscription;
+
+public class SubscriptionJSONParser {
+ final private InputStream input;
+ private OnSubscriptionRetrievedListener listener;
+
+ public SubscriptionJSONParser(final InputStream input) {
+ this.input = input;
+ }
+
+ public InputStream getInput() {
+ return input;
+ }
+
+ public void setListener(final OnSubscriptionRetrievedListener listener) {
+ this.listener = listener;
+ }
+
+ public void parse() throws JsonParseException, IOException, IllegalStateException {
+ final JsonFactory factory = new JsonFactory();
+ final JsonParser parser = factory.createJsonParser(input);
+ Subscription sub = new Subscription();
+ int level = 0;
+ boolean found = false;
+ while (parser.nextToken() != null) {
+ final String name = parser.getCurrentName();
+ switch (parser.getCurrentToken()) {
+ case START_OBJECT:
+ case START_ARRAY:
+ level++;
+ break;
+ case END_OBJECT:
+ case END_ARRAY:
+ level--;
+ break;
+ case VALUE_STRING:
+ if (level == 3) {
+ if ("id".equals(name)) {
+ sub.setUid(parser.getText());
+ } else if ("htmlUrl".equals(name)) {
+ sub.setUrl(parser.getText());
+ } else if ("title".equals(name)) {
+ sub.setTitle(Html.fromHtml(parser.getText()).toString());
+ } else if ("sortid".equals(name)) {
+ sub.setSortId(parser.getText());
+ } else if ("firstitemmsec".equals(name)) {
+ sub.setFirstItemMsec(Long.valueOf(parser.getText()));
+ }
+ } else if (level == 5 && "id".equals(name)) {
+ sub.addTag(parser.getText());
+ }
+ break;
+ case FIELD_NAME:
+ if (level == 1 && "subscriptions".equals(name)) {
+ found = true;
+ }
+ break;
+ default:
+ }
+ if (level == 2) {
+ if (sub.getUid() != null && listener != null) {
+ listener.onSubscriptionRetrieved(sub);
+ }
+ sub = new Subscription();
+ }
+ }
+ parser.close();
+ if (!found) {
+ throw new IllegalStateException("Invalid JSON input");
+ }
+ }
+
+ public void parse(final OnSubscriptionRetrievedListener listener) throws JsonParseException, IllegalStateException,
+ IOException {
+ setListener(listener);
+ parse();
+ }
+
+ public OnSubscriptionRetrievedListener getListener() {
+ return listener;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/TagJSONParser.java b/src/com/pursuer/reader/easyrss/data/parser/TagJSONParser.java
new file mode 100644
index 0000000..38ccad8
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/TagJSONParser.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.pursuer.reader.easyrss.data.Tag;
+
+public class TagJSONParser {
+ final private InputStream input;
+ private OnTagRetrievedListener listener;
+
+ public TagJSONParser(final InputStream input) {
+ this.input = input;
+ }
+
+ public InputStream getInput() {
+ return input;
+ }
+
+ public void setListener(final OnTagRetrievedListener listener) {
+ this.listener = listener;
+ }
+
+ public void parse() throws JsonParseException, IOException, IllegalStateException {
+ final JsonFactory factory = new JsonFactory();
+ final JsonParser parser = factory.createJsonParser(input);
+ Tag tag = new Tag();
+ int level = 0;
+ boolean found = false;
+ while (parser.nextToken() != null) {
+ final String name = parser.getCurrentName();
+ switch (parser.getCurrentToken()) {
+ case START_OBJECT:
+ case START_ARRAY:
+ level++;
+ break;
+ case END_OBJECT:
+ case END_ARRAY:
+ level--;
+ break;
+ case VALUE_STRING:
+ if (level == 3) {
+ if ("id".equals(name)) {
+ tag.setUid(parser.getText());
+ } else if ("sortid".equals(name)) {
+ tag.setSortId(parser.getText());
+ }
+ }
+ case FIELD_NAME:
+ if (level == 1 && "tags".equals(name)) {
+ found = true;
+ }
+ default:
+ }
+ if (level == 2) {
+ if (tag.getUid() != null && listener != null) {
+ listener.onTagRetrieved(tag);
+ }
+ tag = new Tag();
+ }
+ }
+ parser.close();
+ if (!found) {
+ throw new IllegalStateException("Invalid JSON input");
+ }
+ }
+
+ public void parse(final OnTagRetrievedListener listener) throws JsonParseException, IllegalStateException,
+ IOException {
+ setListener(listener);
+ parse();
+ }
+
+ public OnTagRetrievedListener getListener() {
+ return listener;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/parser/UnreadCountJSONParser.java b/src/com/pursuer/reader/easyrss/data/parser/UnreadCountJSONParser.java
new file mode 100644
index 0000000..67334c4
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/parser/UnreadCountJSONParser.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.parser;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.pursuer.reader.easyrss.data.UnreadCount;
+
+public class UnreadCountJSONParser {
+ final private JsonParser parser;
+ private OnUnreadCountRetrievedListener listener;
+
+ public UnreadCountJSONParser(final byte[] content) throws JsonParseException, IOException {
+ final JsonFactory factory = new JsonFactory();
+ this.parser = factory.createJsonParser(content);
+ }
+
+ public UnreadCountJSONParser(final InputStream input) throws JsonParseException, IOException {
+ final JsonFactory factory = new JsonFactory();
+ this.parser = factory.createJsonParser(input);
+ }
+
+ public void setListener(final OnUnreadCountRetrievedListener listener) {
+ this.listener = listener;
+ }
+
+ public void parse() throws JsonParseException, IOException, IllegalStateException {
+ UnreadCount count = new UnreadCount();
+ int level = 0;
+ boolean found = false;
+ while (parser.nextToken() != null) {
+ final String name = parser.getCurrentName();
+ switch (parser.getCurrentToken()) {
+ case START_OBJECT:
+ case START_ARRAY:
+ level++;
+ break;
+ case END_OBJECT:
+ case END_ARRAY:
+ level--;
+ break;
+ case VALUE_NUMBER_INT:
+ if (level == 3 && "count".equals(name)) {
+ count.setCount(parser.getIntValue());
+ }
+ case VALUE_STRING:
+ if (level == 3 && "id".equals(name)) {
+ count.setUid(parser.getText());
+ } else if (level == 3 && "newestItemTimestampUsec".equals(name)) {
+ count.setNewestItemTime(Long.valueOf(parser.getText()));
+ }
+ case FIELD_NAME:
+ if (level == 1 && "unreadcounts".equals(name)) {
+ found = true;
+ }
+ default:
+ }
+ if (level == 2) {
+ if (count.getUid() != null && listener != null) {
+ listener.onUnreadCountRetrieved(count);
+ }
+ count = new UnreadCount();
+ }
+ }
+ parser.close();
+ if (!found) {
+ throw new IllegalStateException("Invalid JSON input");
+ }
+ }
+
+ public void parse(final OnUnreadCountRetrievedListener listener) throws JsonParseException, IllegalStateException,
+ IOException {
+ setListener(listener);
+ parse();
+ }
+
+ public OnUnreadCountRetrievedListener getListener() {
+ return listener;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/AbsSetting.java b/src/com/pursuer/reader/easyrss/data/readersetting/AbsSetting.java
new file mode 100644
index 0000000..640935e
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/AbsSetting.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public abstract class AbsSetting {
+ public AbsSetting(final DataMgr dataMgr) {
+ final Data staticData = getStaticValue();
+ if (staticData == null) {
+ final String settingData = dataMgr.getSettingByName(getName());
+ if (settingData == null) {
+ setData(dataMgr, getDefault());
+ } else {
+ setStaticValue(settingData);
+ }
+ }
+ }
+
+ public Data getData() {
+ return getStaticValue();
+ }
+
+ protected abstract Data getDefault();
+
+ protected abstract String getName();
+
+ protected abstract Data getStaticValue();
+
+ public void setData(final DataMgr dataMgr, final Data data) {
+ setStaticValue(data);
+ dataMgr.updateSetting(toSetting());
+ }
+
+ protected abstract void setStaticValue(Data value);
+
+ protected abstract void setStaticValue(String value);
+
+ public Setting toSetting() {
+ return new Setting(getName(), String.valueOf(getStaticValue()));
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingBrowserChoice.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingBrowserChoice.java
new file mode 100644
index 0000000..f179ef1
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingBrowserChoice.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingBrowserChoice extends AbsSetting {
+ public static final int BROWSER_CHOICE_UNKNOWN = 0;
+ public static final int BROWSER_CHOICE_MOBILIZED = 1;
+ public static final int BROWSER_CHOICE_ORIGINAL = 2;
+ public static final int BROWSER_CHOICE_EXTERNAL = 3;
+
+ private static Integer value;
+
+ public SettingBrowserChoice(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Integer getDefault() {
+ return BROWSER_CHOICE_UNKNOWN;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_BROWSER_CHOICE;
+ }
+
+ @Override
+ protected Integer getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Integer value) {
+ SettingBrowserChoice.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingBrowserChoice.value = Integer.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingDescendingItemsOrdering.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingDescendingItemsOrdering.java
new file mode 100644
index 0000000..611e0ea
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingDescendingItemsOrdering.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingDescendingItemsOrdering extends AbsSetting {
+ private static Boolean value;
+
+ public SettingDescendingItemsOrdering(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Boolean getDefault() {
+ return true;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_DESCENDING_ITEMS_ORDERING;
+ }
+
+ @Override
+ protected Boolean getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected synchronized void setStaticValue(final Boolean value) {
+ SettingDescendingItemsOrdering.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingDescendingItemsOrdering.value = Boolean.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingFontSize.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingFontSize.java
new file mode 100644
index 0000000..5436dba
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingFontSize.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingFontSize extends AbsSetting {
+ private static Integer value;
+
+ public SettingFontSize(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Integer getDefault() {
+ return 15;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_FONT_SIZE;
+ }
+
+ @Override
+ protected Integer getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected synchronized void setStaticValue(final Integer value) {
+ SettingFontSize.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingFontSize.value = Integer.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingHttpsConnection.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingHttpsConnection.java
new file mode 100644
index 0000000..b7acc2d
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingHttpsConnection.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingHttpsConnection extends AbsSetting {
+ private static Boolean value;
+
+ public SettingHttpsConnection(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Boolean getDefault() {
+ return true;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_HTTPS_CONECTION;
+ }
+
+ @Override
+ protected Boolean getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Boolean value) {
+ SettingHttpsConnection.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingHttpsConnection.value = Boolean.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingImageFetching.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingImageFetching.java
new file mode 100644
index 0000000..791b568
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingImageFetching.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingImageFetching extends AbsSetting {
+ public static final int FETCH_METHOD_WIFI = 0;
+ public static final int FETCH_METHOD_NETWORK = 1;
+ public static final int FETCH_METHOD_DISABLED = 2;
+
+ private static Integer value;
+
+ public SettingImageFetching(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Integer getDefault() {
+ return FETCH_METHOD_NETWORK;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_IMAGE_FETCHING;
+ }
+
+ @Override
+ protected Integer getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Integer value) {
+ SettingImageFetching.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingImageFetching.value = Integer.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingImagePrefetching.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingImagePrefetching.java
new file mode 100644
index 0000000..3737bdf
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingImagePrefetching.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingImagePrefetching extends AbsSetting {
+ private static Boolean value;
+
+ public SettingImagePrefetching(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Boolean getDefault() {
+ return false;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_IMAGE_PREFETCHING;
+ }
+
+ @Override
+ protected Boolean getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Boolean value) {
+ SettingImagePrefetching.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingImagePrefetching.value = Boolean.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingImmediateStateSyncing.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingImmediateStateSyncing.java
new file mode 100644
index 0000000..a6f5e05
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingImmediateStateSyncing.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingImmediateStateSyncing extends AbsSetting {
+ private static Boolean value;
+
+ public SettingImmediateStateSyncing(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Boolean getDefault() {
+ return true;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_IMMEDIATE_STATE_SYNCING;
+ }
+
+ @Override
+ protected Boolean getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Boolean value) {
+ SettingImmediateStateSyncing.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingImmediateStateSyncing.value = Boolean.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingMarkAllAsReadConfirmation.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingMarkAllAsReadConfirmation.java
new file mode 100644
index 0000000..9ce11d1
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingMarkAllAsReadConfirmation.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingMarkAllAsReadConfirmation extends AbsSetting {
+ private static Boolean value;
+
+ public SettingMarkAllAsReadConfirmation(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Boolean getDefault() {
+ return true;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_MARK_ALL_AS_READ_CONFIRMATION;
+ }
+
+ @Override
+ protected Boolean getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Boolean value) {
+ SettingMarkAllAsReadConfirmation.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingMarkAllAsReadConfirmation.value = Boolean.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingMaxItems.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingMaxItems.java
new file mode 100644
index 0000000..ed39ab0
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingMaxItems.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingMaxItems extends AbsSetting {
+ private static Integer value;
+
+ public SettingMaxItems(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Integer getDefault() {
+ return 2000;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_MAX_ITEMS;
+ }
+
+ @Override
+ protected Integer getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Integer value) {
+ SettingMaxItems.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingMaxItems.value = Integer.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingNotificationOn.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingNotificationOn.java
new file mode 100644
index 0000000..2b9bdf6
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingNotificationOn.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingNotificationOn extends AbsSetting {
+ private static Boolean value;
+
+ public SettingNotificationOn(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Boolean getDefault() {
+ return false;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_NOTIFICATION_ON;
+ }
+
+ @Override
+ protected Boolean getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Boolean value) {
+ SettingNotificationOn.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingNotificationOn.value = Boolean.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingSyncInterval.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingSyncInterval.java
new file mode 100644
index 0000000..958382b
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingSyncInterval.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingSyncInterval extends AbsSetting {
+ public static final int SYNC_INTERVAL_ONE_HOUR = 0;
+ public static final int SYNC_INTERVAL_TWO_HOURS = 1;
+ public static final int SYNC_INTERVAL_THREE_HOURS = 2;
+ public static final int SYNC_INTERVAL_FOUR_HOURS = 3;
+ public static final int SYNC_INTERVAL_SIX_HOURS = 4;
+
+ private static Integer value;
+
+ public SettingSyncInterval(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Integer getDefault() {
+ return SYNC_INTERVAL_ONE_HOUR;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_SYNC_INTERVAL;
+ }
+
+ @Override
+ protected Integer getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Integer value) {
+ SettingSyncInterval.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingSyncInterval.value = Integer.valueOf(value);
+ }
+
+ public long toSeconds() {
+ switch (value) {
+ case SYNC_INTERVAL_ONE_HOUR:
+ return 3600;
+ case SYNC_INTERVAL_TWO_HOURS:
+ return 2 * 3600;
+ case SYNC_INTERVAL_THREE_HOURS:
+ return 3 * 3600;
+ case SYNC_INTERVAL_FOUR_HOURS:
+ return 4 * 3600;
+ case SYNC_INTERVAL_SIX_HOURS:
+ return 6 * 3600;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingSyncMethod.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingSyncMethod.java
new file mode 100644
index 0000000..a57df7b
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingSyncMethod.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingSyncMethod extends AbsSetting {
+ public static final int SYNC_METHOD_WIFI = 0;
+ public static final int SYNC_METHOD_NETWORK = 1;
+ public static final int SYNC_METHOD_MANUAL = 2;
+
+ private static Integer value;
+
+ public SettingSyncMethod(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Integer getDefault() {
+ return SYNC_METHOD_NETWORK;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_SYNC_METHOD;
+ }
+
+ @Override
+ protected Integer getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Integer value) {
+ SettingSyncMethod.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingSyncMethod.value = Integer.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingTheme.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingTheme.java
new file mode 100644
index 0000000..d6e7f9d
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingTheme.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingTheme extends AbsSetting {
+ public static final int THEME_NORMAL = 0;
+ public static final int THEME_DARK = 1;
+
+ private static Integer value;
+
+ public SettingTheme(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Integer getDefault() {
+ return THEME_NORMAL;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_THEME;
+ }
+
+ @Override
+ public Integer getStaticValue() {
+ return value;
+ }
+
+ @Override
+ public synchronized void setStaticValue(final Integer value) {
+ SettingTheme.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingTheme.value = Integer.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/data/readersetting/SettingVolumeKeySwitching.java b/src/com/pursuer/reader/easyrss/data/readersetting/SettingVolumeKeySwitching.java
new file mode 100644
index 0000000..9436de4
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/data/readersetting/SettingVolumeKeySwitching.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.data.readersetting;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class SettingVolumeKeySwitching extends AbsSetting {
+ private static Boolean value;
+
+ public SettingVolumeKeySwitching(final DataMgr dataMgr) {
+ super(dataMgr);
+ }
+
+ @Override
+ protected Boolean getDefault() {
+ return true;
+ }
+
+ @Override
+ protected String getName() {
+ return Setting.SETTING_VOLUMN_KEY_SWITCHING;
+ }
+
+ @Override
+ protected Boolean getStaticValue() {
+ return value;
+ }
+
+ @Override
+ protected void setStaticValue(final Boolean value) {
+ SettingVolumeKeySwitching.value = value;
+ }
+
+ @Override
+ protected void setStaticValue(final String value) {
+ SettingVolumeKeySwitching.value = Boolean.valueOf(value);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/AbsListAdapterInflater.java b/src/com/pursuer/reader/easyrss/listadapter/AbsListAdapterInflater.java
new file mode 100644
index 0000000..12dc3e0
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/AbsListAdapterInflater.java
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import android.view.View;
+
+public abstract class AbsListAdapterInflater {
+ static public final int MSG_SETTING_FONT_SIZE_UPDATED = 0;
+
+ protected int fontSize;
+
+ public AbsListAdapterInflater(final int fontSize) {
+ this.fontSize = fontSize;
+ }
+
+ public int getFontSize() {
+ return fontSize;
+ }
+
+ public View inflate(final View view, final AbsListItem obj) {
+ return inflateObject(view, obj);
+ }
+
+ abstract protected View inflateObject(View view, AbsListItem obj);
+
+ public void setFontSize(int fontSize) {
+ this.fontSize = fontSize;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/AbsListItem.java b/src/com/pursuer/reader/easyrss/listadapter/AbsListItem.java
new file mode 100644
index 0000000..c56f0df
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/AbsListItem.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import android.view.LayoutInflater;
+import android.view.View;
+
+abstract public class AbsListItem {
+ public static final String ID_TITLE_ALL = "TITLE_ALL";
+ public static final String ID_TITLE_TAGS = "TITLE_ALL_TAGS";
+ public static final String ID_TITLE_SUBSCRIPTIONS = "TITLE_ALL_SUBS";
+ public static final String ID_TAGS_EMPTY = "TAGS_EMPTY";
+ public static final String ID_SUBSCRIPTIONS_EMPTY = "SUBS_EMPTY";
+ public static final String ID_END = "END";
+ public static final String ITEM_TITLE_TYPE_ALL = "ALL";
+ public static final String ITEM_TITLE_TYPE_STARRED = "STARRED";
+ public static final String ITEM_TITLE_TYPE_UNREAD = "UNREAD";
+
+ protected String id;
+
+ public AbsListItem(final String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public abstract View inflate(View view, LayoutInflater inflater, int fontSize);
+
+ public void setId(final String id) {
+ this.id = id;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/HomeListAdapterInflater.java b/src/com/pursuer/reader/easyrss/listadapter/HomeListAdapterInflater.java
new file mode 100644
index 0000000..78e954b
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/HomeListAdapterInflater.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+
+final public class HomeListAdapterInflater extends AbsListAdapterInflater {
+ public static HomeListAdapterInflater instance;
+
+ public static HomeListAdapterInflater getInstance() {
+ return instance;
+ }
+
+ public synchronized static void init(final Context context, final int fontSize) {
+ if (instance == null) {
+ instance = new HomeListAdapterInflater(context, fontSize);
+ }
+ }
+
+ final LayoutInflater inflater;
+
+ private HomeListAdapterInflater(final Context context, final int fontSize) {
+ super(fontSize);
+
+ this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public View inflateObject(View view, final AbsListItem item) {
+ return item.inflate(view, inflater, fontSize);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListAdapter.java b/src/com/pursuer/reader/easyrss/listadapter/ListAdapter.java
new file mode 100644
index 0000000..5e33bb4
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListAdapter.java
@@ -0,0 +1,152 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnTouchListener;
+import android.widget.BaseAdapter;
+
+public class ListAdapter extends BaseAdapter {
+ final private OnTouchListener onTouchListener;
+ final private LayoutInflater inflater;
+ final private List items;
+ final private Map mItems;
+ private OnItemTouchListener listener;
+ private int fontSize;
+
+ public ListAdapter(final Context context, final int fontSize) {
+ super();
+
+ this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ this.items = new ArrayList();
+ this.mItems = new HashMap();
+ this.fontSize = fontSize;
+ this.onTouchListener = new OnTouchListener() {
+ @Override
+ public boolean onTouch(final View view, final MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ view.setPressed(true);
+ break;
+ case MotionEvent.ACTION_UP:
+ view.setPressed(false);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ view.setPressed(false);
+ break;
+ default:
+ }
+ if (listener != null) {
+ listener.onItemTouched(ListAdapter.this, getItem((Integer) view.getTag()), event);
+ }
+ return true;
+ }
+ };
+ }
+
+ public void clear() {
+ items.clear();
+ mItems.clear();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return items.size();
+ }
+
+ public int getFontSize() {
+ return fontSize;
+ }
+
+ @Override
+ public AbsListItem getItem(final int position) {
+ return items.get(position);
+ }
+
+ @Override
+ public long getItemId(final int position) {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ public Integer getItemLocationById(final String id) {
+ return mItems.get(id);
+ }
+
+ public OnItemTouchListener getListener() {
+ return listener;
+ }
+
+ @Override
+ public View getView(final int position, View view, final ViewGroup parent) {
+ final AbsListItem item = items.get(position);
+ view = item.inflate(view, inflater, fontSize);
+ view.setTag(position);
+ view.setOnTouchListener(onTouchListener);
+ return view;
+ }
+
+ public boolean hasItem(final String id) {
+ return mItems.containsKey(id);
+ }
+
+ public void removeItem(final int position) {
+ final AbsListItem ali = items.get(position);
+ items.remove(ali);
+ mItems.remove(ali.getId());
+ for (int i = position; i < items.size(); i++) {
+ mItems.put(items.get(i).getId(), i);
+ }
+ }
+
+ public void setFontSize(final int fontSize) {
+ this.fontSize = fontSize;
+ }
+
+ public void setListener(final OnItemTouchListener listener) {
+ this.listener = listener;
+ }
+
+ /*
+ * Return: whether item exists.
+ */
+ public boolean updateItem(final AbsListItem item) {
+ return updateItem(item, items.size());
+ }
+
+ /*
+ * Return: whether item exists.
+ */
+ public boolean updateItem(final AbsListItem item, final int suggestedLoc) {
+ final Integer loc = mItems.get(item.getId());
+ if (loc == null) {
+ items.add(suggestedLoc, item);
+ for (int i = suggestedLoc; i < items.size(); i++) {
+ mItems.put(items.get(i).getId(), i);
+ }
+ } else {
+ items.set(loc, item);
+ }
+ notifyDataSetChanged();
+ return (loc != null);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListItemEmpty.java b/src/com/pursuer/reader/easyrss/listadapter/ListItemEmpty.java
new file mode 100644
index 0000000..41b80a1
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListItemEmpty.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class ListItemEmpty extends AbsListItem {
+ private String text;
+
+ public ListItemEmpty(final String id, final String text) {
+ super(id);
+
+ this.text = text;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ @Override
+ public View inflate(View view, final LayoutInflater inflater, final int fontSize) {
+ if (view == null || view.getId() != R.id.ListItemEmpty) {
+ view = inflater.inflate(R.layout.list_item_empty, null);
+ }
+ final TextView txtText = (TextView) view.findViewById(R.id.Title);
+ txtText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
+ txtText.setText(text);
+ return view;
+ }
+
+ public void setText(final String text) {
+ this.text = text;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListItemEndDisabled.java b/src/com/pursuer/reader/easyrss/listadapter/ListItemEndDisabled.java
new file mode 100644
index 0000000..8049604
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListItemEndDisabled.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+
+public class ListItemEndDisabled extends AbsListItem {
+ public ListItemEndDisabled(final String id) {
+ super(id);
+ }
+
+ @Override
+ public View inflate(View view, final LayoutInflater inflater, final int fontSize) {
+ if (view == null || view.getId() != R.id.ListItemEndDisabled) {
+ view = inflater.inflate(R.layout.list_item_end_disabled, null);
+ }
+ final Button btn = (Button) view.findViewById(R.id.BtnLoadMore);
+ btn.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
+ return view;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListItemEndEnabled.java b/src/com/pursuer/reader/easyrss/listadapter/ListItemEndEnabled.java
new file mode 100644
index 0000000..9260a70
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListItemEndEnabled.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+
+public class ListItemEndEnabled extends AbsListItem {
+ public ListItemEndEnabled(final String id) {
+ super(id);
+ }
+
+ @Override
+ public View inflate(View view, final LayoutInflater inflater, final int fontSize) {
+ if (view == null || view.getId() != R.id.ListItemEndEnabled) {
+ view = inflater.inflate(R.layout.list_item_end_enabled, null);
+ }
+ final Button btn = (Button) view.findViewById(R.id.BtnLoadMore);
+ btn.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
+ btn.setClickable(false);
+ return view;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListItemEndLoading.java b/src/com/pursuer/reader/easyrss/listadapter/ListItemEndLoading.java
new file mode 100644
index 0000000..d2e4240
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListItemEndLoading.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class ListItemEndLoading extends AbsListItem {
+ public ListItemEndLoading(final String id) {
+ super(id);
+ }
+
+ @Override
+ public View inflate(View view, final LayoutInflater inflater, final int fontSize) {
+ if (view == null || view.getId() != R.id.ListItemEndLoading) {
+ view = inflater.inflate(R.layout.list_item_end_loading, null);
+ }
+ final TextView txt = (TextView) view.findViewById(R.id.TxtLoading);
+ txt.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
+ return view;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListItemItem.java b/src/com/pursuer/reader/easyrss/listadapter/ListItemItem.java
new file mode 100644
index 0000000..c68ffe4
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListItemItem.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.graphics.Typeface;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ListItemItem extends AbsListItem {
+ private String title;
+ private String subscriptionTitle;
+ private boolean isRead;
+ private boolean isStarred;
+ private long timestamp;
+
+ public ListItemItem(final String id, final String title, final String subscriptionTitle, final boolean isRead,
+ final boolean isStarred, final long timestamp) {
+ super(id);
+
+ this.title = title;
+ this.subscriptionTitle = subscriptionTitle;
+ this.isRead = isRead;
+ this.isStarred = isStarred;
+ this.timestamp = timestamp;
+ }
+
+ public String getSubscriptionTitle() {
+ return subscriptionTitle;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public View inflate(View view, final LayoutInflater inflater, final int fontSize) {
+ if (view == null || view.getId() != R.id.ListItemItem) {
+ view = inflater.inflate(R.layout.list_item_item, null);
+ }
+ final ImageView imgState = (ImageView) view.findViewById(R.id.ItemState);
+ final TextView txtTitle = (TextView) view.findViewById(R.id.Title);
+ final TextView txtSubTitle = (TextView) view.findViewById(R.id.SubscriptionTitle);
+ txtTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
+ txtTitle.setText(title);
+ txtSubTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize * 4 / 5);
+ txtSubTitle.setText(subscriptionTitle);
+ if (isRead) {
+ imgState.setImageResource(R.drawable.read_sign);
+ txtTitle.setTypeface(null, Typeface.NORMAL);
+ } else {
+ imgState.setImageResource(R.drawable.unread_sign);
+ txtTitle.setTypeface(null, Typeface.BOLD);
+ }
+ return view;
+ }
+
+ public boolean isRead() {
+ return isRead;
+ }
+
+ public boolean isStarred() {
+ return isStarred;
+ }
+
+ public void setRead(final boolean isRead) {
+ this.isRead = isRead;
+ }
+
+ public void setStarred(final boolean isStarred) {
+ this.isStarred = isStarred;
+ }
+
+ public void setSubscriptionTitle(final String subscriptionTitle) {
+ this.subscriptionTitle = subscriptionTitle;
+ }
+
+ public void setTimestamp(final long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListItemSubTag.java b/src/com/pursuer/reader/easyrss/listadapter/ListItemSubTag.java
new file mode 100644
index 0000000..c86b860
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListItemSubTag.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ListItemSubTag extends AbsListItem {
+ private Bitmap icon;
+ private int number;
+ private String title;
+
+ public ListItemSubTag(final String id, final String title, final int number, final Bitmap icon) {
+ super(id);
+
+ this.title = title;
+ this.icon = icon;
+ this.number = number;
+ }
+
+ public ListItemSubTag(final String id, final String title, final int number, final Resources res,
+ final int iconResId) {
+ super(id);
+
+ this.title = title;
+ this.icon = BitmapFactory.decodeResource(res, iconResId);
+ this.number = number;
+ }
+
+ public Bitmap getIcon() {
+ return icon;
+ }
+
+ public int getNumber() {
+ return number;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public View inflate(View view, final LayoutInflater inflater, final int fontSize) {
+ if (view == null || view.getId() != R.id.ListItemSubTag) {
+ view = inflater.inflate(R.layout.list_item_sub_tag, null);
+ }
+ final TextView txtNumber = (TextView) view.findViewById(R.id.number);
+ final TextView txtTitle = (TextView) view.findViewById(R.id.TagTitle);
+ final ImageView imgIcon = ((ImageView) view.findViewById(R.id.icon));
+ txtTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
+ txtTitle.setText(title);
+ txtNumber.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
+ if (number >= 1000) {
+ txtNumber.setText("1000+");
+ } else if (number > 0) {
+ txtNumber.setText(String.valueOf(number));
+ txtNumber.setVisibility(View.VISIBLE);
+ } else {
+ txtNumber.setVisibility(View.INVISIBLE);
+ }
+ imgIcon.setImageBitmap(icon);
+ return view;
+ }
+
+ public void setIcon(final Bitmap icon) {
+ this.icon = icon;
+ }
+
+ public void setNumber(final int number) {
+ this.number = number;
+ }
+
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/ListItemTitle.java b/src/com/pursuer/reader/easyrss/listadapter/ListItemTitle.java
new file mode 100644
index 0000000..0841cd9
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/ListItemTitle.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class ListItemTitle extends AbsListItem {
+ private String title;
+
+ public ListItemTitle(final String id, final String title) {
+ super(id);
+
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public View inflate(View view, final LayoutInflater inflater, final int fontSize) {
+ if (view == null || view.getId() != R.id.ListItemTitle) {
+ view = inflater.inflate(R.layout.list_item_title, null);
+ }
+ final TextView txtTitle = (TextView) view.findViewById(R.id.TitleName);
+ txtTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize * 4 / 5);
+ txtTitle.setText(title);
+ return view;
+ }
+
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+}
diff --git a/src/com/pursuer/reader/easyrss/listadapter/OnItemTouchListener.java b/src/com/pursuer/reader/easyrss/listadapter/OnItemTouchListener.java
new file mode 100644
index 0000000..5ee9143
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/listadapter/OnItemTouchListener.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.listadapter;
+
+import android.view.MotionEvent;
+
+public interface OnItemTouchListener {
+ void onItemTouched(ListAdapter adapter, AbsListItem item, MotionEvent event);
+}
diff --git a/src/com/pursuer/reader/easyrss/network/AbsDataSyncer.java b/src/com/pursuer/reader/easyrss/network/AbsDataSyncer.java
new file mode 100644
index 0000000..59b0a00
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/AbsDataSyncer.java
@@ -0,0 +1,263 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+
+import org.apache.http.protocol.HTTP;
+
+import com.pursuer.reader.easyrss.account.ReaderAccountMgr;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.readersetting.SettingHttpsConnection;
+import com.pursuer.reader.easyrss.network.NetworkClient.NetworkException;
+import com.pursuer.reader.easyrss.network.url.AbsURL;
+
+public abstract class AbsDataSyncer {
+ public class DataSyncerException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public DataSyncerException(final Exception exception) {
+ super(exception.getMessage(), exception.getCause());
+ }
+
+ public DataSyncerException(final String message) {
+ super(message);
+ }
+
+ public DataSyncerException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public DataSyncerException(final Throwable cause) {
+ super(cause);
+ }
+ }
+
+ final protected static int CONTENT_IO_BUFFER_SIZE = 16384;
+ final protected static int UNREAD_COUNT_LIMIT = 500;
+ final protected static int GLOBAL_ITEMS_LIMIT = 300;
+ final protected static int GLOBAL_ITEM_IDS_LIMIT = 600;
+ final protected static int ITEM_LIST_QUERY_LIMIT = 20;
+
+ public static final long TOKEN_EXPIRE_TIME = 2 * 60 * 1000;
+
+ final protected DataMgr dataMgr;
+ final protected boolean isHttpsConnection;
+ final protected int networkConfig;
+ private DataSyncerListener listener;
+ private Boolean isPending;
+ private Boolean isRunning;
+
+ public AbsDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ this.isPending = false;
+ this.isRunning = false;
+ this.dataMgr = dataMgr;
+ this.networkConfig = networkConfig;
+ this.isHttpsConnection = new SettingHttpsConnection(dataMgr).getData();
+ }
+
+ protected abstract void finishSyncing();
+
+ public DataSyncerListener getListener() {
+ return listener;
+ }
+
+ public int getNetworkConfig() {
+ return networkConfig;
+ }
+
+ protected byte[] httpGetQueryByte(final AbsURL url) throws DataSyncerException {
+ final NetworkClient client = NetworkClient.getInstance();
+ if (url.isAuthNeeded()) {
+ final String auth = ReaderAccountMgr.getInstance().blockingGetAuth();
+ client.setAuth(auth);
+ }
+ try {
+ String rUrl = url.getURL();
+ final String param = url.getParamsString();
+ if (param.length() > 0) {
+ rUrl += "?" + param;
+ }
+ return client.doGetByte(rUrl);
+ } catch (final Exception e) {
+ throw new DataSyncerException(e);
+ }
+ }
+
+ protected Reader httpGetQueryReader(final AbsURL url) throws DataSyncerException {
+ try {
+ return new InputStreamReader(httpGetQueryStream(url), HTTP.UTF_8);
+ } catch (final UnsupportedEncodingException e) {
+ throw new DataSyncerException(e);
+ }
+ }
+
+ protected InputStream httpGetQueryStream(final AbsURL url) throws DataSyncerException {
+ final NetworkClient client = NetworkClient.getInstance();
+ if (url.isAuthNeeded()) {
+ final String auth = ReaderAccountMgr.getInstance().blockingGetAuth();
+ client.setAuth(auth);
+ }
+ try {
+ String rUrl = url.getURL();
+ final String param = url.getParamsString();
+ if (param.length() > 0) {
+ rUrl += "?" + param;
+ }
+ return client.doGetStream(rUrl);
+ } catch (final Exception exception) {
+ throw new DataSyncerException(exception);
+ }
+ }
+
+ protected byte[] httpPostQueryByte(final AbsURL url) throws DataSyncerException {
+ final NetworkClient client = NetworkClient.getInstance();
+ if (url.isAuthNeeded()) {
+ final String auth = ReaderAccountMgr.getInstance().blockingGetAuth();
+ client.setAuth(auth);
+ }
+ try {
+ String rUrl = url.getURL();
+ final String param = url.getParamsString();
+ if (param.length() > 0) {
+ rUrl += "?" + param;
+ }
+ return client.doPostByte(rUrl, param);
+ } catch (final Exception exception) {
+ throw new DataSyncerException(exception);
+ }
+ }
+
+ protected Reader httpPostQueryReader(final AbsURL url) throws DataSyncerException {
+ try {
+ return new InputStreamReader(httpPostQueryStream(url), HTTP.UTF_8);
+ } catch (final UnsupportedEncodingException exception) {
+ throw new DataSyncerException(exception);
+ }
+ }
+
+ protected InputStream httpPostQueryStream(final AbsURL url) throws DataSyncerException {
+ final NetworkClient client = NetworkClient.getInstance();
+ if (url.isAuthNeeded()) {
+ final String auth = ReaderAccountMgr.getInstance().blockingGetAuth();
+ client.setAuth(auth);
+ }
+ try {
+ return NetworkClient.getInstance().doPostStream(url.getURL(), url.getParamsString());
+ } catch (final IOException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final NetworkException exception) {
+ throw new DataSyncerException(exception);
+ }
+ }
+
+ public boolean isPending() {
+ synchronized (this.isPending) {
+ return isPending;
+ }
+ }
+
+ public boolean isRunning() {
+ synchronized (this.isRunning) {
+ return isRunning;
+ }
+ }
+
+ protected void notifyProgressChanged(final String text, final int progress, final int maxProgress) {
+ if (listener != null) {
+ listener.onProgressChanged(text, progress, maxProgress);
+ }
+ }
+
+ protected String parseContent(final Reader in) throws DataSyncerException {
+ final StringBuilder builder = new StringBuilder();
+ try {
+ final char buff[] = new char[8192];
+ int len;
+ final BufferedReader reader = new BufferedReader(in, 8192);
+ while ((len = reader.read(buff, 0, buff.length)) != -1) {
+ builder.append(buff, 0, len);
+ }
+ return builder.toString();
+ } catch (IOException e) {
+ throw new DataSyncerException(e);
+ } finally {
+ try {
+ in.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+ }
+
+ public boolean setEnterPending() {
+ synchronized (this.isPending) {
+ if (isPending) {
+ return false;
+ } else {
+ isPending = true;
+ return true;
+ }
+ }
+ }
+
+ private boolean setEnterRunning() {
+ synchronized (this.isRunning) {
+ if (isRunning) {
+ return false;
+ } else {
+ isRunning = true;
+ return true;
+ }
+ }
+ }
+
+ public void setListener(final DataSyncerListener listener) {
+ this.listener = listener;
+ }
+
+ public void setPending(final boolean isPending) {
+ synchronized (this.isPending) {
+ this.isPending = isPending;
+ }
+ }
+
+ private void setRunning(final boolean isRunning) {
+ synchronized (this.isRunning) {
+ this.isRunning = isRunning;
+ }
+ }
+
+ protected abstract void startSyncing() throws DataSyncerException;
+
+ public void sync() throws DataSyncerException {
+ if (!setEnterRunning()) {
+ return;
+ }
+ DataSyncerException except = null;
+ try {
+ startSyncing();
+ } catch (final DataSyncerException exception) {
+ except = exception;
+ }
+ finishSyncing();
+ setRunning(false);
+ if (except != null) {
+ throw except;
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/DataSyncerListener.java b/src/com/pursuer/reader/easyrss/network/DataSyncerListener.java
new file mode 100644
index 0000000..0b4158b
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/DataSyncerListener.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+public interface DataSyncerListener {
+ void onProgressChanged(String text, int progress, int maxProgress);
+}
diff --git a/src/com/pursuer/reader/easyrss/network/GlobalItemDataSyncer.java b/src/com/pursuer/reader/easyrss/network/GlobalItemDataSyncer.java
new file mode 100644
index 0000000..9dd0d9f
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/GlobalItemDataSyncer.java
@@ -0,0 +1,280 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.Context;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.pursuer.reader.easyrss.NotificationMgr;
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.DataUtils;
+import com.pursuer.reader.easyrss.data.Item;
+import com.pursuer.reader.easyrss.data.ItemId;
+import com.pursuer.reader.easyrss.data.Setting;
+import com.pursuer.reader.easyrss.data.parser.ItemIdJSONParser;
+import com.pursuer.reader.easyrss.data.parser.ItemJSONParser;
+import com.pursuer.reader.easyrss.data.parser.OnItemIdRetrievedListener;
+import com.pursuer.reader.easyrss.data.parser.OnItemRetrievedListener;
+import com.pursuer.reader.easyrss.data.readersetting.SettingMaxItems;
+import com.pursuer.reader.easyrss.data.readersetting.SettingNotificationOn;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncInterval;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncMethod;
+import com.pursuer.reader.easyrss.network.url.StreamContentsURL;
+import com.pursuer.reader.easyrss.network.url.StreamIdsURL;
+
+public class GlobalItemDataSyncer extends AbsDataSyncer implements DataSyncerListener {
+ private class SyncAllItemsItemListener implements OnItemRetrievedListener {
+ final private List items;
+ private String continuation;
+ private long oldestTimestamp;
+ private long newestTimestamp;
+
+ public SyncAllItemsItemListener() {
+ this.oldestTimestamp = (1L << 62L);
+ this.newestTimestamp = 0;
+ this.items = new LinkedList();
+ }
+
+ public String getContinuation() {
+ return continuation;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ public long getNewestTimestamp() {
+ return newestTimestamp;
+ }
+
+ public long getOldestTimestamp() {
+ return oldestTimestamp;
+ }
+
+ @Override
+ public void onItemRetrieved(final Item item) throws IOException {
+ DataUtils.writeItemToFile(item);
+ items.add(item);
+ newestTimestamp = Math.max(item.getTimestamp(), newestTimestamp);
+ oldestTimestamp = Math.min(item.getTimestamp(), oldestTimestamp);
+ }
+
+ @Override
+ public void onListContinuationRetrieved(final String continuation) {
+ this.continuation = continuation;
+ }
+ }
+
+ private static GlobalItemDataSyncer instance;
+
+ private static synchronized void clearInstance() {
+ instance = null;
+ }
+
+ public static synchronized GlobalItemDataSyncer getInstance(final DataMgr dataMgr, final int networkConfig) {
+ if (instance == null) {
+ instance = new GlobalItemDataSyncer(dataMgr, networkConfig);
+ }
+ return instance;
+ }
+
+ public static synchronized boolean hasInstance() {
+ return instance != null;
+ }
+
+ private GlobalItemDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ super(dataMgr, networkConfig);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else {
+ return (obj instanceof GlobalItemDataSyncer);
+ }
+ }
+
+ @Override
+ protected void finishSyncing() {
+ clearInstance();
+ }
+
+ @Override
+ public void onProgressChanged(final String text, final int progress, final int maxProgress) {
+ notifyProgressChanged(text, progress, maxProgress);
+ }
+
+ @Override
+ protected void startSyncing() throws DataSyncerException {
+ final String sExpTime = dataMgr.getSettingByName(Setting.SETTING_ITEM_LIST_EXPIRE_TIME);
+ if (networkConfig != SettingSyncMethod.SYNC_METHOD_MANUAL
+ && sExpTime != null
+ && Long.valueOf(sExpTime) + new SettingSyncInterval(dataMgr).toSeconds() * 1000 - 10 * 60 * 1000 > System
+ .currentTimeMillis()) {
+ return;
+ }
+
+ syncReadStatus();
+ syncAllItems();
+ syncUnreadCount();
+ syncUnreadItems();
+ NetworkMgr.getInstance().startSyncItemContent();
+
+ final SettingNotificationOn sNotification = new SettingNotificationOn(dataMgr);
+ if (sNotification.getData()) {
+ final String sSetting = dataMgr.getSettingByName(Setting.SETTING_GLOBAL_ITEM_UNREAD_COUNT);
+ final int unreadCount = (sSetting == null) ? 0 : Integer.valueOf(sSetting);
+ if (unreadCount > 0) {
+ NotificationMgr.getInstance().showNewItemsNotification(unreadCount);
+ }
+ }
+
+ dataMgr.updateSetting(new Setting(Setting.SETTING_ITEM_LIST_EXPIRE_TIME, System.currentTimeMillis()));
+ }
+
+ private void syncAllItems() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ int count = 0;
+ String sSetting = dataMgr.getSettingByName(Setting.SETTING_GLOBAL_NEWEST_ITEM_TIMESTAMP);
+ long newestTimestamp = (sSetting == null) ? 0 : Long.valueOf(sSetting);
+ long oldestTimestamp = (sSetting == null) ? (1L << 62L) : Long.valueOf(sSetting);
+ long newNewestTimestamp = newestTimestamp;
+ long newOldestTimestamp = (1L << 62L);
+ long lastTimestamp = (1L << 62L);
+ String continuation = null;
+ do {
+ notifyProgressChanged(context.getString(R.string.TxtSyncingAllItems), count, GLOBAL_ITEMS_LIMIT);
+ final int limit = (count == 0) ? 5 : ITEM_LIST_QUERY_LIMIT;
+ final InputStream stream = httpGetQueryStream(new StreamContentsURL(isHttpsConnection, "", continuation, 0,
+ limit, false));
+ try {
+ final ItemJSONParser parser = new ItemJSONParser(stream);
+ final SyncAllItemsItemListener listener = new SyncAllItemsItemListener();
+ parser.parse(listener);
+ newOldestTimestamp = Math.min(newOldestTimestamp, listener.getOldestTimestamp());
+ newNewestTimestamp = Math.max(newNewestTimestamp, listener.getNewestTimestamp());
+ lastTimestamp = Math.min(lastTimestamp, listener.getOldestTimestamp());
+ continuation = listener.getContinuation();
+ final List items = listener.getItems();
+ dataMgr.addItems(items);
+ count += items.size();
+ if (newOldestTimestamp <= newestTimestamp || items.size() < limit) {
+ break;
+ }
+ } catch (final JsonParseException exception) {
+ exception.printStackTrace();
+ throw new DataSyncerException(exception);
+ } catch (final IllegalStateException exception) {
+ exception.printStackTrace();
+ throw new DataSyncerException(exception);
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ throw new DataSyncerException(exception);
+ } finally {
+ try {
+ stream.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+ } while (count < GLOBAL_ITEMS_LIMIT);
+ notifyProgressChanged(context.getString(R.string.TxtSyncingAllItems), -1, -1);
+ if (newOldestTimestamp <= newestTimestamp) {
+ oldestTimestamp = Math.min(oldestTimestamp, newOldestTimestamp);
+ }
+ newestTimestamp = newNewestTimestamp;
+
+ dataMgr.removeOutdatedItemsWithLimit(new SettingMaxItems(dataMgr).getData());
+ dataMgr.updateSetting(new Setting(Setting.SETTING_GLOBAL_NEWEST_ITEM_TIMESTAMP, String.valueOf(newestTimestamp)));
+ }
+
+ private void syncReadStatus() throws DataSyncerException {
+ final TransactionDataSyncer syncer = TransactionDataSyncer.getInstance(dataMgr, networkConfig);
+ syncer.setListener(this);
+ syncer.sync();
+ syncer.setListener(null);
+ }
+
+ private void syncUnreadCount() throws DataSyncerException {
+ final UnreadCountDataSyncer syncer = new UnreadCountDataSyncer(dataMgr, networkConfig);
+ syncer.setListener(this);
+ syncer.sync();
+ syncer.setListener(null);
+ }
+
+ private void syncUnreadItems() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ String sSetting = dataMgr.getSettingByName(Setting.SETTING_GLOBAL_ITEM_UNREAD_COUNT);
+ final int unreadCount = (sSetting == null) ? 0 : Integer.valueOf(sSetting);
+ if (unreadCount == 0) {
+ dataMgr.markAllItemsAsRead();
+ return;
+ } else if (dataMgr.calcGlobalUnreadItemCount() == unreadCount) {
+ return;
+ }
+
+ notifyProgressChanged(context.getString(R.string.TxtSyncingUnreadItems), -1, -1);
+ final InputStream stream = httpGetQueryStream(new StreamIdsURL(isHttpsConnection,
+ "user/-/state/com.google/reading-list", GLOBAL_ITEM_IDS_LIMIT, true));
+ try {
+ final ItemIdJSONParser parser = new ItemIdJSONParser(stream);
+ final List itemIds = new ArrayList();
+ parser.parse(new OnItemIdRetrievedListener() {
+ @Override
+ public void onItemIdRetrieved(final ItemId itemId) throws IOException {
+ itemIds.add(itemId);
+ }
+ });
+ for (int i = 0; i < itemIds.size(); i += 50) {
+ if (i + 50 <= itemIds.size()) {
+ dataMgr.markItemsAsReadItemIds(itemIds, i, i + 50);
+ } else if (itemIds.size() < GLOBAL_ITEMS_LIMIT) {
+ dataMgr.markItemsAsReadItemIds(itemIds, i, itemIds.size(), true);
+ } else {
+ dataMgr.markItemsAsReadItemIds(itemIds, i, itemIds.size());
+ }
+ if (dataMgr.calcGlobalUnreadItemCount() == unreadCount) {
+ break;
+ }
+ }
+ } catch (final JsonParseException exception) {
+ exception.printStackTrace();
+ throw new DataSyncerException(exception);
+ } catch (final IllegalStateException exception) {
+ exception.printStackTrace();
+ throw new DataSyncerException(exception);
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ throw new DataSyncerException(exception);
+ } finally {
+ try {
+ stream.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/ItemContentDataSyncer.java b/src/com/pursuer/reader/easyrss/network/ItemContentDataSyncer.java
new file mode 100644
index 0000000..531f1d7
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/ItemContentDataSyncer.java
@@ -0,0 +1,328 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import org.htmlcleaner.CleanerProperties;
+import org.htmlcleaner.DefaultTagProvider;
+import org.htmlcleaner.FastHtmlSerializer;
+import org.htmlcleaner.HtmlCleaner;
+import org.htmlcleaner.TagNode;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Pair;
+
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.DataUtils;
+import com.pursuer.reader.easyrss.data.Item;
+import com.pursuer.reader.easyrss.data.ItemState;
+import com.pursuer.reader.easyrss.data.readersetting.SettingImagePrefetching;
+
+// Remember that in this class, the meaning networkConfig is different from other syncers!
+public class ItemContentDataSyncer extends AbsDataSyncer {
+ private class FetchingHelper {
+ final private ExecutorService execService;
+ final private List wrappers;
+ private int totalItems;
+ private int finishedItems;
+
+ public FetchingHelper() {
+ this.execService = Executors.newFixedThreadPool(FETCHING_THREAD_COUNT, new ThreadFactory() {
+ @Override
+ public Thread newThread(final Runnable runnable) {
+ final Thread thread = new Thread(runnable);
+ thread.setPriority(Thread.MIN_PRIORITY);
+ return thread;
+ }
+ });
+ this.wrappers = new ArrayList();
+ this.totalItems = 0;
+ this.finishedItems = 0;
+ }
+
+ public void fetch() {
+ final Context context = dataMgr.getContext();
+ final ContentResolver resolver = context.getContentResolver();
+ final Cursor cur = resolver.query(Item.CONTENT_URI, new String[] { "count(*)" },
+ ItemState._ISCACHED + "=0", null, null);
+ if (cur.moveToFirst()) {
+ totalItems = cur.getInt(0);
+ } else {
+ totalItems = 0;
+ }
+ notifyProgressChanged(context.getString(R.string.TxtSyncingItemContent), finishedItems, totalItems);
+ for (int i = 0; i < FETCHING_THREAD_COUNT; i++) {
+ execService.execute(new FetchingProcess(this));
+ }
+ execService.shutdown();
+ try {
+ execService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+ } catch (final InterruptedException exception) {
+ exception.printStackTrace();
+ }
+ wrappers.clear();
+ }
+
+ public ItemWrapper getNextItemWrapper() {
+ synchronized (wrappers) {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkImageFetchingNetworkStatus(context, networkConfig)) {
+ return null;
+ }
+ while (!wrappers.isEmpty() && wrappers.get(0).isFinished()) {
+ finishedItems++;
+ notifyProgressChanged(context.getString(R.string.TxtSyncingItemContent), finishedItems, totalItems);
+ wrappers.remove(0);
+ }
+ if (!wrappers.isEmpty()) {
+ return wrappers.get(0);
+ }
+ final ContentResolver resolver = context.getContentResolver();
+ final Cursor cur;
+ cur = resolver.query(Uri.withAppendedPath(Item.CONTENT_URI, "limit/10"), ITEM_PROJECTION,
+ ItemState._ISCACHED + "=0", null, null);
+ for (cur.moveToFirst(); !cur.isAfterLast(); cur.moveToNext()) {
+ wrappers.add(new ItemWrapper(Item.fromCursor(cur)));
+ }
+ cur.close();
+ return wrappers.isEmpty() ? null : wrappers.get(0);
+ }
+ }
+ }
+
+ private class FetchingProcess implements Runnable {
+ final private FetchingHelper helper;
+
+ public FetchingProcess(final FetchingHelper helper) {
+ super();
+
+ this.helper = helper;
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ final ItemWrapper wrapper = helper.getNextItemWrapper();
+ if (wrapper == null) {
+ break;
+ }
+ final Pair psi = wrapper.downloadNextImage();
+ if (psi != null) {
+ final int picId = psi.first;
+ final String src = psi.second;
+ final String sDStateString = Environment.getExternalStorageState();
+ if (!sDStateString.equals(android.os.Environment.MEDIA_MOUNTED)) {
+ wrapper.onFinishImageFetching(picId, DOWNLOADING_STATUS_FILE_ERROR);
+ break;
+ } else {
+ final File file = new File(wrapper.getItem().getImageStoragePath(picId));
+ if (file.isDirectory()) {
+ DataUtils.deleteFile(file);
+ }
+ try {
+ if (!file.exists()) {
+ final URLConnection connection = new URL(src).openConnection();
+ connection.setConnectTimeout(30 * 1000);
+ connection.setReadTimeout(20 * 1000);
+ final InputStream input = connection.getInputStream();
+ final OutputStream out = new FileOutputStream(file);
+ final byte buff[] = new byte[CONTENT_IO_BUFFER_SIZE];
+ int len;
+ while ((len = input.read(buff)) != -1) {
+ out.write(buff, 0, len);
+ }
+ out.close();
+ try {
+ input.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+ wrapper.onFinishImageFetching(picId, DOWNLOADING_STATUS_SUCCEEDED);
+ } catch (final Exception exception) {
+ wrapper.onFinishImageFetching(picId, DOWNLOADING_STATUS_NETWORK_ERROR);
+ exception.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private class ItemWrapper {
+ final private Item item;
+ final private List imgList;
+ final private TagNode root;
+ private boolean hasFileError;
+ private int downloadedImageCount;
+
+ public ItemWrapper(final Item item) {
+ this.item = item;
+ this.imgList = new ArrayList();
+ this.root = new HtmlCleaner().clean(DataUtils.readFromFile(new File(item.getOriginalContentStoragePath())));
+ final Queue nodes = new LinkedList();
+ nodes.add(root);
+ while (!nodes.isEmpty()) {
+ final TagNode tag = nodes.poll();
+ final String tagName = tag.getName();
+ if ("img".equals(tagName)) {
+ final String src = tag.getAttributeByName("src");
+ if (src != null && (src.startsWith("http://") || src.startsWith("https://"))) {
+ imgList.add(tag);
+ } else {
+ tag.removeFromTree();
+ }
+ } else if (tag.hasChildren()) {
+ nodes.addAll(tag.getChildTagList());
+ }
+ }
+ this.hasFileError = false;
+ this.downloadedImageCount = 0;
+
+ if (isSucceeded()) {
+ markAsCached();
+ }
+ }
+
+ public synchronized Pair downloadNextImage() {
+ if (downloadedImageCount < imgList.size()) {
+ final TagNode ret = imgList.get(downloadedImageCount);
+ downloadedImageCount++;
+ return Pair.create(downloadedImageCount, ret.getAttributeByName("src"));
+ } else {
+ return null;
+ }
+ }
+
+ public Item getItem() {
+ return item;
+ }
+
+ public boolean isFinished() {
+ return (downloadedImageCount >= imgList.size());
+ }
+
+ public boolean isSucceeded() {
+ return (isFinished() && !hasFileError);
+ }
+
+ private void markAsCached() {
+ final CleanerProperties prop = new CleanerProperties();
+ prop.setTagInfoProvider(DefaultTagProvider.getInstance());
+ prop.setOmitXmlDeclaration(false);
+ final FastHtmlSerializer serializer = new FastHtmlSerializer(prop);
+ try {
+ final OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(
+ item.getFullContentStoragePath())), 8192);
+ serializer.writeToStream(root, out);
+ out.close();
+ final ContentResolver resolver = dataMgr.getContext().getContentResolver();
+ final ContentValues values = new ContentValues(1);
+ values.put(ItemState._ISCACHED, true);
+ resolver.update(Item.CONTENT_URI, values, Item._UID + "=?", new String[] { item.getUid() });
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ public synchronized void onFinishImageFetching(final int id, final int status) {
+ final TagNode ele = imgList.get(id - 1);
+ if (status == DOWNLOADING_STATUS_FILE_ERROR) {
+ hasFileError = true;
+ downloadedImageCount = imgList.size();
+ return;
+ }
+ ele.setAttribute("src", id + ".erss");
+ if (status != DOWNLOADING_STATUS_SUCCEEDED) {
+ final InputStream input = dataMgr.getContext().getResources().openRawResource(R.raw.no_such_picture);
+ try {
+ final FileOutputStream output = new FileOutputStream(item.getImageStoragePath(id));
+ DataUtils.streamTransfer(input, output);
+ } catch (final FileNotFoundException exception) {
+ exception.printStackTrace();
+ }
+ }
+ if (isSucceeded()) {
+ markAsCached();
+ }
+ }
+ }
+
+ private final static String[] ITEM_PROJECTION = { Item._UID };
+ final private static int DOWNLOADING_STATUS_SUCCEEDED = 0;
+ final private static int DOWNLOADING_STATUS_NETWORK_ERROR = 1;
+ final private static int DOWNLOADING_STATUS_FILE_ERROR = 2;
+ final private static int FETCHING_THREAD_COUNT = 5;
+
+ private static ItemContentDataSyncer instance;
+
+ private static synchronized void clearInstance() {
+ instance = null;
+ }
+
+ public static synchronized ItemContentDataSyncer getInstance(final DataMgr dataMgr, final int networkConfig) {
+ if (instance == null) {
+ instance = new ItemContentDataSyncer(dataMgr, networkConfig);
+ }
+ return instance;
+ }
+
+ private ItemContentDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ super(dataMgr, networkConfig);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else {
+ return (obj instanceof ItemContentDataSyncer);
+ }
+ }
+
+ @Override
+ protected void finishSyncing() {
+ clearInstance();
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ final SettingImagePrefetching sImgPrefetch = new SettingImagePrefetching(dataMgr);
+ if (sImgPrefetch.getData()) {
+ final FetchingHelper helper = new FetchingHelper();
+ helper.fetch();
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/ItemDataSyncer.java b/src/com/pursuer/reader/easyrss/network/ItemDataSyncer.java
new file mode 100644
index 0000000..8b46b1a
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/ItemDataSyncer.java
@@ -0,0 +1,201 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import android.content.Context;
+import android.util.Pair;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.DataUtils;
+import com.pursuer.reader.easyrss.data.Item;
+import com.pursuer.reader.easyrss.data.parser.ItemJSONParser;
+import com.pursuer.reader.easyrss.data.parser.OnItemRetrievedListener;
+import com.pursuer.reader.easyrss.data.readersetting.SettingMaxItems;
+import com.pursuer.reader.easyrss.network.url.StreamContentsURL;
+
+public class ItemDataSyncer extends AbsDataSyncer implements DataSyncerListener {
+ private class ItemListener implements OnItemRetrievedListener {
+ final private List items;
+ private String continuation;
+
+ public ItemListener() {
+ this.items = new LinkedList();
+ }
+
+ public String getContinuation() {
+ return continuation;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ @Override
+ public void onItemRetrieved(final Item item) throws IOException {
+ DataUtils.writeItemToFile(item);
+ items.add(item);
+ }
+
+ @Override
+ public void onListContinuationRetrieved(final String continuation) {
+ this.continuation = continuation;
+ }
+ }
+
+ final private long initTime;
+ final private long newestItemTime;
+ final private boolean isUnread;
+ final private String uid;
+ private Boolean isEnd;
+ private String continuation;
+
+ private static Map, ItemDataSyncer> instances = new HashMap, ItemDataSyncer>();
+
+ public static void clearInstance(final ItemDataSyncer instance) {
+ synchronized (instances) {
+ instances.remove(Pair.create(instance.getUid(), instance.isUnread()));
+ }
+ }
+
+ public static ItemDataSyncer getInstance(final DataMgr dataMgr, final int networkConfig, final String uid,
+ final long newestItemTime, final boolean isUnread) {
+ synchronized (instances) {
+ ItemDataSyncer ret = instances.get(Pair.create(uid, isUnread));
+ if (ret == null) {
+ ret = new ItemDataSyncer(dataMgr, networkConfig, uid, newestItemTime, isUnread);
+ instances.put(Pair.create(uid, isUnread), ret);
+ }
+ return ret;
+ }
+ }
+
+ private ItemDataSyncer(final DataMgr dataMgr, final int networkConfig, final String uid, final long newestItemTime,
+ final boolean isUnread) {
+ super(dataMgr, networkConfig);
+
+ this.initTime = System.currentTimeMillis();
+ this.uid = uid;
+ this.newestItemTime = newestItemTime;
+ this.isUnread = isUnread;
+ this.isEnd = false;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof ItemDataSyncer)) {
+ return false;
+ }
+ final ItemDataSyncer syncer = (ItemDataSyncer) obj;
+ return uid.equals(syncer.uid) && isUnread == syncer.isUnread;
+ }
+
+ @Override
+ protected void finishSyncing() {
+ if (System.currentTimeMillis() - initTime >= 1000 * 3600) {
+ clearInstance(this);
+ }
+ }
+
+ public long getNewestItemTime() {
+ return newestItemTime;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ public boolean isEnd() {
+ return isEnd;
+ }
+
+ public boolean isUnread() {
+ return isUnread;
+ }
+
+ @Override
+ public void onProgressChanged(final String text, final int progress, final int maxProgress) {
+ notifyProgressChanged(text, progress, maxProgress);
+ }
+
+ private void setEnd(final boolean isEnd) {
+ synchronized (this.isEnd) {
+ this.isEnd = isEnd;
+ }
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ if (isEnd()) {
+ return;
+ }
+ syncReadStatus();
+ syncItems();
+ syncItemContent();
+ }
+
+ private void syncItemContent() {
+ NetworkMgr.getInstance().startSyncItemContent();
+ }
+
+ private void syncItems() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ notifyProgressChanged(context.getString(R.string.TxtSyncingAllItems), -1, -1);
+
+ final InputStream stream = httpGetQueryStream(new StreamContentsURL(isHttpsConnection, uid, continuation,
+ newestItemTime, ITEM_LIST_QUERY_LIMIT, isUnread));
+ try {
+ final ItemJSONParser parser = new ItemJSONParser(stream);
+ final ItemListener listener = new ItemListener();
+ parser.parse(listener);
+ continuation = listener.getContinuation();
+ final List items = listener.getItems();
+ dataMgr.addItems(items);
+ if (items.size() < ITEM_LIST_QUERY_LIMIT) {
+ setEnd(true);
+ }
+ } catch (final JsonParseException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IllegalStateException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IOException exception) {
+ throw new DataSyncerException(exception);
+ } finally {
+ try {
+ stream.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+ dataMgr.removeOutdatedItemsWithLimit(new SettingMaxItems(dataMgr).getData());
+ }
+
+ private void syncReadStatus() throws DataSyncerException {
+ final TransactionDataSyncer syncer = TransactionDataSyncer.getInstance(dataMgr, networkConfig);
+ syncer.setListener(this);
+ syncer.sync();
+ syncer.setListener(null);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/ItemTagDataSyncer.java b/src/com/pursuer/reader/easyrss/network/ItemTagDataSyncer.java
new file mode 100644
index 0000000..339e391
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/ItemTagDataSyncer.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.network.url.EditItemTagURL;
+
+public class ItemTagDataSyncer extends AbsDataSyncer {
+ final private EditItemTagURL url;
+
+ public ItemTagDataSyncer(final DataMgr dataMgr, final int networkConfig, final String itemUid, final String tagUid,
+ final boolean isAdd) {
+ super(dataMgr, networkConfig);
+
+ this.url = new EditItemTagURL(isHttpsConnection, itemUid, tagUid, isAdd);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof ItemTagDataSyncer)) {
+ return false;
+ }
+ final ItemTagDataSyncer syncer = (ItemTagDataSyncer) obj;
+ return (url.equals(syncer.url));
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ final byte[] data = httpPostQueryByte(url);
+ if (!"OK".equals(new String(data))) {
+ throw new DataSyncerException("Sync failed");
+ }
+ }
+
+ @Override
+ protected void finishSyncing() {
+ /*
+ * TODO Empty method: Do nothing here. This class will only be called by
+ * TransactionDataSyncer.
+ */
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/LoginDataSyncer.java b/src/com/pursuer/reader/easyrss/network/LoginDataSyncer.java
new file mode 100644
index 0000000..512b517
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/LoginDataSyncer.java
@@ -0,0 +1,77 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncMethod;
+import com.pursuer.reader.easyrss.network.url.LoginURL;
+
+public class LoginDataSyncer extends AbsDataSyncer {
+ final private LoginURL url;
+
+ public LoginDataSyncer(final DataMgr dataMgr, final String username, final String password) {
+ super(dataMgr, SettingSyncMethod.SYNC_METHOD_NETWORK);
+ url = new LoginURL(username, password);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof LoginDataSyncer)) {
+ return false;
+ }
+ final LoginDataSyncer syncer = (LoginDataSyncer) obj;
+ return url.equals(syncer.url);
+ }
+
+ @Override
+ protected void finishSyncing() {
+ /*
+ * TODO Empty method: Do nothing here. This class will only be called
+ * once.
+ */
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ final BufferedReader input = new BufferedReader(httpPostQueryReader(url));
+ String auth = null;
+ try {
+ String line;
+ while ((line = input.readLine()) != null) {
+ if (line.indexOf("Auth=") == 0) {
+ auth = line.substring("Auth=".length());
+ break;
+ }
+ }
+ } catch (IOException e) {
+ throw new DataSyncerException(e);
+ } finally {
+ try {
+ input.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ if (auth == null) {
+ throw new DataSyncerException("Invalid auth");
+ } else {
+ dataMgr.updateSetting(new Setting(Setting.SETTING_AUTH, auth));
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/NetworkClient.java b/src/com/pursuer/reader/easyrss/network/NetworkClient.java
new file mode 100644
index 0000000..a81b6ff
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/NetworkClient.java
@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.apache.http.HttpStatus;
+
+import com.pursuer.reader.easyrss.account.ReaderAccountMgr;
+
+public class NetworkClient {
+ public class NetworkException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public NetworkException(final String message) {
+ super(message);
+ }
+
+ public NetworkException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public NetworkException(final Throwable cause) {
+ super(cause);
+ }
+ }
+
+ private static NetworkClient instance = null;
+
+ public synchronized static NetworkClient getInstance() {
+ if (instance == null) {
+ instance = new NetworkClient();
+ }
+ return instance;
+ }
+
+ private String auth;
+
+ private NetworkClient() {
+ // TODO empty method
+ }
+
+ public byte[] doGetByte(final String url) throws Exception {
+ final InputStream stream = doGetStream(url);
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final byte[] data = new byte[8192];
+ int len;
+ while ((len = stream.read(data, 0, 8192)) != -1) {
+ output.write(data, 0, len);
+ }
+ final byte[] ret = output.toByteArray();
+ output.close();
+ return ret;
+ }
+
+ public InputStream doGetStream(final String url) throws Exception {
+ final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setConnectTimeout(40 * 1000);
+ conn.setReadTimeout(30 * 1000);
+ conn.setRequestMethod("GET");
+ if (auth != null) {
+ conn.setRequestProperty("Authorization", "GoogleLogin auth=" + auth);
+ }
+ try {
+ final int resStatus = conn.getResponseCode();
+ if (resStatus == HttpStatus.SC_UNAUTHORIZED) {
+ ReaderAccountMgr.getInstance().invalidateAuth();
+ }
+ if (resStatus != HttpStatus.SC_OK) {
+ throw new NetworkException("Invalid HTTP status " + resStatus + ": " + url + ".");
+ }
+ } catch (final Exception exception) {
+ if (exception.getMessage() != null && exception.getMessage().contains("authentication")) {
+ ReaderAccountMgr.getInstance().invalidateAuth();
+ }
+ throw exception;
+ }
+ return conn.getInputStream();
+ }
+
+ public byte[] doPostByte(final String url, final String params) throws Exception {
+ final InputStream stream = doPostStream(url, params);
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final byte[] data = new byte[8192];
+ int len;
+ while ((len = stream.read(data, 0, 8192)) != -1) {
+ output.write(data, 0, len);
+ }
+ final byte[] ret = output.toByteArray();
+ output.close();
+ return ret;
+ }
+
+ public InputStream doPostStream(final String url, final String params) throws IOException, NetworkException {
+ final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setConnectTimeout(40 * 1000);
+ conn.setReadTimeout(30 * 1000);
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ if (auth != null) {
+ conn.setRequestProperty("Authorization", "GoogleLogin auth=" + auth);
+ }
+ conn.setDoInput(true);
+ conn.setDoOutput(true);
+ final OutputStream output = conn.getOutputStream();
+ output.write(params.getBytes());
+ output.flush();
+ output.close();
+
+ conn.connect();
+ try {
+ final int resStatus = conn.getResponseCode();
+ if (resStatus == HttpStatus.SC_UNAUTHORIZED) {
+ ReaderAccountMgr.getInstance().invalidateAuth();
+ }
+ if (resStatus != HttpStatus.SC_OK) {
+ throw new NetworkException("Invalid HTTP status " + resStatus + ": " + url + ".");
+ }
+ } catch (final IOException exception) {
+ if (exception.getMessage() != null && exception.getMessage().contains("authentication")) {
+ ReaderAccountMgr.getInstance().invalidateAuth();
+ }
+ throw exception;
+ }
+ return conn.getInputStream();
+ }
+
+ public String getAuth() {
+ return auth;
+ }
+
+ public void setAuth(final String auth) {
+ this.auth = auth;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/NetworkListener.java b/src/com/pursuer/reader/easyrss/network/NetworkListener.java
new file mode 100644
index 0000000..8572bd8
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/NetworkListener.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+public interface NetworkListener {
+ void onDataSyncerProgressChanged(String text, int progress, int maxProgress);
+
+ void onLogin(boolean succeeded);
+
+ void onSyncFinished(String syncerType, boolean succeeded);
+
+ void onSyncStarted(String syncerType);
+}
diff --git a/src/com/pursuer/reader/easyrss/network/NetworkMgr.java b/src/com/pursuer/reader/easyrss/network/NetworkMgr.java
new file mode 100644
index 0000000..8d93d9a
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/NetworkMgr.java
@@ -0,0 +1,308 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.util.ConcurrentModificationException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.readersetting.SettingImageFetching;
+import com.pursuer.reader.easyrss.data.readersetting.SettingImmediateStateSyncing;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncMethod;
+import com.pursuer.reader.easyrss.network.AbsDataSyncer.DataSyncerException;
+
+final public class NetworkMgr implements DataSyncerListener {
+ final static private String KEY_MAX_PROGRESS = "maxProgress";
+ final static private String KEY_PROGRESS = "progress";
+ final static private String KEY_SYNCER_TYPE = "syncerType";
+ final static private String KEY_SUCCEEDED = "succeeded";
+ final static private String KEY_TEXT = "text";
+
+ final static private int MSG_ON_DATA_SYNCER_PROGRESS_CHANGED = 0;
+ final static private int MSG_ON_LOGIN = 1;
+ final static private int MSG_SYNC_FINISHED = 2;
+ final static private int MSG_SYNC_STARTED = 3;
+
+ private class ItemContentSyncThread extends Thread {
+ private int networkConfig;
+
+ public synchronized int getNetworkConfig() {
+ return networkConfig;
+ }
+
+ public synchronized void notifySyncStarted() {
+ notifyAll();
+ }
+
+ @Override
+ public void run() {
+ final DataMgr dataMgr = DataMgr.getInstance();
+ while (true) {
+ try {
+ waitForSync();
+ } catch (final InterruptedException exception) {
+ exception.printStackTrace();
+ }
+ final ItemContentDataSyncer syncer = ItemContentDataSyncer.getInstance(dataMgr, getNetworkConfig());
+ NetworkMgr.this.notifySyncStarted(syncer);
+ syncer.setListener(NetworkMgr.this);
+ boolean succeeded;
+ try {
+ syncer.sync();
+ succeeded = true;
+ } catch (final DataSyncerException exception) {
+ exception.printStackTrace();
+ succeeded = false;
+ }
+ syncer.setListener(null);
+ notifySyncFinished(syncer, succeeded);
+ }
+ }
+
+ public synchronized void setNetworkConfig(final int networkConfig) {
+ this.networkConfig = networkConfig;
+ }
+
+ public synchronized void waitForSync() throws InterruptedException {
+ wait();
+ }
+ };
+
+ private class SyncThread extends Thread {
+ @Override
+ public void run() {
+ while (true) {
+ final AbsDataSyncer syncer;
+ synchronized (syncers) {
+ while (syncers.isEmpty()) {
+ try {
+ syncers.wait();
+ } catch (final InterruptedException exception) {
+ exception.printStackTrace();
+ }
+ }
+ syncer = syncers.remove();
+ }
+ notifySyncStarted(syncer);
+ syncer.setListener(NetworkMgr.this);
+ boolean succeeded;
+ try {
+ syncer.sync();
+ succeeded = true;
+ } catch (final Exception exception) {
+ notifyOnDataSyncerProgressChanged(
+ context.getString(R.string.TxtSyncFailed) + ": " + exception.getMessage() + ".", -1, -1);
+ succeeded = false;
+ }
+ syncer.setPending(false);
+ syncer.setListener(null);
+ notifySyncFinished(syncer, succeeded);
+ }
+ }
+ }
+
+ private static NetworkMgr instance = null;
+
+ public static synchronized void init(final Context context) {
+ if (instance == null) {
+ instance = new NetworkMgr(context);
+ }
+ }
+
+ public static NetworkMgr getInstance() {
+ return instance;
+ }
+
+ final private Context context;
+ final private Queue syncers;
+ final private List listeners;
+ final private ItemContentSyncThread itemContentSyncThread;
+ final private SyncThread syncThread;
+ final private Handler handler;
+ private Thread loginThread;
+
+ @SuppressLint("HandlerLeak")
+ private NetworkMgr(final Context context) {
+ this.context = context;
+ this.syncers = new LinkedList();
+ this.listeners = new LinkedList();
+ this.syncThread = new SyncThread();
+ syncThread.setPriority(Thread.MIN_PRIORITY);
+ syncThread.start();
+ this.itemContentSyncThread = new ItemContentSyncThread();
+ itemContentSyncThread.setPriority(Thread.MIN_PRIORITY);
+ itemContentSyncThread.start();
+ this.handler = new Handler() {
+ @Override
+ public void handleMessage(final Message msg) {
+ final Bundle bundle = msg.getData();
+ switch (msg.what) {
+ case MSG_ON_DATA_SYNCER_PROGRESS_CHANGED: {
+ final String text = bundle.getString(KEY_TEXT);
+ final int progress = bundle.getInt(KEY_PROGRESS);
+ final int maxProgress = bundle.getInt(KEY_MAX_PROGRESS);
+ try {
+ for (final NetworkListener listener : listeners) {
+ listener.onDataSyncerProgressChanged(text, progress, maxProgress);
+ }
+ } catch (final ConcurrentModificationException exception) {
+ exception.printStackTrace();
+ }
+ }
+ break;
+ case MSG_ON_LOGIN: {
+ final boolean succeeded = bundle.getBoolean(KEY_SUCCEEDED);
+ try {
+ for (final NetworkListener listener : listeners) {
+ listener.onLogin(succeeded);
+ }
+ } catch (final ConcurrentModificationException exception) {
+ exception.printStackTrace();
+ }
+ }
+ break;
+ case MSG_SYNC_FINISHED: {
+ final String syncerType = bundle.getString(KEY_SYNCER_TYPE);
+ final boolean succeeded = bundle.getBoolean(KEY_SUCCEEDED);
+ try {
+ for (final NetworkListener listener : listeners) {
+ listener.onSyncFinished(syncerType, succeeded);
+ }
+ } catch (final ConcurrentModificationException exception) {
+ exception.printStackTrace();
+ }
+ }
+ break;
+ case MSG_SYNC_STARTED: {
+ final String syncerType = bundle.getString(KEY_SYNCER_TYPE);
+ try {
+ for (final NetworkListener listener : listeners) {
+ listener.onSyncStarted(syncerType);
+ }
+ } catch (final ConcurrentModificationException exception) {
+ exception.printStackTrace();
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ }
+
+ /*
+ * This method need to be called in MAIN thread.
+ */
+ public void addListener(final NetworkListener listener) {
+ listeners.add(listener);
+ }
+
+ public void login(final String user, final String pass) {
+ if (loginThread != null && loginThread.isAlive()) {
+ return;
+ }
+ loginThread = new Thread() {
+ @Override
+ public void run() {
+ final LoginDataSyncer syncer = new LoginDataSyncer(DataMgr.getInstance(), user, pass);
+ try {
+ syncer.sync();
+ notifyOnLogin(true);
+ } catch (final DataSyncerException exception) {
+ notifyOnLogin(false);
+ exception.printStackTrace();
+ }
+ }
+ };
+ loginThread.start();
+ }
+
+ private void notifyOnDataSyncerProgressChanged(final String text, final int progress, final int maxProgress) {
+ final Message msg = handler.obtainMessage(MSG_ON_DATA_SYNCER_PROGRESS_CHANGED);
+ final Bundle bundle = new Bundle();
+ bundle.putString(KEY_TEXT, text);
+ bundle.putInt(KEY_PROGRESS, progress);
+ bundle.putInt(KEY_MAX_PROGRESS, maxProgress);
+ msg.setData(bundle);
+ handler.sendMessage(msg);
+ }
+
+ private void notifyOnLogin(final boolean succeeded) {
+ final Message msg = handler.obtainMessage(MSG_ON_LOGIN);
+ final Bundle bundle = new Bundle();
+ bundle.putBoolean(KEY_SUCCEEDED, succeeded);
+ msg.setData(bundle);
+ handler.sendMessage(msg);
+ }
+
+ private void notifySyncFinished(final AbsDataSyncer syncer, final boolean succeeded) {
+ final Message msg = handler.obtainMessage(MSG_SYNC_FINISHED);
+ final Bundle bundle = new Bundle();
+ bundle.putString(KEY_SYNCER_TYPE, syncer.getClass().getName());
+ bundle.putBoolean(KEY_SUCCEEDED, succeeded);
+ msg.setData(bundle);
+ handler.sendMessage(msg);
+ }
+
+ private void notifySyncStarted(final AbsDataSyncer syncer) {
+ final Message msg = handler.obtainMessage(MSG_SYNC_STARTED);
+ final Bundle bundle = new Bundle();
+ bundle.putString(KEY_SYNCER_TYPE, syncer.getClass().getName());
+ msg.setData(bundle);
+ handler.sendMessage(msg);
+ }
+
+ @Override
+ public void onProgressChanged(final String text, final int progress, final int maxProgress) {
+ notifyOnDataSyncerProgressChanged(text, progress, maxProgress);
+ }
+
+ /*
+ * This method need to be called in MAIN thread.
+ */
+ public void removeListener(final NetworkListener listener) {
+ listeners.remove(listener);
+ }
+
+ public void startImmediateItemStateSyncing() {
+ final SettingImmediateStateSyncing sStateSyncing = new SettingImmediateStateSyncing(DataMgr.getInstance());
+ if (sStateSyncing.getData()) {
+ startSync(TransactionDataSyncer.getInstance(DataMgr.getInstance(), SettingSyncMethod.SYNC_METHOD_MANUAL));
+ }
+ }
+
+ public void startSync(final AbsDataSyncer syncer) {
+ if (!syncer.setEnterPending()) {
+ return;
+ }
+ synchronized (syncers) {
+ syncers.add(syncer);
+ syncers.notify();
+ }
+ }
+
+ public void startSyncItemContent() {
+ final SettingImageFetching sImageFetch = new SettingImageFetching(DataMgr.getInstance());
+ itemContentSyncThread.setNetworkConfig(sImageFetch.getData());
+ itemContentSyncThread.notifySyncStarted();
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/NetworkUtils.java b/src/com/pursuer/reader/easyrss/network/NetworkUtils.java
new file mode 100644
index 0000000..a4e6c62
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/NetworkUtils.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import com.pursuer.reader.easyrss.SyncingReceiver;
+import com.pursuer.reader.easyrss.Utils;
+import com.pursuer.reader.easyrss.account.ReaderAccountMgr;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.GoogleAnalyticsMgr;
+import com.pursuer.reader.easyrss.data.readersetting.SettingImageFetching;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncMethod;
+
+final public class NetworkUtils {
+ public static boolean checkImageFetchingNetworkStatus(final Context context, final int config) {
+ final ConnectivityManager connMgr = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ final NetworkInfo info = connMgr.getActiveNetworkInfo();
+ if (info == null || !info.isConnected()) {
+ return false;
+ }
+ if (config == SettingImageFetching.FETCH_METHOD_DISABLED) {
+ return false;
+ } else if (config == SettingImageFetching.FETCH_METHOD_WIFI) {
+ final android.net.NetworkInfo.State wifi = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState();
+ return (wifi == NetworkInfo.State.CONNECTED);
+ } else if (config == SettingImageFetching.FETCH_METHOD_NETWORK) {
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean checkSyncingNetworkStatus(final Context context, final int config) {
+ final ConnectivityManager connMgr = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ final NetworkInfo info = connMgr.getActiveNetworkInfo();
+ if (info == null || !info.isConnected()) {
+ return false;
+ }
+ if (config == SettingSyncMethod.SYNC_METHOD_MANUAL) {
+ return true;
+ } else if (config == SettingSyncMethod.SYNC_METHOD_WIFI) {
+ final android.net.NetworkInfo.State wifi = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState();
+ return (wifi == NetworkInfo.State.CONNECTED);
+ } else if (config == SettingSyncMethod.SYNC_METHOD_NETWORK) {
+ return true;
+ }
+ return false;
+ }
+
+ public static void doGlobalSyncing(final Context context, final int syncingMethod) {
+ Utils.initManagers(context);
+ if (ReaderAccountMgr.getInstance().hasAccount() && !GlobalItemDataSyncer.hasInstance()
+ && !SubscriptionDataSyncer.hasInstance() && !TagDataSyncer.hasInstance()) {
+ final NetworkMgr nMgr = NetworkMgr.getInstance();
+ final DataMgr dataMgr = DataMgr.getInstance();
+ nMgr.startSync(TagDataSyncer.getInstance(dataMgr, syncingMethod));
+ nMgr.startSync(SubscriptionDataSyncer.getInstance(dataMgr, syncingMethod));
+ nMgr.startSync(GlobalItemDataSyncer.getInstance(dataMgr, syncingMethod));
+ GoogleAnalyticsMgr.getInstance().dispatch();
+ }
+ }
+
+ public static void startSyncingTimer(final Context context) {
+ final AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ final Intent intent = new Intent(context, SyncingReceiver.class);
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ alarmMgr.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(),
+ AlarmManager.INTERVAL_FIFTEEN_MINUTES, pendingIntent);
+ }
+
+ private NetworkUtils() {
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/SubscriptionDataSyncer.java b/src/com/pursuer/reader/easyrss/network/SubscriptionDataSyncer.java
new file mode 100644
index 0000000..77d533c
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/SubscriptionDataSyncer.java
@@ -0,0 +1,172 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.GoogleAnalyticsMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+import com.pursuer.reader.easyrss.data.Subscription;
+import com.pursuer.reader.easyrss.data.parser.OnSubscriptionRetrievedListener;
+import com.pursuer.reader.easyrss.data.parser.SubscriptionJSONParser;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncInterval;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncMethod;
+import com.pursuer.reader.easyrss.network.url.SubscriptionIconUrl;
+import com.pursuer.reader.easyrss.network.url.SubscriptionListURL;
+
+public class SubscriptionDataSyncer extends AbsDataSyncer {
+ private class SyncerSubscriptionListener implements OnSubscriptionRetrievedListener {
+ final private List subscriptions;
+
+ public SyncerSubscriptionListener() {
+ this.subscriptions = new LinkedList();
+ }
+
+ public List getSubscriptions() {
+ return subscriptions;
+ }
+
+ public void onSubscriptionRetrieved(final Subscription sub) {
+ subscriptions.add(sub);
+ }
+ }
+
+ private static SubscriptionDataSyncer instance;
+
+ private static synchronized void clearInstance() {
+ instance = null;
+ }
+
+ public static synchronized SubscriptionDataSyncer getInstance(final DataMgr dataMgr, final int networkConfig) {
+ if (instance == null) {
+ instance = new SubscriptionDataSyncer(dataMgr, networkConfig);
+ }
+ return instance;
+ }
+
+ public static synchronized boolean hasInstance() {
+ return instance != null;
+ }
+
+ private SubscriptionDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ super(dataMgr, networkConfig);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else {
+ return (obj instanceof SubscriptionDataSyncer);
+ }
+ }
+
+ @Override
+ protected void finishSyncing() {
+ clearInstance();
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ final String sExpTime = dataMgr.getSettingByName(Setting.SETTING_SUBSCRIPTION_LIST_EXPIRE_TIME);
+ if (networkConfig != SettingSyncMethod.SYNC_METHOD_MANUAL
+ && sExpTime != null
+ && Long.valueOf(sExpTime) + new SettingSyncInterval(dataMgr).toSeconds() * 1000 - 10 * 60 * 1000 > System
+ .currentTimeMillis()) {
+ return;
+ }
+
+ syncSubscriptions();
+ syncSubscriptionIcons();
+
+ dataMgr.updateSetting(new Setting(Setting.SETTING_SUBSCRIPTION_LIST_EXPIRE_TIME, System.currentTimeMillis()));
+ }
+
+ private void syncSubscriptionIcons() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ final ContentResolver resolver = context.getContentResolver();
+ final Cursor cur = resolver.query(Subscription.CONTENT_URI, new String[] { Subscription._UID,
+ Subscription._ICON, Subscription._URL }, null, null, null);
+ for (cur.moveToFirst(); !cur.isAfterLast(); cur.moveToNext()) {
+ final String uid = cur.getString(0);
+ final byte[] data = cur.getBlob(1);
+ final String subUrl = cur.getString(2);
+ if (subUrl != null && data == null) {
+ final SubscriptionIconUrl fetchUrl = new SubscriptionIconUrl(isHttpsConnection, subUrl);
+ try {
+ final byte[] iconData = httpGetQueryByte(fetchUrl);
+ final Bitmap icon = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
+ final int size = icon.getWidth() * icon.getHeight() * 2;
+ final ByteArrayOutputStream output = new ByteArrayOutputStream(size);
+ icon.compress(Bitmap.CompressFormat.PNG, 100, output);
+ output.flush();
+ output.close();
+ dataMgr.updateSubscriptionIconByUid(uid, output.toByteArray());
+ } catch (final IOException exception) {
+ cur.close();
+ throw new DataSyncerException(exception);
+ }
+ }
+ }
+ cur.close();
+ }
+
+ private void syncSubscriptions() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ notifyProgressChanged(context.getString(R.string.TxtSyncingSubscriptions), -1, -1);
+
+ final InputStream stream = httpGetQueryStream(new SubscriptionListURL(isHttpsConnection));
+ final SubscriptionJSONParser parser = new SubscriptionJSONParser(stream);
+ final long curTime = System.currentTimeMillis();
+ try {
+ final SyncerSubscriptionListener listener = new SyncerSubscriptionListener();
+ parser.parse(listener);
+ dataMgr.addSubscriptions(listener.getSubscriptions());
+ final int sAll = listener.getSubscriptions().size();
+ final int sAllRange = sAll / 10 * 10;
+ GoogleAnalyticsMgr.getInstance().trackEvent(GoogleAnalyticsMgr.CATEGORY_SYNCING,
+ GoogleAnalyticsMgr.ACTION_SYNCING_SUBSCRIPTIONS, sAllRange + "-" + (sAllRange + 9), sAll);
+ } catch (final JsonParseException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IllegalStateException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IOException exception) {
+ throw new DataSyncerException(exception);
+ } finally {
+ try {
+ stream.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+ dataMgr.removeOutdatedSubscriptions(curTime);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/TagDataSyncer.java b/src/com/pursuer/reader/easyrss/network/TagDataSyncer.java
new file mode 100644
index 0000000..d0bd887
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/TagDataSyncer.java
@@ -0,0 +1,133 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.Context;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.GoogleAnalyticsMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+import com.pursuer.reader.easyrss.data.Tag;
+import com.pursuer.reader.easyrss.data.parser.OnTagRetrievedListener;
+import com.pursuer.reader.easyrss.data.parser.TagJSONParser;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncInterval;
+import com.pursuer.reader.easyrss.data.readersetting.SettingSyncMethod;
+import com.pursuer.reader.easyrss.network.url.TagListURL;
+
+public class TagDataSyncer extends AbsDataSyncer {
+ private class TagListener implements OnTagRetrievedListener {
+ final private List tags;
+
+ public TagListener() {
+ tags = new LinkedList();
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public void onTagRetrieved(final Tag tag) {
+ tags.add(tag);
+ }
+ }
+
+ private static TagDataSyncer instance;
+
+ private static synchronized void clearInstance() {
+ instance = null;
+ }
+
+ public static synchronized TagDataSyncer getInstance(final DataMgr dataMgr, final int networkConfig) {
+ if (instance == null) {
+ instance = new TagDataSyncer(dataMgr, networkConfig);
+ }
+ return instance;
+ }
+
+ public static synchronized boolean hasInstance() {
+ return instance != null;
+ }
+
+ private TagDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ super(dataMgr, networkConfig);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else {
+ return (obj instanceof TagDataSyncer);
+ }
+ }
+
+ @Override
+ protected void finishSyncing() {
+ clearInstance();
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ final String sExpTime = dataMgr.getSettingByName(Setting.SETTING_TAG_LIST_EXPIRE_TIME);
+ if (networkConfig != SettingSyncMethod.SYNC_METHOD_MANUAL
+ && sExpTime != null
+ && Long.valueOf(sExpTime) + new SettingSyncInterval(dataMgr).toSeconds() * 1000 - 10 * 60 * 1000 > System
+ .currentTimeMillis()) {
+ return;
+ }
+
+ syncTags();
+
+ dataMgr.updateSetting(new Setting(Setting.SETTING_TAG_LIST_EXPIRE_TIME, System.currentTimeMillis()));
+ }
+
+ private void syncTags() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ notifyProgressChanged(context.getString(R.string.TxtSyncingTags), -1, -1);
+
+ final InputStream stream = httpGetQueryStream(new TagListURL(isHttpsConnection));
+ final TagJSONParser parser = new TagJSONParser(stream);
+ final long curTime = System.currentTimeMillis();
+ try {
+ final TagListener listener = new TagListener();
+ parser.parse(listener);
+ dataMgr.addTags(listener.getTags());
+ final int tAll = listener.getTags().size();
+ final int tAllRange = tAll / 10 * 10;
+ GoogleAnalyticsMgr.getInstance().trackEvent(GoogleAnalyticsMgr.CATEGORY_SYNCING,
+ GoogleAnalyticsMgr.ACTION_SYNCING_TAGS, tAllRange + "-" + (tAllRange + 9), tAll);
+ } catch (final JsonParseException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IllegalStateException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IOException exception) {
+ throw new DataSyncerException(exception);
+ } finally {
+ try {
+ stream.close();
+ } catch (final IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+ dataMgr.removeOutdatedTags(curTime);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/TokenDataSyncer.java b/src/com/pursuer/reader/easyrss/network/TokenDataSyncer.java
new file mode 100644
index 0000000..cf3c421
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/TokenDataSyncer.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+import com.pursuer.reader.easyrss.network.url.TokenURL;
+
+public class TokenDataSyncer extends AbsDataSyncer {
+ public TokenDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ super(dataMgr, networkConfig);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else {
+ return (obj instanceof TokenDataSyncer);
+ }
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ final String sExpTime = dataMgr.getSettingByName(Setting.SETTING_TOKEN_EXPIRE_TIME);
+ final String token = dataMgr.getSettingByName(Setting.SETTING_TOKEN);
+ if (token != null && sExpTime != null && Long.valueOf(sExpTime) >= System.currentTimeMillis()) {
+ return;
+ }
+
+ syncToken();
+
+ dataMgr.updateSetting(new Setting(Setting.SETTING_TOKEN_EXPIRE_TIME, String.valueOf(System.currentTimeMillis()
+ + TOKEN_EXPIRE_TIME)));
+ }
+
+ private void syncToken() throws DataSyncerException {
+ final byte[] data = httpGetQueryByte(new TokenURL(isHttpsConnection));
+ dataMgr.updateSetting(new Setting(Setting.SETTING_TOKEN, new String(data)));
+ }
+
+ @Override
+ protected void finishSyncing() {
+ /*
+ * TODO Empty method: Do nothing here. This class will only be called by
+ * TransactionDataSyncer.
+ */
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/TransactionDataSyncer.java b/src/com/pursuer/reader/easyrss/network/TransactionDataSyncer.java
new file mode 100644
index 0000000..9d080ee
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/TransactionDataSyncer.java
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Transaction;
+
+public class TransactionDataSyncer extends AbsDataSyncer {
+ private class SyncingThread implements Runnable {
+ private Exception exception;
+
+ public Exception getException() {
+ return exception;
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ final Transaction trans = getNextTransaction();
+ if (trans == null) {
+ return;
+ }
+ String tag = "";
+ boolean isAdd = false;
+ switch (trans.getType()) {
+ case Transaction.TYPE_SET_READ:
+ tag = "user/-/state/com.google/read";
+ isAdd = true;
+ break;
+ case Transaction.TYPE_REMOVE_READ:
+ tag = "user/-/state/com.google/read";
+ isAdd = false;
+ break;
+ case Transaction.TYPE_SET_STARRED:
+ tag = "user/-/state/com.google/starred";
+ isAdd = true;
+ break;
+ case Transaction.TYPE_REMOVE_STARRED:
+ tag = "user/-/state/com.google/starred";
+ isAdd = false;
+ break;
+ default:
+ }
+ if (tag.length() > 0) {
+ final ItemTagDataSyncer syncer = new ItemTagDataSyncer(dataMgr, networkConfig, trans.getUid(),
+ tag, isAdd);
+ syncer.sync();
+ }
+ dataMgr.removeTransactionById(trans.getId());
+ } catch (final DataSyncerException exception) {
+ this.exception = exception;
+ return;
+ }
+ }
+ }
+ }
+
+ final static int SYNCING_THREAD_COUNT = 5;
+
+ final private List transactions;
+ private int progress;
+
+ private static TransactionDataSyncer instance;
+
+ public static synchronized TransactionDataSyncer getInstance(final DataMgr dataMgr, final int networkConfig) {
+ if (instance == null) {
+ instance = new TransactionDataSyncer(dataMgr, networkConfig);
+ }
+ return instance;
+ }
+
+ private TransactionDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ super(dataMgr, networkConfig);
+ this.transactions = new ArrayList();
+ }
+
+ @Override
+ protected void finishSyncing() {
+ // TODO nothing needed
+ }
+
+ private Transaction getNextTransaction() throws DataSyncerException {
+ synchronized (transactions) {
+ final Context context = dataMgr.getContext();
+ notifyProgressChanged(context.getString(R.string.TxtSyncingItemStatus), progress, transactions.size());
+ if (progress < transactions.size()) {
+ final TokenDataSyncer tSyncer = new TokenDataSyncer(dataMgr, networkConfig);
+ tSyncer.sync();
+ final Transaction ret = transactions.get(progress);
+ progress++;
+ return ret;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ syncTransactions();
+ }
+
+ private void syncTransactions() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ final ContentResolver resolver = context.getContentResolver();
+ while (true) {
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ progress = 0;
+ transactions.clear();
+ final Cursor cur = resolver.query(Transaction.CONTENT_URI, null, null, null, Transaction._ID + " LIMIT 50");
+ for (cur.moveToFirst(); !cur.isAfterLast(); cur.moveToNext()) {
+ transactions.add(Transaction.fromCursor(cur));
+ }
+ cur.close();
+ if (!transactions.isEmpty()) {
+ final int tCount = Math.min(SYNCING_THREAD_COUNT, transactions.size());
+ final List syncingThreads = new ArrayList(tCount);
+ final ExecutorService execService = Executors.newFixedThreadPool(tCount, new ThreadFactory() {
+ @Override
+ public Thread newThread(final Runnable runnable) {
+ final Thread thread = new Thread(runnable);
+ thread.setPriority(Thread.MIN_PRIORITY);
+ return thread;
+ }
+ });
+ for (int i = 0; i < tCount; i++) {
+ final SyncingThread thread = new SyncingThread();
+ syncingThreads.add(thread);
+ execService.execute(thread);
+ }
+ execService.shutdown();
+ try {
+ execService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+ for (final SyncingThread thread : syncingThreads) {
+ if (thread.getException() != null) {
+ throw new DataSyncerException(thread.getException());
+ }
+ }
+ } catch (final InterruptedException exception) {
+ exception.printStackTrace();
+ }
+ } else {
+ break;
+ }
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/UnreadCountDataSyncer.java b/src/com/pursuer/reader/easyrss/network/UnreadCountDataSyncer.java
new file mode 100644
index 0000000..c0c27da
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/UnreadCountDataSyncer.java
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.Context;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.GoogleAnalyticsMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+import com.pursuer.reader.easyrss.data.UnreadCount;
+import com.pursuer.reader.easyrss.data.parser.OnUnreadCountRetrievedListener;
+import com.pursuer.reader.easyrss.data.parser.UnreadCountJSONParser;
+import com.pursuer.reader.easyrss.network.url.UnreadCountURL;
+
+public class UnreadCountDataSyncer extends AbsDataSyncer {
+ private class UnreadCountListener implements OnUnreadCountRetrievedListener {
+ final private List unreadCounts;
+
+ public UnreadCountListener() {
+ unreadCounts = new LinkedList();
+ }
+
+ public List getUnreadCounts() {
+ return unreadCounts;
+ }
+
+ @Override
+ public void onUnreadCountRetrieved(final UnreadCount count) {
+ unreadCounts.add(count);
+ }
+ }
+
+ public UnreadCountDataSyncer(final DataMgr dataMgr, final int networkConfig) {
+ super(dataMgr, networkConfig);
+ }
+
+ @Override
+ protected void finishSyncing() {
+ /*
+ * TODO Empty method: Do nothing here. The instance will be called by
+ * other syncers.
+ */
+ }
+
+ @Override
+ public void startSyncing() throws DataSyncerException {
+ syncUnreadCount();
+ }
+
+ private void syncUnreadCount() throws DataSyncerException {
+ final Context context = dataMgr.getContext();
+ if (!NetworkUtils.checkSyncingNetworkStatus(context, networkConfig)) {
+ return;
+ }
+ final byte[] content = httpGetQueryByte(new UnreadCountURL(isHttpsConnection));
+ final long curTime = System.currentTimeMillis();
+ try {
+ final UnreadCountJSONParser parser = new UnreadCountJSONParser(content);
+ final UnreadCountListener listener = new UnreadCountListener();
+ parser.parse(listener);
+ dataMgr.updateUnreadCounts(listener.getUnreadCounts());
+ } catch (final JsonParseException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IllegalStateException exception) {
+ throw new DataSyncerException(exception);
+ } catch (final IOException exception) {
+ throw new DataSyncerException(exception);
+ }
+ dataMgr.removeOutdatedUnreadCounts(curTime);
+ final String sUpdTime = dataMgr.getSettingByName(Setting.SETTING_GLOBAL_ITEM_UPDATE_TIME);
+ final long updTime = (sUpdTime == null) ? 0 : Long.valueOf(sUpdTime);
+ if (updTime < curTime) {
+ dataMgr.updateSetting(new Setting(Setting.SETTING_GLOBAL_ITEM_UNREAD_COUNT, "0"));
+ }
+ final int ucAll = dataMgr.getGlobalUnreadCount();
+ final int ucAllRange = ucAll / 100 * 100;
+ GoogleAnalyticsMgr.getInstance().trackEvent(GoogleAnalyticsMgr.CATEGORY_SYNCING,
+ GoogleAnalyticsMgr.ACTION_SYNCING_UNREADCOUNTS, ucAllRange + "-" + (ucAllRange + 99), ucAll);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/AbsURL.java b/src/com/pursuer/reader/easyrss/network/url/AbsURL.java
new file mode 100644
index 0000000..8d70e44
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/AbsURL.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.message.BasicNameValuePair;
+
+import android.net.Uri;
+
+public abstract class AbsURL {
+ protected static final String URL_GOOGLE_READER_BASE = "www.google.com/reader";
+ protected static final String URL_GOOGLE_READER_API = URL_GOOGLE_READER_BASE + "/api/0";
+
+ protected static String appendParams(final String str, final String param) {
+ if (str.length() == 0 || str.endsWith("&")) {
+ return str + param;
+ } else {
+ return str + "&" + param;
+ }
+ }
+
+ protected static String appendURL(String str, String param) {
+ if (param.startsWith("/")) {
+ param = param.substring(1);
+ }
+ if (!str.endsWith("/")) {
+ str += "/";
+ }
+ return str + param;
+ }
+
+ private static String paramsToString(final List params) {
+ String ret = "";
+ for (final NameValuePair p : params) {
+ ret = appendParams(ret, p.getName() + "=" + p.getValue());
+ }
+ return ret;
+ }
+
+ final private transient List params;
+ final private boolean authNeeded;
+ final private boolean isListQuery;
+ final private boolean isHttpsConnection;
+
+ public AbsURL(final boolean isHttpsConnection, final boolean authNeeded, final boolean isListQuery) {
+ this.params = new ArrayList();
+ this.authNeeded = authNeeded;
+ this.isListQuery = isListQuery;
+ this.isHttpsConnection = isHttpsConnection;
+ }
+
+ protected void addParam(final String key, final int value) {
+ addParam(key, String.valueOf(value));
+ }
+
+ protected void addParam(final String key, final long value) {
+ addParam(key, String.valueOf(value));
+ }
+
+ protected void addParam(final String key, final String value) {
+ for (final NameValuePair p : params) {
+ if (p.getName().equals(key)) {
+ params.remove(p);
+ break;
+ }
+ }
+ params.add(new BasicNameValuePair(key, Uri.encode(value)));
+ }
+
+ protected abstract String getBaseURL();
+
+ public List getParams() {
+ if (isListQuery) {
+ final List ret = new ArrayList(params);
+ ret.add(new BasicNameValuePair("client", "android"));
+ ret.add(new BasicNameValuePair("output", "json"));
+ ret.add(new BasicNameValuePair("ck", String.valueOf(System.currentTimeMillis())));
+ return ret;
+ } else {
+ return params;
+ }
+ }
+
+ public String getParamsString() {
+ return paramsToString(getParams());
+ }
+
+ public String getURL() {
+ return (isHttpsConnection ? "https://" : "http://") + getBaseURL();
+ }
+
+ public boolean isAuthNeeded() {
+ return authNeeded;
+ }
+
+ public boolean isListQuery() {
+ return isListQuery;
+ }
+
+ protected void removeParam(final String key) {
+ for (final NameValuePair p : params) {
+ if (p.getName().equals(key)) {
+ params.remove(p);
+ return;
+ }
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/EditItemTagURL.java b/src/com/pursuer/reader/easyrss/network/url/EditItemTagURL.java
new file mode 100644
index 0000000..3462dff
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/EditItemTagURL.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class EditItemTagURL extends AbsURL {
+ private static final String URL_API_EDIT_TAG = URL_GOOGLE_READER_API + "/edit-tag?client=scroll";
+
+ private String itemUid;
+ private String tagUid;
+ private boolean isAdd;
+
+ public EditItemTagURL(final boolean isHttpsConnection, final String itemUid, final String tagUid,
+ final boolean isAdd) {
+ super(isHttpsConnection, true, false);
+
+ setItemUid(itemUid);
+ setTagUid(tagUid);
+ setAdd(isAdd);
+ init();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof EditItemTagURL)) {
+ return false;
+ }
+ final EditItemTagURL url = (EditItemTagURL) obj;
+ return (itemUid.equals(url.itemUid) && tagUid.equals(url.tagUid) && isAdd == url.isAdd);
+ }
+
+ public String getItemUid() {
+ return itemUid;
+ }
+
+ public String getTagUid() {
+ return tagUid;
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_API_EDIT_TAG;
+ }
+
+ private void init() {
+ addParam("T", DataMgr.getInstance().getSettingByName(Setting.SETTING_TOKEN));
+ addParam("async", "true");
+ addParam("pos", "0");
+ }
+
+ public boolean isAdd() {
+ return isAdd;
+ }
+
+ public void setAdd(final boolean isAdd) {
+ this.isAdd = isAdd;
+ if (tagUid != null) {
+ if (isAdd) {
+ addParam("a", tagUid);
+ removeParam("r");
+ } else {
+ addParam("r", tagUid);
+ removeParam("a");
+ }
+ }
+ }
+
+ public void setItemUid(final String itemUid) {
+ this.itemUid = itemUid;
+ addParam("i", itemUid);
+ }
+
+ public void setTagUid(final String tagUid) {
+ this.tagUid = tagUid;
+ if (isAdd) {
+ addParam("a", tagUid);
+ removeParam("r");
+ } else {
+ addParam("r", tagUid);
+ removeParam("a");
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/EditItemURL.java b/src/com/pursuer/reader/easyrss/network/url/EditItemURL.java
new file mode 100644
index 0000000..08d9695
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/EditItemURL.java
@@ -0,0 +1,80 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.data.Setting;
+
+public class EditItemURL extends AbsURL {
+ private static final String URL_API_EDIT_ITEM = URL_GOOGLE_READER_API + "/item/edit?client=scroll";
+
+ private String itemUid;
+ private String annotation;
+ private boolean isShare;
+
+ public EditItemURL(final boolean isHttpsConnection, final String itemUid, final String annotation,
+ final boolean isShare) {
+ super(isHttpsConnection, true, false);
+
+ setItemUid(itemUid);
+ setAnnotation(annotation);
+ setShare(isShare);
+ init();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof EditItemURL)) {
+ return false;
+ }
+ final EditItemURL url = (EditItemURL) obj;
+ return (itemUid.equals(url.itemUid) && annotation.equals(url.annotation) && isShare == url.isShare);
+ }
+
+ public String getAnnotation() {
+ return annotation;
+ }
+
+ public String getItemUid() {
+ return itemUid;
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_API_EDIT_ITEM;
+ }
+
+ private void init() {
+ addParam("T", DataMgr.getInstance().getSettingByName(Setting.SETTING_TOKEN));
+ }
+
+ public boolean isShare() {
+ return isShare;
+ }
+
+ public void setAnnotation(final String annotation) {
+ this.annotation = annotation;
+ addParam("annotation", annotation);
+ }
+
+ public void setItemUid(final String itemUid) {
+ this.itemUid = itemUid;
+ addParam("srcItemId", itemUid);
+ }
+
+ public void setShare(final boolean isShare) {
+ this.isShare = isShare;
+ addParam("share", (isShare) ? "true" : "false");
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/LoginURL.java b/src/com/pursuer/reader/easyrss/network/url/LoginURL.java
new file mode 100644
index 0000000..5f211d4
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/LoginURL.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+public class LoginURL extends AbsURL {
+ private static final String URL_API_LOGIN = "www.google.com/accounts/ClientLogin";
+
+ private transient String username;
+ private transient String password;
+
+ public LoginURL(final String username, final String password) {
+ super(true, false, false);
+
+ setUsername(username);
+ setPassword(password);
+ init();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof LoginURL)) {
+ return false;
+ }
+ final LoginURL url = (LoginURL) obj;
+ return (url.username.equals(username) && url.password.equals(password));
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_API_LOGIN;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ private void init() {
+ addParam("accountType", "GOOGLE");
+ addParam("service", "reader");
+ addParam("source", "Sun-easyRSS-1.0");
+ }
+
+ public void setPassword(final String password) {
+ this.password = password;
+ addParam("Passwd", password);
+ }
+
+ public void setUsername(final String username) {
+ this.username = username;
+ addParam("Email", username);
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/RawURL.java b/src/com/pursuer/reader/easyrss/network/url/RawURL.java
new file mode 100644
index 0000000..dd31a3f
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/RawURL.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+public class RawURL extends AbsURL {
+ private String url;
+
+ public RawURL(final String url) {
+ super(url.startsWith("https://"), false, false);
+
+ if (url.startsWith("https://")) {
+ this.url = url.substring(8);
+ } else {
+ this.url = url.substring(7);
+ }
+ }
+
+ @Override
+ public String getBaseURL() {
+ return url;
+ }
+
+ public void setURL(final String url) {
+ this.url = url;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/StreamContentsURL.java b/src/com/pursuer/reader/easyrss/network/url/StreamContentsURL.java
new file mode 100644
index 0000000..bac2582
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/StreamContentsURL.java
@@ -0,0 +1,126 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+import android.net.Uri;
+
+/*
+ * Reference: http://code.google.com/p/google-reader-api/wiki/ApiStreamContents
+ */
+
+public class StreamContentsURL extends AbsURL {
+ private static final String URL_STREAM_CONTENTS = URL_GOOGLE_READER_API + "/stream/contents/";
+
+ private String uid;
+ private String continuation;
+ private long newestItemTime;
+ private int limit;
+ private boolean isUnread;
+
+ public StreamContentsURL(final boolean isHttpsConnection, final String uid, final boolean isUnread) {
+ super(isHttpsConnection, true, true);
+ init(uid, null, 20, 0, isUnread);
+ }
+
+ public StreamContentsURL(final boolean isHttpsConnection, final String uid, final String continuation,
+ final boolean isUnread) {
+ super(isHttpsConnection, true, true);
+ init(uid, continuation, 20, 0, isUnread);
+ }
+
+ public StreamContentsURL(final boolean isHttpsConnection, final String uid, final String continuation,
+ final long newestItemTime, final boolean isUnread) {
+ super(isHttpsConnection, true, true);
+ init(uid, continuation, newestItemTime, 20, isUnread);
+ }
+
+ public StreamContentsURL(final boolean isHttpsConnection, final String uid, final String continuation,
+ final long newestItemTime, final int limit, final boolean isUnread) {
+ super(isHttpsConnection, true, true);
+ init(uid, continuation, newestItemTime, limit, isUnread);
+ }
+
+ @Override
+ public String getBaseURL() {
+ return appendURL(URL_STREAM_CONTENTS, Uri.encode(uid));
+ }
+
+ public String getContinuation() {
+ return continuation;
+ }
+
+ public int getLimit() {
+ return limit;
+ }
+
+ public long getNewestItemTime() {
+ return newestItemTime;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ private void init(final String uid, final String continuation, final long newestItemTime, final int limit,
+ final boolean isUnread) {
+ this.setUid(uid);
+ this.setContinuation(continuation);
+ this.setNewestItemTime(newestItemTime);
+ this.setLimit(limit);
+ this.setUnread(isUnread);
+ addParam("likes", "false");
+ addParam("comments", "false");
+ addParam("r", "n");
+ }
+
+ public boolean isUnread() {
+ return isUnread;
+ }
+
+ public void setContinuation(final String continuation) {
+ this.continuation = continuation;
+ if (continuation == null || continuation.length() == 0) {
+ removeParam("c");
+ } else {
+ addParam("c", continuation);
+ }
+ }
+
+ public void setLimit(final int limit) {
+ this.limit = limit;
+ if (limit > 0) {
+ addParam("n", limit);
+ }
+ }
+
+ public void setNewestItemTime(final long newestItemTime) {
+ this.newestItemTime = newestItemTime;
+ if (newestItemTime > 0) {
+ addParam("nt", String.valueOf(newestItemTime));
+ } else {
+ removeParam("nt");
+ }
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ }
+
+ public void setUnread(final boolean isUnread) {
+ this.isUnread = isUnread;
+ if (isUnread) {
+ addParam("xt", "user/-/state/com.google/read");
+ } else {
+ removeParam("xt");
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/StreamIdsURL.java b/src/com/pursuer/reader/easyrss/network/url/StreamIdsURL.java
new file mode 100644
index 0000000..efa253c
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/StreamIdsURL.java
@@ -0,0 +1,79 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+public class StreamIdsURL extends AbsURL {
+ private static final String URL_STREAM_IDS = URL_GOOGLE_READER_API + "/stream/items/ids";
+
+ private String uid;
+ private int limit;
+ private boolean isUnread;
+
+ public StreamIdsURL(final boolean isHttpsConnection, final String uid) {
+ super(isHttpsConnection, true, true);
+ init(uid, 100, false);
+ }
+
+ public StreamIdsURL(final boolean isHttpsConnection, final String uid, final int limit) {
+ super(isHttpsConnection, true, true);
+ init(uid, limit, false);
+ }
+
+ public StreamIdsURL(final boolean isHttpsConnection, final String uid, final int limit, final boolean isUnread) {
+ super(isHttpsConnection, true, true);
+ init(uid, limit, isUnread);
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_STREAM_IDS;
+ }
+
+ public int getLimit() {
+ return limit;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ private void init(final String uid, final int limit, final boolean isUnread) {
+ this.setUid(uid);
+ this.setLimit(limit);
+ this.setUnread(isUnread);
+ }
+
+ public boolean isUnread() {
+ return isUnread;
+ }
+
+ public void setLimit(final int limit) {
+ this.limit = limit;
+ if (limit > 0) {
+ addParam("n", limit);
+ }
+ }
+
+ public void setUid(final String uid) {
+ this.uid = uid;
+ addParam("s", uid);
+ }
+
+ public void setUnread(final boolean isUnread) {
+ this.isUnread = isUnread;
+ if (isUnread) {
+ addParam("xt", "user/-/state/com.google/read");
+ } else {
+ removeParam("xt");
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/SubscriptionIconUrl.java b/src/com/pursuer/reader/easyrss/network/url/SubscriptionIconUrl.java
new file mode 100644
index 0000000..0daa754
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/SubscriptionIconUrl.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class SubscriptionIconUrl extends AbsURL {
+ final private static String URL_BASE = "s2.googleusercontent.com/s2/favicons";
+
+ private String subscriptionUrl;
+
+ public SubscriptionIconUrl(final boolean isHttpsConnection, final String subscriptionUrl) {
+ super(isHttpsConnection, false, false);
+
+ setSubscriptionUrl(subscriptionUrl);
+ init();
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_BASE;
+ }
+
+ public String getSubscriptionUrl() {
+ return subscriptionUrl;
+ }
+
+ private void init() {
+ addParam("alt", "feed");
+ }
+
+ public void setSubscriptionUrl(final String subscriptionUrl) {
+ this.subscriptionUrl = subscriptionUrl;
+ try {
+ final URL url = new URL(subscriptionUrl);
+ addParam("domain", url.getHost());
+ } catch (final MalformedURLException exception) {
+ exception.printStackTrace();
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/SubscriptionListURL.java b/src/com/pursuer/reader/easyrss/network/url/SubscriptionListURL.java
new file mode 100644
index 0000000..24c60df
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/SubscriptionListURL.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+public class SubscriptionListURL extends AbsURL {
+ private static final String URL_SUBSCRIPTION_LIST = URL_GOOGLE_READER_API + "/subscription/list";
+
+ public SubscriptionListURL(final boolean isHttpsConnection) {
+ super(isHttpsConnection, true, true);
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_SUBSCRIPTION_LIST;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/TagListURL.java b/src/com/pursuer/reader/easyrss/network/url/TagListURL.java
new file mode 100644
index 0000000..a8c32d4
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/TagListURL.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+public class TagListURL extends AbsURL {
+ private static final String URL_TAG_LIST = URL_GOOGLE_READER_API + "/tag/list";
+
+ public TagListURL(final boolean isHttpsConnection) {
+ super(isHttpsConnection, true, true);
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_TAG_LIST;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/TokenURL.java b/src/com/pursuer/reader/easyrss/network/url/TokenURL.java
new file mode 100644
index 0000000..22dfe6f
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/TokenURL.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+public class TokenURL extends AbsURL {
+ public static final String URL_API_TOKEN = URL_GOOGLE_READER_API + "/token";
+
+ public TokenURL(final boolean isHttpsConnection) {
+ super(isHttpsConnection, true, false);
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_API_TOKEN;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/network/url/UnreadCountURL.java b/src/com/pursuer/reader/easyrss/network/url/UnreadCountURL.java
new file mode 100644
index 0000000..642e351
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/network/url/UnreadCountURL.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.network.url;
+
+public class UnreadCountURL extends AbsURL {
+ private static final String URL_UNREAD_COUNT = URL_GOOGLE_READER_API + "/unread-count";
+
+ public UnreadCountURL(final boolean isHttpsConnection) {
+ super(isHttpsConnection, true, true);
+ }
+
+ @Override
+ public String getBaseURL() {
+ return URL_UNREAD_COUNT;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/AbsViewCtrl.java b/src/com/pursuer/reader/easyrss/view/AbsViewCtrl.java
new file mode 100644
index 0000000..92f5067
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/AbsViewCtrl.java
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+
+import com.pursuer.reader.easyrss.data.DataMgr;
+import com.pursuer.reader.easyrss.network.NetworkListener;
+
+public abstract class AbsViewCtrl implements NetworkListener {
+ final protected Context context;
+ final protected DataMgr dataMgr;
+ final protected ICachedView view;
+ final protected int resId;
+ protected ViewCtrlListener listener;
+
+ public AbsViewCtrl(final DataMgr dataMgr, final int resId, final Context context) {
+ this.resId = resId;
+ this.context = context;
+ this.dataMgr = dataMgr;
+
+ final LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ this.view = (ICachedView) inflater.inflate(resId, null);
+ }
+
+ public ViewCtrlListener getListener() {
+ return listener;
+ }
+
+ public int getResId() {
+ return resId;
+ }
+
+ public ICachedView getView() {
+ return view;
+ }
+
+ protected void handleOnDataSyncerProgressChanged(final String text, final int progress, final int maxProgress) {
+ // TODO Empty method
+ }
+
+ protected void handleOnLogin(final boolean succeeded) {
+ // TODO Empty method
+ }
+
+ protected void handleOnSyncFinished(final String syncerType, final boolean succeeded) {
+ // TODO Empty method
+ }
+
+ protected void handleOnSyncStarted(final String syncerType) {
+ // TODO Empty method
+ }
+
+ public abstract void onActivate();
+
+ public abstract void onCreate();
+
+ @Override
+ public void onDataSyncerProgressChanged(final String text, final int progress, final int maxProgress) {
+ handleOnDataSyncerProgressChanged(text, progress, maxProgress);
+ }
+
+ public abstract void onDeactivate();
+
+ public abstract void onDestory();
+
+ @Override
+ public void onLogin(final boolean succeeded) {
+ handleOnLogin(succeeded);
+ }
+
+ @Override
+ public void onSyncFinished(final String syncerType, final boolean succeeded) {
+ handleOnSyncFinished(syncerType, succeeded);
+ }
+
+ @Override
+ public void onSyncStarted(final String syncerType) {
+ handleOnSyncStarted(syncerType);
+ }
+
+ public void setListener(final ViewCtrlListener listener) {
+ this.listener = listener;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/CachedFrameLayout.java b/src/com/pursuer/reader/easyrss/view/CachedFrameLayout.java
new file mode 100644
index 0000000..e23bc41
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/CachedFrameLayout.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+public class CachedFrameLayout extends FrameLayout implements ICachedView {
+ final private Paint paint;
+ private boolean isCacheEnabled;
+ private Bitmap cache;
+
+ public CachedFrameLayout(final Context context) {
+ super(context);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public CachedFrameLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public CachedFrameLayout(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public void disableCache() {
+ if (isCacheEnabled) {
+ setDrawingCacheEnabled(false);
+ if (cache != null && !cache.isRecycled()) {
+ cache.recycle();
+ }
+ isCacheEnabled = false;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(final Canvas canvas) {
+ if (!isCacheEnabled) {
+ super.dispatchDraw(canvas);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ if (isCacheEnabled) {
+ if (cache == null || cache.isRecycled()) {
+ updateCache();
+ }
+ if (cache != null && !cache.isRecycled()) {
+ canvas.drawBitmap(cache, null, new Rect(0, 0, getWidth(), getHeight()), paint);
+ }
+ } else {
+ super.draw(canvas);
+ }
+ }
+
+ public void enableCache() {
+ if (!isCacheEnabled) {
+ updateCache();
+ isCacheEnabled = true;
+ }
+ }
+
+ @Override
+ public void invalidate() {
+ if (!isCacheEnabled) {
+ super.invalidate();
+ }
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ if (!isCacheEnabled) {
+ super.onDraw(canvas);
+ }
+ }
+
+ @Override
+ public void setAlpha(final float alpha) {
+ paint.setAlpha((int) (255 * alpha));
+ }
+
+ private void updateCache() {
+ setDrawingCacheEnabled(true);
+ final Bitmap current = getDrawingCache();
+ if (current != null && !current.isRecycled()) {
+ cache = current.copy(Bitmap.Config.RGB_565, false);
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/CachedLinearLayout.java b/src/com/pursuer/reader/easyrss/view/CachedLinearLayout.java
new file mode 100644
index 0000000..06cf2fc
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/CachedLinearLayout.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+public class CachedLinearLayout extends LinearLayout implements ICachedView {
+ final private Paint paint;
+ private boolean isCacheEnabled;
+ private Bitmap cache;
+
+ public CachedLinearLayout(final Context context) {
+ super(context);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public CachedLinearLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public void disableCache() {
+ if (isCacheEnabled) {
+ setDrawingCacheEnabled(false);
+ if (cache != null && !cache.isRecycled()) {
+ cache.recycle();
+ }
+ isCacheEnabled = false;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(final Canvas canvas) {
+ if (!isCacheEnabled) {
+ super.dispatchDraw(canvas);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ if (isCacheEnabled) {
+ if (cache == null || cache.isRecycled()) {
+ updateCache();
+ }
+ if (cache != null && !cache.isRecycled()) {
+ canvas.drawBitmap(cache, null, new Rect(0, 0, getWidth(), getHeight()), paint);
+ }
+ } else {
+ super.draw(canvas);
+ }
+ }
+
+ public void enableCache() {
+ if (!isCacheEnabled) {
+ updateCache();
+ isCacheEnabled = true;
+ }
+ }
+
+ @Override
+ public void invalidate() {
+ if (!isCacheEnabled) {
+ super.invalidate();
+ }
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ if (!isCacheEnabled) {
+ super.onDraw(canvas);
+ }
+ }
+
+ @Override
+ public void setAlpha(final float alpha) {
+ paint.setAlpha((int) (255 * alpha));
+ }
+
+ private void updateCache() {
+ setDrawingCacheEnabled(true);
+ final Bitmap current = getDrawingCache();
+ if (current != null && !current.isRecycled()) {
+ cache = current.copy(Bitmap.Config.RGB_565, false);
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/CachedRelativeLayout.java b/src/com/pursuer/reader/easyrss/view/CachedRelativeLayout.java
new file mode 100644
index 0000000..4e78fae
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/CachedRelativeLayout.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+public class CachedRelativeLayout extends RelativeLayout implements ICachedView {
+ final private Paint paint;
+ private boolean isCacheEnabled;
+ private Bitmap cache;
+
+ public CachedRelativeLayout(final Context context) {
+ super(context);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public CachedRelativeLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public CachedRelativeLayout(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ this.paint = new Paint();
+ this.isCacheEnabled = false;
+ }
+
+ public void disableCache() {
+ if (isCacheEnabled) {
+ setDrawingCacheEnabled(false);
+ if (cache != null && !cache.isRecycled()) {
+ cache.recycle();
+ }
+ isCacheEnabled = false;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(final Canvas canvas) {
+ if (isCacheEnabled) {
+ if (cache == null || cache.isRecycled()) {
+ updateCache();
+ }
+ if (cache != null && !cache.isRecycled()) {
+ canvas.drawBitmap(cache, null, new Rect(0, 0, getWidth(), getHeight()), paint);
+ }
+ } else {
+ super.dispatchDraw(canvas);
+ }
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ if (!isCacheEnabled) {
+ super.draw(canvas);
+ }
+ }
+
+ public void enableCache() {
+ if (!isCacheEnabled) {
+ updateCache();
+ isCacheEnabled = true;
+ }
+ }
+
+ @Override
+ public void invalidate() {
+ if (!isCacheEnabled) {
+ super.invalidate();
+ }
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ if (!isCacheEnabled) {
+ super.onDraw(canvas);
+ }
+ }
+
+ @Override
+ public void setAlpha(final float alpha) {
+ paint.setAlpha((int) (255 * alpha));
+ }
+
+ private void updateCache() {
+ setDrawingCacheEnabled(true);
+ final Bitmap current = getDrawingCache();
+ if (current != null && !current.isRecycled()) {
+ cache = current.copy(Bitmap.Config.RGB_565, false);
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/HorizontalPager.java b/src/com/pursuer/reader/easyrss/view/HorizontalPager.java
new file mode 100644
index 0000000..8a9446b
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/HorizontalPager.java
@@ -0,0 +1,321 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.Scroller;
+
+public final class HorizontalPager extends ViewGroup {
+ private static final int ANIMATION_SCREEN_SET_DURATION_MILLIS = 500;
+ private static final int FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE = 15;
+ private static final int INVALID_SCREEN = -1;
+ private static final int SNAP_VELOCITY_DIP_PER_SECOND = 600;
+ private static final int VELOCITY_UNIT_PIXELS_PER_SECOND = 1000;
+
+ private int mCurrentScreen;
+ private int mDensityAdjustedSnapVelocity;
+ private boolean mFirstLayout = true;
+ private float mLastMotionX;
+ private float mLastMotionY;
+ private int mMaximumVelocity;
+ private int mNextScreen = INVALID_SCREEN;
+ private Scroller scroller;
+ private int mTouchSlop;
+ private boolean isDragging;
+ private VelocityTracker mVelocityTracker;
+ private HorizontalPagerListener listener;
+ private int mLastSeenLayoutWidth = -1;
+
+ public HorizontalPager(final Context context) {
+ super(context);
+ init();
+ }
+
+ public HorizontalPager(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ @Override
+ public void computeScroll() {
+ if (scroller.computeScrollOffset()) {
+ scrollTo(scroller.getCurrX(), scroller.getCurrY());
+ postInvalidate();
+ } else if (mNextScreen != INVALID_SCREEN) {
+ mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1));
+ if (listener != null) {
+ listener.onScreenSwitch(mCurrentScreen);
+ }
+ mNextScreen = INVALID_SCREEN;
+ }
+ }
+
+ public int getCurrentScreen() {
+ return mCurrentScreen;
+ }
+
+ public HorizontalPagerListener getListener() {
+ return listener;
+ }
+
+ private void init() {
+ this.scroller = new Scroller(getContext());
+ this.isDragging = false;
+
+ final DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(
+ displayMetrics);
+ mDensityAdjustedSnapVelocity = (int) (displayMetrics.density * SNAP_VELOCITY_DIP_PER_SECOND);
+
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent event) {
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mLastMotionX = x;
+ mLastMotionY = y;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!isDragging) {
+ final int xDiff = (int) Math.abs(x - mLastMotionX);
+ final int yDiff = (int) Math.abs(y - mLastMotionY);
+
+ if (xDiff > mTouchSlop && yDiff > mTouchSlop) {
+ isDragging = (xDiff >= yDiff);
+ } else if (xDiff > mTouchSlop) {
+ isDragging = true;
+ } else if (yDiff > mTouchSlop) {
+ isDragging = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ isDragging = false;
+ break;
+ default:
+ break;
+ }
+ if (isDragging) {
+ mLastMotionX = x;
+ mLastMotionY = y;
+ }
+ return isDragging;
+ }
+
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
+ int childLeft = 0;
+ final int count = getChildCount();
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
+ childLeft += childWidth;
+ }
+ }
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("HorizontalPager can only be used in EXACTLY mode.");
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("HorizontalPager can only be used in EXACTLY mode.");
+ }
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ if (mFirstLayout) {
+ scrollTo(mCurrentScreen * width, 0);
+ if (listener != null) {
+ listener.onScreenSwitch(mCurrentScreen);
+ }
+ mFirstLayout = false;
+ } else if (width != mLastSeenLayoutWidth) {
+ final Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ @SuppressWarnings("deprecation")
+ final int displayWidth = display.getWidth();
+ mNextScreen = Math.max(0, Math.min(getCurrentScreen(), getChildCount() - 1));
+ final int newX = mNextScreen * displayWidth;
+ final int delta = newX - getScrollX();
+ scroller.startScroll(getScrollX(), 0, delta, 0, 0);
+ }
+
+ mLastSeenLayoutWidth = width;
+ }
+
+ @Override
+ protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) {
+ if (listener != null) {
+ listener.onScrollChanged(l, t, oldl, oldt);
+ }
+ super.onScrollChanged(l, t, oldl, oldt);
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ mVelocityTracker.addMovement(event);
+
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (!scroller.isFinished()) {
+ scroller.abortAnimation();
+ }
+
+ mLastMotionX = x;
+ mLastMotionY = y;
+ isDragging = true;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final int xDiff = (int) Math.abs(x - mLastMotionX);
+ if (xDiff > mTouchSlop) {
+ isDragging = true;
+ }
+
+ if (isDragging) {
+ final int deltaX = (int) (mLastMotionX - x);
+ final int scrollX = getScrollX();
+
+ if (deltaX < 0) {
+ if (scrollX > 0) {
+ scrollBy(Math.max(-scrollX, deltaX), 0);
+ }
+ } else if (deltaX > 0) {
+ final int availableToScroll = getChildAt(getChildCount() - 1).getRight() - scrollX - getWidth();
+ if (availableToScroll > 0) {
+ scrollBy(Math.min(availableToScroll, deltaX), 0);
+ }
+ }
+
+ mLastMotionX = x;
+ mLastMotionY = y;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (isDragging) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(VELOCITY_UNIT_PIXELS_PER_SECOND, mMaximumVelocity);
+ final int velocityX = (int) velocityTracker.getXVelocity();
+
+ if (velocityX > mDensityAdjustedSnapVelocity && mCurrentScreen > 0) {
+ snapToScreen(mCurrentScreen - 1);
+ } else if (velocityX < -mDensityAdjustedSnapVelocity && mCurrentScreen < getChildCount() - 1) {
+ snapToScreen(mCurrentScreen + 1);
+ } else {
+ snapToDestination();
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+ isDragging = false;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ isDragging = false;
+ break;
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ public void setCurrentScreen(final int currentScreen, final boolean animate) {
+ mCurrentScreen = Math.max(0, Math.min(currentScreen, getChildCount() - 1));
+ if (animate) {
+ snapToScreen(currentScreen, ANIMATION_SCREEN_SET_DURATION_MILLIS);
+ } else {
+ scrollTo(mCurrentScreen * getWidth(), 0);
+ }
+ invalidate();
+ }
+
+ public void setListener(final HorizontalPagerListener listener) {
+ this.listener = listener;
+ }
+
+ private void snapToDestination() {
+ final int screenWidth = getWidth();
+ final int scrollX = getScrollX();
+ final int deltaX = scrollX - (screenWidth * mCurrentScreen);
+ int whichScreen = mCurrentScreen;
+
+ if ((deltaX < 0) && mCurrentScreen != 0 && ((screenWidth / FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE) < -deltaX)) {
+ whichScreen--;
+ } else if ((deltaX > 0) && (mCurrentScreen + 1 != getChildCount())
+ && ((screenWidth / FRACTION_OF_SCREEN_WIDTH_FOR_SWIPE) < deltaX)) {
+ whichScreen++;
+ }
+
+ snapToScreen(whichScreen);
+ }
+
+ private void snapToScreen(final int whichScreen) {
+ snapToScreen(whichScreen, -1);
+ }
+
+ private void snapToScreen(final int whichScreen, final int duration) {
+ mNextScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
+ final int newX = mNextScreen * getWidth();
+ final int delta = newX - getScrollX();
+
+ if (duration < 0) {
+ scroller.startScroll(getScrollX(), 0, delta, 0,
+ (int) (Math.abs(delta) / (float) getWidth() * ANIMATION_SCREEN_SET_DURATION_MILLIS));
+ } else {
+ scroller.startScroll(getScrollX(), 0, delta, 0, duration);
+ }
+
+ invalidate();
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/HorizontalPagerListener.java b/src/com/pursuer/reader/easyrss/view/HorizontalPagerListener.java
new file mode 100644
index 0000000..41ca983
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/HorizontalPagerListener.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+public interface HorizontalPagerListener {
+ void onScreenSwitch(int currentScreen);
+
+ void onScrollChanged(int l, int t, int oldl, int oldt);
+}
diff --git a/src/com/pursuer/reader/easyrss/view/HorizontalSwipeView.java b/src/com/pursuer/reader/easyrss/view/HorizontalSwipeView.java
new file mode 100644
index 0000000..f9b6d2a
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/HorizontalSwipeView.java
@@ -0,0 +1,205 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+public final class HorizontalSwipeView extends LinearLayout {
+ private static final int SNAP_VELOCITY_DIP_PER_SECOND = 600;
+ private static final int VELOCITY_UNIT_PIXELS_PER_SECOND = 1000;
+
+ private int mMaximumVelocity;
+ private int mDensityAdjustedSnapVelocity;
+ private int mTouchSlop;
+ private float mLastMotionX;
+ private float mLastMotionY;
+ private boolean isDragging;
+ private boolean isRightSwipeValid;
+ private boolean isLeftSwipeValid;
+ private VelocityTracker mVelocityTracker;
+ private HorizontalSwipeViewListener listener;
+
+ public HorizontalSwipeView(final Context context) {
+ super(context);
+ init();
+ }
+
+ public HorizontalSwipeView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public HorizontalSwipeViewListener getListener() {
+ return listener;
+ }
+
+ private void init() {
+ this.isDragging = false;
+
+ final DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(
+ displayMetrics);
+ mDensityAdjustedSnapVelocity = (int) (displayMetrics.density * SNAP_VELOCITY_DIP_PER_SECOND);
+
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ }
+
+ public boolean isLeftSwipeValid() {
+ return isLeftSwipeValid;
+ }
+
+ public boolean isRightSwipeValid() {
+ return isRightSwipeValid;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent event) {
+ if (!isLeftSwipeValid && !isRightSwipeValid) {
+ return false;
+ }
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mLastMotionX = x;
+ mLastMotionY = y;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!isDragging) {
+ final int xDiff = (int) Math.abs(x - mLastMotionX);
+ final int yDiff = (int) Math.abs(y - mLastMotionY);
+
+ if (xDiff > mTouchSlop) {
+ isDragging = (xDiff >= yDiff)
+ && ((isLeftSwipeValid && x < mLastMotionX) || (isRightSwipeValid && x > mLastMotionX));
+ } else if (yDiff > mTouchSlop) {
+ isDragging = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (isDragging) {
+ if (listener != null) {
+ listener.cancelSwipe();
+ }
+ isDragging = false;
+ }
+ break;
+ default:
+ break;
+ }
+ return isDragging;
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+ if (!isLeftSwipeValid && !isRightSwipeValid) {
+ return false;
+ }
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ mVelocityTracker.addMovement(event);
+
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mLastMotionX = x;
+ mLastMotionY = y;
+ isDragging = true;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!isDragging) {
+ final int xDiff = (int) Math.abs(x - mLastMotionX);
+ final int yDiff = (int) Math.abs(y - mLastMotionY);
+
+ if (xDiff > mTouchSlop) {
+ isDragging = (xDiff >= yDiff)
+ && ((isLeftSwipeValid && x < mLastMotionX) || (isRightSwipeValid && x > mLastMotionX));
+ } else if (yDiff > mTouchSlop) {
+ isDragging = false;
+ }
+ }
+
+ if (isDragging) {
+ final int deltaX = (int) (mLastMotionX - x);
+ if (listener != null) {
+ listener.swipeTo(deltaX);
+ }
+
+ mLastMotionX = x;
+ mLastMotionY = y;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (isDragging) {
+ mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_PIXELS_PER_SECOND, mMaximumVelocity);
+ final int velocityX = (int) mVelocityTracker.getXVelocity();
+ if (listener != null) {
+ if (velocityX > mDensityAdjustedSnapVelocity) {
+ listener.swipeRight();
+ } else if (velocityX < -mDensityAdjustedSnapVelocity) {
+ listener.swipeLeft();
+ } else {
+ listener.cancelSwipe();
+ }
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+ isDragging = false;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (isDragging) {
+ if (listener != null) {
+ listener.cancelSwipe();
+ }
+ isDragging = false;
+ }
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ public void setLeftSwipeValid(final boolean isLeftSwipeValid) {
+ this.isLeftSwipeValid = isLeftSwipeValid;
+ }
+
+ public void setListener(final HorizontalSwipeViewListener listener) {
+ this.listener = listener;
+ }
+
+ public void setRightSwipeValid(final boolean isRightSwipeValid) {
+ this.isRightSwipeValid = isRightSwipeValid;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/HorizontalSwipeViewListener.java b/src/com/pursuer/reader/easyrss/view/HorizontalSwipeViewListener.java
new file mode 100644
index 0000000..ebb63f9
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/HorizontalSwipeViewListener.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+public interface HorizontalSwipeViewListener {
+ void cancelSwipe();
+
+ void swipeLeft();
+
+ void swipeRight();
+
+ void swipeTo(int deltaX);
+}
diff --git a/src/com/pursuer/reader/easyrss/view/ICachedView.java b/src/com/pursuer/reader/easyrss/view/ICachedView.java
new file mode 100644
index 0000000..e773da5
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/ICachedView.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.graphics.Paint;
+import android.os.IBinder;
+import android.view.View;
+import android.view.animation.Animation;
+
+public interface ICachedView {
+ void disableCache();
+
+ void enableCache();
+
+ View findViewById(int id);
+
+ int getMeasuredWidth();
+
+ IBinder getWindowToken();
+
+ void setAlpha(float alpha);
+
+ void setAnimation(Animation animation);
+
+ void setLayerType(int layerType, Paint paint);
+}
diff --git a/src/com/pursuer/reader/easyrss/view/OnScaleChangedListener.java b/src/com/pursuer/reader/easyrss/view/OnScaleChangedListener.java
new file mode 100644
index 0000000..6695a7f
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/OnScaleChangedListener.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+public interface OnScaleChangedListener {
+ void onScaleChanged(TouchImageView view, float scale);
+}
diff --git a/src/com/pursuer/reader/easyrss/view/OnScrollChangedListener.java b/src/com/pursuer/reader/easyrss/view/OnScrollChangedListener.java
new file mode 100644
index 0000000..bdbc801
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/OnScrollChangedListener.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.view.View;
+
+public interface OnScrollChangedListener {
+ void onScrollChanged(View view, int x, int y, int oldx, int oldy);
+}
diff --git a/src/com/pursuer/reader/easyrss/view/OverScrollView.java b/src/com/pursuer/reader/easyrss/view/OverScrollView.java
new file mode 100644
index 0000000..ac0f004
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/OverScrollView.java
@@ -0,0 +1,354 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+public class OverScrollView extends FrameLayout {
+ final static private int DRAGGING_MODE_IDLE = 0;
+ final static private int DRAGGING_MODE_HORIZONTAL = 1;
+ final static private int DRAGGING_MODE_VERTICAL = 2;
+
+ private OnScrollChangedListener onScrollChangedListener;
+ private OverScroller scroller;
+ private int topScrollMargin;
+ private int bottomScrollMargin;
+ private int draggingMode;
+ private int touchSlop;
+ private int minVelocity;
+ private int maxVelocity;
+ private float lastMotionX;
+ private float lastMotionY;
+ private boolean isScrollHold;
+ private View topScrollView;
+ private View bottomScrollView;
+ VelocityTracker velocityTracker;
+
+ public OverScrollView(final Context context) {
+ super(context);
+ init();
+ }
+
+ public OverScrollView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public OverScrollView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void adjustScrollY() {
+ if (isScrollHold || draggingMode == DRAGGING_MODE_VERTICAL) {
+ return;
+ }
+ final int y = getScrollY();
+ if (y < getMinScrollY()) {
+ scrollTo(0, getMinScrollY());
+ } else if (y > getMaxScrollY()) {
+ scrollTo(0, getMaxScrollY());
+ }
+ }
+
+ @Override
+ public void computeScroll() {
+ if (!isScrollHold && scroller.computeScrollOffset()) {
+ scrollTo(scroller.getCurrX(), scroller.getCurrY());
+ postInvalidate();
+ }
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return Math.max(super.computeVerticalScrollOffset() - topScrollMargin, 0);
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return getChildAt(0).getMeasuredHeight() - topScrollMargin - bottomScrollMargin;
+ }
+
+ public void fling(final int velocityY) {
+ scroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, getMinScrollY(), getMaxScrollY(), 0,
+ Math.min(topScrollMargin, bottomScrollMargin) - 1);
+ invalidate();
+ }
+
+ public int getBottomScrollMargin() {
+ return bottomScrollMargin;
+ }
+
+ public View getBottomScrollView() {
+ return bottomScrollView;
+ }
+
+ private int getMaxScrollY() {
+ return getChildAt(0).getMeasuredHeight() - bottomScrollMargin - getHeight();
+ }
+
+ private int getMinScrollY() {
+ return topScrollMargin;
+ }
+
+ public OnScrollChangedListener getOnScrollChangeListener() {
+ return onScrollChangedListener;
+ }
+
+ public int getTopScrollMargin() {
+ return topScrollMargin;
+ }
+
+ public View getTopScrollView() {
+ return topScrollView;
+ }
+
+ private void init() {
+ this.draggingMode = DRAGGING_MODE_IDLE;
+ this.topScrollMargin = 0;
+ this.bottomScrollMargin = 0;
+ this.lastMotionY = 0;
+ this.scroller = new OverScroller(getContext());
+ this.isScrollHold = false;
+
+ setFocusable(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ this.touchSlop = configuration.getScaledTouchSlop();
+ this.minVelocity = configuration.getScaledMinimumFlingVelocity();
+ this.maxVelocity = configuration.getScaledMaximumFlingVelocity();
+ }
+
+ public boolean isScrollHold() {
+ return isScrollHold;
+ }
+
+ @Override
+ protected void measureChild(final View child, final int parentWidthMeasureSpec, final int parentHeightMeasureSpec) {
+ final ViewGroup.LayoutParams lp = child.getLayoutParams();
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 0, lp.width);
+ final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ protected void measureChildWithMargins(final View child, final int parentWidthMeasureSpec, final int widthUsed,
+ final int parentHeightMeasureSpec, final int heightUsed) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, lp.leftMargin + lp.rightMargin
+ + widthUsed, lp.width);
+ final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin,
+ MeasureSpec.UNSPECIFIED);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent event) {
+ final int action = event.getAction();
+
+ final float x = event.getX();
+ final float y = event.getY();
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ if (draggingMode == DRAGGING_MODE_IDLE) {
+ final int xDiff = (int) Math.abs(x - lastMotionX);
+ final int yDiff = (int) Math.abs(y - lastMotionY);
+ if (yDiff > touchSlop) {
+ draggingMode = DRAGGING_MODE_VERTICAL;
+ } else if (xDiff > touchSlop) {
+ draggingMode = DRAGGING_MODE_HORIZONTAL;
+ }
+ }
+
+ if (draggingMode == DRAGGING_MODE_VERTICAL) {
+ lastMotionX = x;
+ lastMotionY = y;
+ }
+
+ break;
+ case MotionEvent.ACTION_DOWN:
+ if (!scroller.isFinished()) {
+ draggingMode = DRAGGING_MODE_IDLE;
+ scroller.abortAnimation();
+ }
+ lastMotionX = x;
+ lastMotionY = y;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ draggingMode = DRAGGING_MODE_IDLE;
+ if (scroller.springBack(0, getScrollY(), 0, 0, getMinScrollY(), getMaxScrollY())) {
+ invalidate();
+ }
+ break;
+ default:
+ }
+ return draggingMode == DRAGGING_MODE_VERTICAL;
+ }
+
+ @Override
+ protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ adjustScrollY();
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ return;
+ }
+
+ if (topScrollView != null) {
+ setTopScrollMargin(topScrollView.getMeasuredHeight());
+ }
+ if (bottomScrollView != null) {
+ setBottomScrollMargin(bottomScrollView.getMeasuredHeight());
+ }
+
+ if (getChildCount() > 0) {
+ final View child = getChildAt(0);
+ final int height = getMeasuredHeight() + topScrollMargin + bottomScrollMargin;
+ if (child.getMeasuredHeight() < height) {
+ final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
+ final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+ }
+
+ @Override
+ protected void onScrollChanged(final int x, final int y, final int oldx, final int oldy) {
+ super.onScrollChanged(x, y, oldx, oldy);
+ if (onScrollChangedListener != null) {
+ onScrollChangedListener.onScrollChanged(this, x, y, oldx, oldy);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ if (velocityTracker == null) {
+ velocityTracker = VelocityTracker.obtain();
+ }
+ velocityTracker.addMovement(event);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (!scroller.isFinished()) {
+ scroller.abortAnimation();
+ draggingMode = DRAGGING_MODE_IDLE;
+ }
+ draggingMode = DRAGGING_MODE_VERTICAL;
+ lastMotionX = x;
+ lastMotionY = y;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final int xDiff = (int) Math.abs(x - lastMotionX);
+ final int yDiff = (int) Math.abs(y - lastMotionY);
+ if (draggingMode == DRAGGING_MODE_IDLE) {
+ if (yDiff > touchSlop) {
+ draggingMode = DRAGGING_MODE_VERTICAL;
+ } else if (xDiff > touchSlop) {
+ draggingMode = DRAGGING_MODE_HORIZONTAL;
+ }
+ }
+
+ if (draggingMode == DRAGGING_MODE_VERTICAL) {
+ final int scrollY = getScrollY();
+ int deltaY = (int) (lastMotionY - y);
+ deltaY = Math.max(deltaY, -scrollY);
+ deltaY = Math.min(deltaY, getChildAt(0).getHeight() - getHeight() - scrollY);
+ if (deltaY != 0) {
+ scrollBy(0, deltaY);
+ }
+ lastMotionX = x;
+ lastMotionY = y;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (draggingMode == DRAGGING_MODE_VERTICAL) {
+ velocityTracker.computeCurrentVelocity(1000, maxVelocity);
+ final int initialVelocity = (int) velocityTracker.getYVelocity();
+
+ if (getChildCount() > 0) {
+ if ((Math.abs(initialVelocity) > minVelocity)) {
+ fling(-initialVelocity);
+ } else {
+ if (scroller.springBack(0, getScrollY(), 0, 0, getMinScrollY(), getMaxScrollY())) {
+ invalidate();
+ }
+ }
+ }
+ }
+ draggingMode = DRAGGING_MODE_IDLE;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (draggingMode == DRAGGING_MODE_VERTICAL
+ && scroller.springBack(0, getScrollY(), 0, 0, getMinScrollY(), getMaxScrollY())) {
+ invalidate();
+ }
+ draggingMode = DRAGGING_MODE_IDLE;
+ break;
+ default:
+ }
+ return true;
+ }
+
+ private void setBottomScrollMargin(final int bottomScrollMargin) {
+ this.bottomScrollMargin = bottomScrollMargin;
+ adjustScrollY();
+ }
+
+ public void setBottomScrollView(final View bottomScrollView) {
+ this.bottomScrollView = bottomScrollView;
+ if (bottomScrollView != null) {
+ setBottomScrollMargin(bottomScrollView.getMeasuredHeight());
+ }
+ }
+
+ public void setOnScrollChangeListener(final OnScrollChangedListener onScrollChangeListener) {
+ this.onScrollChangedListener = onScrollChangeListener;
+ }
+
+ public void setScrollHold(final boolean isScrollHold) {
+ this.isScrollHold = isScrollHold;
+ scroller.forceFinished(isScrollHold);
+ }
+
+ private void setTopScrollMargin(final int topScrollMargin) {
+ this.topScrollMargin = topScrollMargin;
+ adjustScrollY();
+ }
+
+ public void setTopScrollView(final View topScrollView) {
+ this.topScrollView = topScrollView;
+ if (topScrollView != null) {
+ setTopScrollMargin(topScrollView.getMeasuredHeight());
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/OverScroller.java b/src/com/pursuer/reader/easyrss/view/OverScroller.java
new file mode 100644
index 0000000..4bda752
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/OverScroller.java
@@ -0,0 +1,893 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.util.FloatMath;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.pursuer.reader.easyrss.view.Scroller;
+
+/**
+ * This class encapsulates scrolling with the ability to overshoot the bounds of
+ * a scrolling operation. This class is a drop-in replacement for
+ * {@link android.widget.Scroller} in most cases.
+ */
+public class OverScroller {
+ private int mMode;
+
+ private MagneticOverScroller mScrollerX;
+ private MagneticOverScroller mScrollerY;
+
+ private final Interpolator mInterpolator;
+
+ private static final int DEFAULT_DURATION = 250;
+ private static final int SCROLL_MODE = 0;
+ private static final int FLING_MODE = 1;
+
+ /**
+ * Creates an OverScroller with a viscous fluid scroll interpolator.
+ *
+ * @param context
+ */
+ public OverScroller(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Creates an OverScroller with default edge bounce coefficients.
+ *
+ * @param context
+ * The context of this application.
+ * @param interpolator
+ * The scroll interpolator. If null, a default (viscous)
+ * interpolator will be used.
+ */
+ public OverScroller(Context context, Interpolator interpolator) {
+ this(context, interpolator, MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT,
+ MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT);
+ }
+
+ /**
+ * Creates an OverScroller.
+ *
+ * @param context
+ * The context of this application.
+ * @param interpolator
+ * The scroll interpolator. If null, a default (viscous)
+ * interpolator will be used.
+ * @param bounceCoefficientX
+ * A value between 0 and 1 that will determine the proportion of
+ * the velocity which is preserved in the bounce when the
+ * horizontal edge is reached. A null value means no bounce.
+ * @param bounceCoefficientY
+ * Same as bounceCoefficientX but for the vertical direction.
+ */
+ public OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY) {
+ mInterpolator = interpolator;
+ mScrollerX = new MagneticOverScroller();
+ mScrollerY = new MagneticOverScroller();
+ MagneticOverScroller.initializeFromContext(context);
+
+ mScrollerX.setBounceCoefficient(bounceCoefficientX);
+ mScrollerY.setBounceCoefficient(bounceCoefficientY);
+ }
+
+ /**
+ *
+ * Returns whether the scroller has finished scrolling.
+ *
+ * @return True if the scroller has finished scrolling, false otherwise.
+ */
+ public final boolean isFinished() {
+ return mScrollerX.mFinished && mScrollerY.mFinished;
+ }
+
+ /**
+ * Force the finished field to a particular value. Contrary to
+ * {@link #abortAnimation()}, forcing the animation to finished does NOT
+ * cause the scroller to move to the final x and y position.
+ *
+ * @param finished
+ * The new finished value.
+ */
+ public final void forceFinished(boolean finished) {
+ mScrollerX.mFinished = mScrollerY.mFinished = finished;
+ }
+
+ /**
+ * Returns the current X offset in the scroll.
+ *
+ * @return The new X offset as an absolute distance from the origin.
+ */
+ public final int getCurrX() {
+ return mScrollerX.mCurrentPosition;
+ }
+
+ /**
+ * Returns the current Y offset in the scroll.
+ *
+ * @return The new Y offset as an absolute distance from the origin.
+ */
+ public final int getCurrY() {
+ return mScrollerY.mCurrentPosition;
+ }
+
+ /**
+ * @hide Returns the current velocity.
+ *
+ * @return The original velocity less the deceleration, norm of the X and Y
+ * velocity vector.
+ */
+ public float getCurrVelocity() {
+ float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
+ squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
+ return FloatMath.sqrt(squaredNorm);
+ }
+
+ /**
+ * Returns the start X offset in the scroll.
+ *
+ * @return The start X offset as an absolute distance from the origin.
+ */
+ public final int getStartX() {
+ return mScrollerX.mStart;
+ }
+
+ /**
+ * Returns the start Y offset in the scroll.
+ *
+ * @return The start Y offset as an absolute distance from the origin.
+ */
+ public final int getStartY() {
+ return mScrollerY.mStart;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final X offset as an absolute distance from the origin.
+ */
+ public final int getFinalX() {
+ return mScrollerX.mFinal;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final Y offset as an absolute distance from the origin.
+ */
+ public final int getFinalY() {
+ return mScrollerY.mFinal;
+ }
+
+ /**
+ * Returns how long the scroll event will take, in milliseconds.
+ *
+ * @return The duration of the scroll in milliseconds.
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScrollers don't necessarily have a fixed duration. This
+ * function will lie to the best of its ability.
+ */
+ public final int getDuration() {
+ return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
+ }
+
+ /**
+ * Extend the scroll animation. This allows a running animation to scroll
+ * further and longer, when used with {@link #setFinalX(int)} or
+ * {@link #setFinalY(int)}.
+ *
+ * @param extend
+ * Additional time to scroll in milliseconds.
+ * @see #setFinalX(int)
+ * @see #setFinalY(int)
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScrollers don't necessarily have a fixed duration.
+ * Instead of setting a new final position and extending the
+ * duration of an existing scroll, use startScroll to begin a
+ * new animation.
+ */
+ public void extendDuration(int extend) {
+ mScrollerX.extendDuration(extend);
+ mScrollerY.extendDuration(extend);
+ }
+
+ /**
+ * Sets the final position (X) for this scroller.
+ *
+ * @param newX
+ * The new X offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalY(int)
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScroller's final position may change during an animation.
+ * Instead of setting a new final position and extending the
+ * duration of an existing scroll, use startScroll to begin a
+ * new animation.
+ */
+ public void setFinalX(int newX) {
+ mScrollerX.setFinalPosition(newX);
+ }
+
+ /**
+ * Sets the final position (Y) for this scroller.
+ *
+ * @param newY
+ * The new Y offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalX(int)
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScroller's final position may change during an animation.
+ * Instead of setting a new final position and extending the
+ * duration of an existing scroll, use startScroll to begin a
+ * new animation.
+ */
+ public void setFinalY(int newY) {
+ mScrollerY.setFinalPosition(newY);
+ }
+
+ /**
+ * Call this when you want to know the new location. If it returns true, the
+ * animation is not yet finished.
+ */
+ public boolean computeScrollOffset() {
+ if (isFinished()) {
+ return false;
+ }
+
+ switch (mMode) {
+ case SCROLL_MODE:
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ // Any scroller can be used for time, since they were started
+ // together in scroll mode. We use X here.
+ final long elapsedTime = time - mScrollerX.mStartTime;
+
+ final int duration = mScrollerX.mDuration;
+ if (elapsedTime < duration) {
+ float q = (float) (elapsedTime) / duration;
+
+ if (mInterpolator == null) {
+ q = Scroller.viscousFluid(q);
+ } else {
+ q = mInterpolator.getInterpolation(q);
+ }
+
+ mScrollerX.updateScroll(q);
+ mScrollerY.updateScroll(q);
+ } else {
+ abortAnimation();
+ }
+ break;
+
+ case FLING_MODE:
+ if (!mScrollerX.mFinished) {
+ if (!mScrollerX.update()) {
+ if (!mScrollerX.continueWhenFinished()) {
+ mScrollerX.finish();
+ }
+ }
+ }
+
+ if (!mScrollerY.mFinished) {
+ if (!mScrollerY.update()) {
+ if (!mScrollerY.continueWhenFinished()) {
+ mScrollerY.finish();
+ }
+ }
+ }
+
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ * The scroll will use the default value of 250 milliseconds for the
+ * duration.
+ *
+ * @param startX
+ * Starting horizontal scroll offset in pixels. Positive numbers
+ * will scroll the content to the left.
+ * @param startY
+ * Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx
+ * Horizontal distance to travel. Positive numbers will scroll
+ * the content to the left.
+ * @param dy
+ * Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy) {
+ startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ *
+ * @param startX
+ * Starting horizontal scroll offset in pixels. Positive numbers
+ * will scroll the content to the left.
+ * @param startY
+ * Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx
+ * Horizontal distance to travel. Positive numbers will scroll
+ * the content to the left.
+ * @param dy
+ * Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ * @param duration
+ * Duration of the scroll in milliseconds.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+ mMode = SCROLL_MODE;
+ mScrollerX.startScroll(startX, dx, duration);
+ mScrollerY.startScroll(startY, dy, duration);
+ }
+
+ /**
+ * Call this when you want to 'spring back' into a valid coordinate range.
+ *
+ * @param startX
+ * Starting X coordinate
+ * @param startY
+ * Starting Y coordinate
+ * @param minX
+ * Minimum valid X value
+ * @param maxX
+ * Maximum valid X value
+ * @param minY
+ * Minimum valid Y value
+ * @param maxY
+ * Minimum valid Y value
+ * @return true if a springback was initiated, false if startX and startY
+ * were already within the valid range.
+ */
+ public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
+ mMode = FLING_MODE;
+
+ // Make sure both methods are called.
+ final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
+ final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
+ return spingbackX || spingbackY;
+ }
+
+ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
+ fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
+ }
+
+ /**
+ * Start scrolling based on a fling gesture. The distance traveled will
+ * depend on the initial velocity of the fling.
+ *
+ * @param startX
+ * Starting point of the scroll (X)
+ * @param startY
+ * Starting point of the scroll (Y)
+ * @param velocityX
+ * Initial velocity of the fling (X) measured in pixels per
+ * second.
+ * @param velocityY
+ * Initial velocity of the fling (Y) measured in pixels per
+ * second
+ * @param minX
+ * Minimum X value. The scroller will not scroll past this point
+ * unless overX > 0. If overfling is allowed, it will use minX as
+ * a springback boundary.
+ * @param maxX
+ * Maximum X value. The scroller will not scroll past this point
+ * unless overX > 0. If overfling is allowed, it will use maxX as
+ * a springback boundary.
+ * @param minY
+ * Minimum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use minY as
+ * a springback boundary.
+ * @param maxY
+ * Maximum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use maxY as
+ * a springback boundary.
+ * @param overX
+ * Overfling range. If > 0, horizontal overfling in either
+ * direction will be possible.
+ * @param overY
+ * Overfling range. If > 0, vertical overfling in either
+ * direction will be possible.
+ */
+ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY,
+ int overX, int overY) {
+ mMode = FLING_MODE;
+ mScrollerX.fling(startX, velocityX, minX, maxX, overX);
+ mScrollerY.fling(startY, velocityY, minY, maxY, overY);
+ }
+
+ /**
+ * Notify the scroller that we've reached a horizontal boundary. Normally
+ * the information to handle this will already be known when the animation
+ * is started, such as in a call to one of the fling functions. However
+ * there are cases where this cannot be known in advance. This function will
+ * transition the current motion and animate from startX to finalX as
+ * appropriate.
+ *
+ * @param startX
+ * Starting/current X position
+ * @param finalX
+ * Desired final X position
+ * @param overX
+ * Magnitude of overscroll allowed. This should be the maximum
+ * desired distance from finalX. Absolute value - must be
+ * positive.
+ */
+ public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
+ mScrollerX.notifyEdgeReached(startX, finalX, overX);
+ }
+
+ /**
+ * Notify the scroller that we've reached a vertical boundary. Normally the
+ * information to handle this will already be known when the animation is
+ * started, such as in a call to one of the fling functions. However there
+ * are cases where this cannot be known in advance. This function will
+ * animate a parabolic motion from startY to finalY.
+ *
+ * @param startY
+ * Starting/current Y position
+ * @param finalY
+ * Desired final Y position
+ * @param overY
+ * Magnitude of overscroll allowed. This should be the maximum
+ * desired distance from finalY.
+ */
+ public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
+ mScrollerY.notifyEdgeReached(startY, finalY, overY);
+ }
+
+ /**
+ * Returns whether the current Scroller is currently returning to a valid
+ * position. Valid bounds were provided by the
+ * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
+ *
+ * One should check this value before calling
+ * {@link #startScroll(int, int, int, int)} as the interpolation currently
+ * in progress to restore a valid position will then be stopped. The caller
+ * has to take into account the fact that the started scroll will start from
+ * an overscrolled position.
+ *
+ * @return true when the current position is overscrolled and in the process
+ * of interpolating back to a valid value.
+ */
+ public boolean isOverScrolled() {
+ return ((!mScrollerX.mFinished && mScrollerX.mState != MagneticOverScroller.TO_EDGE) || (!mScrollerY.mFinished && mScrollerY.mState != MagneticOverScroller.TO_EDGE));
+ }
+
+ /**
+ * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+ * aborting the animating causes the scroller to move to the final x and y
+ * positions.
+ *
+ * @see #forceFinished(boolean)
+ */
+ public void abortAnimation() {
+ mScrollerX.finish();
+ mScrollerY.finish();
+ }
+
+ /**
+ * Returns the time elapsed since the beginning of the scrolling.
+ *
+ * @return The elapsed time in milliseconds.
+ *
+ * @hide
+ */
+ public int timePassed() {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
+ return (int) (time - startTime);
+ }
+
+ static class MagneticOverScroller {
+ // Initial position
+ int mStart;
+
+ // Current position
+ int mCurrentPosition;
+
+ // Final position
+ int mFinal;
+
+ // Initial velocity
+ int mVelocity;
+
+ // Current velocity
+ float mCurrVelocity;
+
+ // Constant current deceleration
+ float mDeceleration;
+
+ // Animation starting time, in system milliseconds
+ long mStartTime;
+
+ // Animation duration, in milliseconds
+ int mDuration;
+
+ // Whether the animation is currently in progress
+ boolean mFinished;
+
+ // Constant gravity value, used to scale deceleration
+ static float GRAVITY;
+
+ static void initializeFromContext(Context context) {
+ final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+ GRAVITY = SensorManager.GRAVITY_EARTH // g (m/s^2)
+ * 39.37f // inch/meter
+ * ppi // pixels per inch
+ * ViewConfiguration.getScrollFriction();
+ }
+
+ private static final int TO_EDGE = 0;
+ private static final int TO_BOUNDARY = 1;
+ private static final int TO_BOUNCE = 2;
+
+ private int mState = TO_EDGE;
+
+ // The allowed overshot distance before boundary is reached.
+ private int mOver;
+
+ // Duration in milliseconds to go back from edge to edge. Springback is
+ // half of it.
+ private static final int OVERSCROLL_SPRINGBACK_DURATION = 300;
+
+ // Oscillation period
+ private static final float TIME_COEF = 1000.0f * (float) Math.PI / OVERSCROLL_SPRINGBACK_DURATION;
+
+ // If the velocity is smaller than this value, no bounce is triggered
+ // when the edge limits are reached (would result in a zero pixels
+ // displacement anyway).
+ private static final float MINIMUM_VELOCITY_FOR_BOUNCE = Float.MAX_VALUE;// 140.0f;
+
+ // Proportion of the velocity that is preserved when the edge is
+ // reached.
+ private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f;
+
+ private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT;
+
+ MagneticOverScroller() {
+ mFinished = true;
+ }
+
+ void updateScroll(float q) {
+ mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
+ }
+
+ /*
+ * Get a signed deceleration that will reduce the velocity.
+ */
+ static float getDeceleration(int velocity) {
+ return velocity > 0 ? -GRAVITY : GRAVITY;
+ }
+
+ /*
+ * Returns the time (in milliseconds) it will take to go from start to
+ * end.
+ */
+ static int computeDuration(int start, int end, float initialVelocity, float deceleration) {
+ final int distance = start - end;
+ final float discriminant = initialVelocity * initialVelocity - 2.0f * deceleration * distance;
+ if (discriminant >= 0.0f) {
+ float delta = (float) Math.sqrt(discriminant);
+ if (deceleration < 0.0f) {
+ delta = -delta;
+ }
+ return (int) (1000.0f * (-initialVelocity - delta) / deceleration);
+ }
+
+ // End position can not be reached
+ return 0;
+ }
+
+ void startScroll(int start, int distance, int duration) {
+ mFinished = false;
+
+ mStart = start;
+ mFinal = start + distance;
+
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = duration;
+
+ // Unused
+ mDeceleration = 0.0f;
+ mVelocity = 0;
+ }
+
+ void fling(int start, int velocity, int min, int max) {
+ mFinished = false;
+
+ mStart = start;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+
+ mVelocity = velocity;
+
+ mDeceleration = getDeceleration(velocity);
+
+ // A start from an invalid position immediately brings back to a
+ // valid position
+ if (mStart < min) {
+ mDuration = 0;
+ mFinal = min;
+ return;
+ }
+
+ if (mStart > max) {
+ mDuration = 0;
+ mFinal = max;
+ return;
+ }
+
+ // Duration are expressed in milliseconds
+ mDuration = (int) (-1000.0f * velocity / mDeceleration);
+
+ mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration));
+
+ // Clamp to a valid final position
+ if (mFinal < min) {
+ mFinal = min;
+ mDuration = computeDuration(mStart, min, mVelocity, mDeceleration);
+ }
+
+ if (mFinal > max) {
+ mFinal = max;
+ mDuration = computeDuration(mStart, max, mVelocity, mDeceleration);
+ }
+ }
+
+ void finish() {
+ mCurrentPosition = mFinal;
+ // Not reset since WebView relies on this value for fast fling.
+ // mCurrVelocity = 0.0f;
+ mFinished = true;
+ }
+
+ void setFinalPosition(int position) {
+ mFinal = position;
+ mFinished = false;
+ }
+
+ void extendDuration(int extend) {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final int elapsedTime = (int) (time - mStartTime);
+ mDuration = elapsedTime + extend;
+ mFinished = false;
+ }
+
+ void setBounceCoefficient(float coefficient) {
+ mBounceCoefficient = coefficient;
+ }
+
+ boolean springback(int start, int min, int max) {
+ mFinished = true;
+
+ mStart = start;
+ mVelocity = 0;
+
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = 0;
+
+ if (start < min) {
+ startSpringback(start, min, false);
+ } else if (start > max) {
+ startSpringback(start, max, true);
+ }
+
+ return !mFinished;
+ }
+
+ private void startSpringback(int start, int end, boolean positive) {
+ mFinished = false;
+ mState = TO_BOUNCE;
+ mStart = mFinal = end;
+ mDuration = OVERSCROLL_SPRINGBACK_DURATION;
+ mStartTime -= OVERSCROLL_SPRINGBACK_DURATION / 2;
+ mVelocity = (int) (Math.abs(end - start) * TIME_COEF * (positive ? 1.0 : -1.0f));
+ }
+
+ void fling(int start, int velocity, int min, int max, int over) {
+ mState = TO_EDGE;
+ mOver = over;
+
+ mFinished = false;
+
+ mStart = start;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+
+ mVelocity = velocity;
+
+ mDeceleration = getDeceleration(velocity);
+
+ // Duration are expressed in milliseconds
+ mDuration = (int) (-1000.0f * velocity / mDeceleration);
+
+ mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration));
+
+ // Clamp to a valid final position
+ if (mFinal < min) {
+ mFinal = min;
+ mDuration = computeDuration(mStart, min, mVelocity, mDeceleration);
+ }
+
+ if (mFinal > max) {
+ mFinal = max;
+ mDuration = computeDuration(mStart, max, mVelocity, mDeceleration);
+ }
+
+ if (start > max) {
+ if (start >= max + over) {
+ springback(max + over, min, max);
+ } else {
+ if (velocity <= 0) {
+ springback(start, min, max);
+ } else {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final double durationSinceEdge = Math.atan((start - max) * TIME_COEF / velocity) / TIME_COEF;
+ mStartTime = (int) (time - 1000.0f * durationSinceEdge);
+
+ // Simulate a bounce that started from edge
+ mStart = max;
+
+ mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
+
+ onEdgeReached();
+ }
+ }
+ } else {
+ if (start < min) {
+ if (start <= min - over) {
+ springback(min - over, min, max);
+ } else {
+ if (velocity >= 0) {
+ springback(start, min, max);
+ } else {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final double durationSinceEdge = Math.atan((start - min) * TIME_COEF / velocity)
+ / TIME_COEF;
+ mStartTime = (int) (time - 1000.0f * durationSinceEdge);
+
+ // Simulate a bounce that started from edge
+ mStart = min;
+
+ mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
+
+ onEdgeReached();
+ }
+
+ }
+ }
+ }
+ }
+
+ void notifyEdgeReached(int start, int end, int over) {
+ mDeceleration = getDeceleration(mVelocity);
+
+ // Local time, used to compute edge crossing time.
+ final float timeCurrent = mCurrVelocity / mDeceleration;
+ final int distance = end - start;
+ final float timeEdge = -(float) Math.sqrt((2.0f * distance / mDeceleration) + (timeCurrent * timeCurrent));
+
+ mVelocity = (int) (mDeceleration * timeEdge);
+
+ // Simulate a symmetric bounce that started from edge
+ mStart = end;
+
+ mOver = over;
+
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ mStartTime = (int) (time - 1000.0f * (timeCurrent - timeEdge));
+
+ onEdgeReached();
+ }
+
+ private void onEdgeReached() {
+ // mStart, mVelocity and mStartTime were adjusted to their values
+ // when edge was reached.
+ final float distance = mVelocity / TIME_COEF;
+
+ if (Math.abs(distance) < mOver) {
+ // Spring force will bring us back to final position
+ mState = TO_BOUNCE;
+ mFinal = mStart;
+ mDuration = OVERSCROLL_SPRINGBACK_DURATION;
+ } else {
+ // Velocity is too high, we will hit the boundary limit
+ mState = TO_BOUNDARY;
+ final int over = mVelocity > 0 ? mOver : -mOver;
+ mFinal = mStart + over;
+ mDuration = (int) (1000.0f * Math.asin(over / distance) / TIME_COEF);
+ }
+ }
+
+ boolean continueWhenFinished() {
+ switch (mState) {
+ case TO_EDGE:
+ // Duration from start to null velocity
+ final int duration = (int) (-1000.0f * mVelocity / mDeceleration);
+ if (mDuration < duration) {
+ // If the animation was clamped, we reached the edge
+ mStart = mFinal;
+ // Speed when edge was reached
+ mVelocity = (int) (mVelocity + mDeceleration * mDuration / 1000.0f);
+ mStartTime += mDuration;
+ onEdgeReached();
+ } else {
+ // Normal stop, no need to continue
+ return false;
+ }
+ break;
+ case TO_BOUNDARY:
+ mStartTime += mDuration;
+ startSpringback(mFinal, mFinal - (mVelocity > 0 ? mOver : -mOver), mVelocity > 0);
+ break;
+ case TO_BOUNCE:
+ // mVelocity = (int) (mVelocity * BOUNCE_COEFFICIENT);
+ mVelocity = (int) (mVelocity * mBounceCoefficient);
+ if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) {
+ return false;
+ }
+ mStartTime += mDuration;
+ break;
+ }
+
+ update();
+ return true;
+ }
+
+ /*
+ * Update the current position and velocity for current time. Returns
+ * true if update has been done and false if animation duration has been
+ * reached.
+ */
+ boolean update() {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final long duration = time - mStartTime;
+
+ if (duration > mDuration) {
+ return false;
+ }
+
+ double distance;
+ final float t = duration / 1000.0f;
+ if (mState == TO_EDGE) {
+ mCurrVelocity = mVelocity + mDeceleration * t;
+ distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+ } else {
+ final float d = t * TIME_COEF;
+ mCurrVelocity = mVelocity * (float) Math.cos(d);
+ distance = mVelocity / TIME_COEF * Math.sin(d);
+ }
+
+ mCurrentPosition = mStart + (int) distance;
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/pursuer/reader/easyrss/view/PopupMenu.java b/src/com/pursuer/reader/easyrss/view/PopupMenu.java
new file mode 100644
index 0000000..7ad8799
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/PopupMenu.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import com.pursuer.reader.easyrss.R;
+
+import android.content.Context;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+public class PopupMenu extends PopupWindow {
+ final private Context context;
+ final private ViewGroup viewGroup;
+ final private SparseArray itemView;
+ private PopupMenuListener listener;
+
+ @SuppressWarnings("deprecation")
+ public PopupMenu(final Context context) {
+ super(context);
+
+ final FrameLayout layout = new FrameLayout(context);
+ final LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.popup_menu, layout);
+ setBackgroundDrawable(new BitmapDrawable());
+ setWidth(WindowManager.LayoutParams.WRAP_CONTENT);
+ setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
+ setTouchable(true);
+ setFocusable(true);
+ setOutsideTouchable(true);
+ setAnimationStyle(R.style.Animations_PopupMenu);
+
+ setContentView(layout);
+ this.context = context;
+ this.viewGroup = (ViewGroup) layout.findViewById(R.id.ListPopupMenu);
+ this.itemView = new SparseArray();
+ }
+
+ public void addItem(final PopupMenuItem item) {
+ if (itemView.get(item.getId()) != null) {
+ return;
+ }
+ final LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ final View container = inflater.inflate(R.layout.popup_menu_item, null);
+ final ImageView img = (ImageView) container.findViewById(R.id.MenuItemIcon);
+ final TextView text = (TextView) container.findViewById(R.id.MenuItemTitle);
+ img.setImageResource(item.getResId());
+ text.setText(item.getTitle());
+
+ final int id = item.getId();
+ container.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(final View view) {
+ if (listener != null) {
+ listener.onItemClick(id);
+ }
+ dismiss();
+ }
+ });
+
+ container.setFocusable(true);
+ container.setClickable(true);
+ viewGroup.addView(container);
+ viewGroup.invalidate();
+ itemView.put(id, container);
+ }
+
+ public void clearItems() {
+ viewGroup.removeAllViews();
+ viewGroup.invalidate();
+ itemView.clear();
+ }
+
+ public PopupMenuListener getListener() {
+ return listener;
+ }
+
+ public void removeItem(final int id) {
+ final View view = itemView.get(id);
+ if (view == null) {
+ return;
+ }
+ view.setOnClickListener(null);
+ viewGroup.removeView(view);
+ viewGroup.measure(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
+ itemView.remove(id);
+ }
+
+ public void setListener(final PopupMenuListener listener) {
+ this.listener = listener;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/PopupMenuItem.java b/src/com/pursuer/reader/easyrss/view/PopupMenuItem.java
new file mode 100644
index 0000000..743aa51
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/PopupMenuItem.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+public class PopupMenuItem {
+ private final int id;
+ private final int resId;
+ private final String title;
+
+ public PopupMenuItem(final int id, final int resId, final String title) {
+ this.id = id;
+ this.resId = resId;
+ this.title = title;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public int getResId() {
+ return resId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/PopupMenuListener.java b/src/com/pursuer/reader/easyrss/view/PopupMenuListener.java
new file mode 100644
index 0000000..5035f9d
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/PopupMenuListener.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+public interface PopupMenuListener {
+ void onItemClick(int id);
+}
diff --git a/src/com/pursuer/reader/easyrss/view/ScaleGestureDetector.java b/src/com/pursuer/reader/easyrss/view/ScaleGestureDetector.java
new file mode 100644
index 0000000..de95209
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/ScaleGestureDetector.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * Detects transformation gestures involving more than one pointer
+ * ("multitouch") using the supplied {@link MotionEvent}s. The
+ * {@link OnScaleGestureListener} callback will notify users when a particular
+ * gesture event has occurred. This class should only be used with
+ * {@link MotionEvent}s reported via touch.
+ *
+ * To use this class:
+ *
+ *
Create an instance of the {@code ScaleGestureDetector} for your
+ * {@link View}
+ *
In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
+ * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback will
+ * be executed when the events occur.
+ *
+ */
+public class ScaleGestureDetector {
+ /**
+ * The listener for receiving notifications when gestures occur. If you want
+ * to listen for all the different gestures then implement this interface.
+ * If you only want to listen for a subset it might be easier to extend
+ * {@link SimpleOnScaleGestureListener}.
+ *
+ * An application will receive events in the following order:
+ *
+ *
One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
+ *
Zero or more
+ * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
+ *
One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
+ *
+ */
+ public interface OnScaleGestureListener {
+ /**
+ * Responds to scaling events for a gesture in progress. Reported by
+ * pointer motion.
+ *
+ * @param detector
+ * The detector reporting the event - use this to retrieve
+ * extended info about event state.
+ * @return Whether or not the detector should consider this event as
+ * handled. If an event was not handled, the detector will
+ * continue to accumulate movement until an event is handled.
+ * This can be useful if an application, for example, only wants
+ * to update scaling factors if the change is greater than 0.01.
+ */
+ public boolean onScale(ScaleGestureDetector detector);
+
+ /**
+ * Responds to the beginning of a scaling gesture. Reported by new
+ * pointers going down.
+ *
+ * @param detector
+ * The detector reporting the event - use this to retrieve
+ * extended info about event state.
+ * @return Whether or not the detector should continue recognizing this
+ * gesture. For example, if a gesture is beginning with a focal
+ * point outside of a region where it makes sense,
+ * onScaleBegin() may return false to ignore the rest of the
+ * gesture.
+ */
+ public boolean onScaleBegin(ScaleGestureDetector detector);
+
+ /**
+ * Responds to the end of a scale gesture. Reported by existing pointers
+ * going up.
+ *
+ * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} and
+ * {@link ScaleGestureDetector#getFocusY()} will return the location of
+ * the pointer remaining on the screen.
+ *
+ * @param detector
+ * The detector reporting the event - use this to retrieve
+ * extended info about event state.
+ */
+ public void onScaleEnd(ScaleGestureDetector detector);
+ }
+
+ /**
+ * A convenience class to extend when you only want to listen for a subset
+ * of scaling-related events. This implements all methods in
+ * {@link OnScaleGestureListener} but does nothing.
+ * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
+ * {@code false} so that a subclass can retrieve the accumulated scale
+ * factor in an overridden onScaleEnd.
+ * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
+ * {@code true}.
+ */
+ public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
+
+ public boolean onScale(ScaleGestureDetector detector) {
+ return false;
+ }
+
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return true;
+ }
+
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ // Intentionally empty
+ }
+ }
+
+ /**
+ * This value is the threshold ratio between our previous combined pressure
+ * and the current combined pressure. We will only fire an onScale event if
+ * the computed ratio between the current and previous event pressures is
+ * greater than this value. When pressure decreases rapidly between events
+ * the position values can often be imprecise, as it usually indicates that
+ * the user is in the process of lifting a pointer off of the device. Its
+ * value was tuned experimentally.
+ */
+ private static final float PRESSURE_THRESHOLD = 0.67f;
+
+ private final Context mContext;
+ private final OnScaleGestureListener mListener;
+ private boolean mGestureInProgress;
+
+ private MotionEvent mPrevEvent;
+ private MotionEvent mCurrEvent;
+
+ private float mFocusX;
+ private float mFocusY;
+ private float mPrevFingerDiffX;
+ private float mPrevFingerDiffY;
+ private float mCurrFingerDiffX;
+ private float mCurrFingerDiffY;
+ private float mCurrLen;
+ private float mPrevLen;
+ private float mScaleFactor;
+ private float mCurrPressure;
+ private float mPrevPressure;
+ private long mTimeDelta;
+
+ private final float mEdgeSlop;
+ private float mRightSlopEdge;
+ private float mBottomSlopEdge;
+ private boolean mSloppyGesture;
+
+ public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
+ ViewConfiguration config = ViewConfiguration.get(context);
+ mContext = context;
+ mListener = listener;
+ mEdgeSlop = config.getScaledEdgeSlop();
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ final int action = event.getAction();
+ boolean handled = true;
+
+ if (!mGestureInProgress) {
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ // We have a new multi-finger gesture
+
+ // as orientation can change, query the metrics in touch down
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+ mRightSlopEdge = metrics.widthPixels - mEdgeSlop;
+ mBottomSlopEdge = metrics.heightPixels - mEdgeSlop;
+
+ // Be paranoid in case we missed an event
+ reset();
+
+ mPrevEvent = MotionEvent.obtain(event);
+ mTimeDelta = 0;
+
+ setContext(event);
+
+ // Check if we have a sloppy gesture. If so, delay
+ // the beginning of the gesture until we're sure that's
+ // what the user wanted. Sloppy gestures can happen if the
+ // edge of the user's hand is touching the screen, for example.
+ final float edgeSlop = mEdgeSlop;
+ final float rightSlop = mRightSlopEdge;
+ final float bottomSlop = mBottomSlopEdge;
+ final float x0 = event.getRawX();
+ final float y0 = event.getRawY();
+ final float x1 = getRawX(event, 1);
+ final float y1 = getRawY(event, 1);
+
+ boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop || x0 > rightSlop || y0 > bottomSlop;
+ boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop || x1 > rightSlop || y1 > bottomSlop;
+
+ if (p0sloppy && p1sloppy) {
+ mFocusX = -1;
+ mFocusY = -1;
+ mSloppyGesture = true;
+ } else if (p0sloppy) {
+ mFocusX = event.getX(1);
+ mFocusY = event.getY(1);
+ mSloppyGesture = true;
+ } else if (p1sloppy) {
+ mFocusX = event.getX(0);
+ mFocusY = event.getY(0);
+ mSloppyGesture = true;
+ } else {
+ mGestureInProgress = mListener.onScaleBegin(this);
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mSloppyGesture) {
+ // Initiate sloppy gestures if we've moved outside of the
+ // slop area.
+ final float edgeSlop = mEdgeSlop;
+ final float rightSlop = mRightSlopEdge;
+ final float bottomSlop = mBottomSlopEdge;
+ final float x0 = event.getRawX();
+ final float y0 = event.getRawY();
+ final float x1 = getRawX(event, 1);
+ final float y1 = getRawY(event, 1);
+
+ boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop || x0 > rightSlop || y0 > bottomSlop;
+ boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop || x1 > rightSlop || y1 > bottomSlop;
+
+ if (p0sloppy && p1sloppy) {
+ mFocusX = -1;
+ mFocusY = -1;
+ } else if (p0sloppy) {
+ mFocusX = event.getX(1);
+ mFocusY = event.getY(1);
+ } else if (p1sloppy) {
+ mFocusX = event.getX(0);
+ mFocusY = event.getY(0);
+ } else {
+ mSloppyGesture = false;
+ mGestureInProgress = mListener.onScaleBegin(this);
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ if (mSloppyGesture) {
+ // Set focus point to the remaining finger
+ // int id = (((action &
+ // MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+ // MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1
+ // : 0;
+ final int id = 0;
+ mFocusX = event.getX(id);
+ mFocusY = event.getY(id);
+ }
+ break;
+ }
+ } else {
+ // Transform gesture in progress - attempt to handle it
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_POINTER_UP:
+ // Gesture ended
+ setContext(event);
+
+ // Set focus point to the remaining finger
+ // int id = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ // >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1
+ // : 0;
+ final int id = 0;
+ mFocusX = event.getX(id);
+ mFocusY = event.getY(id);
+
+ if (!mSloppyGesture) {
+ mListener.onScaleEnd(this);
+ }
+
+ reset();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (!mSloppyGesture) {
+ mListener.onScaleEnd(this);
+ }
+
+ reset();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ setContext(event);
+
+ // Only accept the event if our relative pressure is within
+ // a certain limit - this can help filter shaky data as a
+ // finger is lifted.
+ if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
+ final boolean updatePrevious = mListener.onScale(this);
+
+ if (updatePrevious) {
+ mPrevEvent.recycle();
+ mPrevEvent = MotionEvent.obtain(event);
+ }
+ }
+ break;
+ }
+ }
+ return handled;
+ }
+
+ /**
+ * MotionEvent has no getRawX(int) method; simulate it pending future API
+ * approval.
+ */
+ private static float getRawX(MotionEvent event, int pointerIndex) {
+ float offset = event.getX() - event.getRawX();
+ return event.getX(pointerIndex) + offset;
+ }
+
+ /**
+ * MotionEvent has no getRawY(int) method; simulate it pending future API
+ * approval.
+ */
+ private static float getRawY(MotionEvent event, int pointerIndex) {
+ float offset = event.getY() - event.getRawY();
+ return event.getY(pointerIndex) + offset;
+ }
+
+ private void setContext(MotionEvent curr) {
+ if (mCurrEvent != null) {
+ mCurrEvent.recycle();
+ }
+ mCurrEvent = MotionEvent.obtain(curr);
+
+ mCurrLen = -1;
+ mPrevLen = -1;
+ mScaleFactor = -1;
+
+ final MotionEvent prev = mPrevEvent;
+
+ final float px0 = prev.getX(0);
+ final float py0 = prev.getY(0);
+ final float px1 = prev.getX(1);
+ final float py1 = prev.getY(1);
+ final float cx0 = curr.getX(0);
+ final float cy0 = curr.getY(0);
+ final float cx1 = curr.getX(1);
+ final float cy1 = curr.getY(1);
+
+ final float pvx = px1 - px0;
+ final float pvy = py1 - py0;
+ final float cvx = cx1 - cx0;
+ final float cvy = cy1 - cy0;
+ mPrevFingerDiffX = pvx;
+ mPrevFingerDiffY = pvy;
+ mCurrFingerDiffX = cvx;
+ mCurrFingerDiffY = cvy;
+
+ mFocusX = cx0 + cvx * 0.5f;
+ mFocusY = cy0 + cvy * 0.5f;
+ mTimeDelta = curr.getEventTime() - prev.getEventTime();
+ mCurrPressure = curr.getPressure(0) + curr.getPressure(1);
+ mPrevPressure = prev.getPressure(0) + prev.getPressure(1);
+ }
+
+ private void reset() {
+ if (mPrevEvent != null) {
+ mPrevEvent.recycle();
+ mPrevEvent = null;
+ }
+ if (mCurrEvent != null) {
+ mCurrEvent.recycle();
+ mCurrEvent = null;
+ }
+ mSloppyGesture = false;
+ mGestureInProgress = false;
+ }
+
+ /**
+ * Returns {@code true} if a two-finger scale gesture is in progress.
+ *
+ * @return {@code true} if a scale gesture is in progress, {@code false}
+ * otherwise.
+ */
+ public boolean isInProgress() {
+ return mGestureInProgress;
+ }
+
+ /**
+ * Get the X coordinate of the current gesture's focal point. If a gesture
+ * is in progress, the focal point is directly between the two pointers
+ * forming the gesture. If a gesture is ending, the focal point is the
+ * location of the remaining pointer on the screen. If
+ * {@link #isInProgress()} would return false, the result of this function
+ * is undefined.
+ *
+ * @return X coordinate of the focal point in pixels.
+ */
+ public float getFocusX() {
+ return mFocusX;
+ }
+
+ /**
+ * Get the Y coordinate of the current gesture's focal point. If a gesture
+ * is in progress, the focal point is directly between the two pointers
+ * forming the gesture. If a gesture is ending, the focal point is the
+ * location of the remaining pointer on the screen. If
+ * {@link #isInProgress()} would return false, the result of this function
+ * is undefined.
+ *
+ * @return Y coordinate of the focal point in pixels.
+ */
+ public float getFocusY() {
+ return mFocusY;
+ }
+
+ /**
+ * Return the current distance between the two pointers forming the gesture
+ * in progress.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpan() {
+ if (mCurrLen == -1) {
+ final float cvx = mCurrFingerDiffX;
+ final float cvy = mCurrFingerDiffY;
+ mCurrLen = FloatMath.sqrt(cvx * cvx + cvy * cvy);
+ }
+ return mCurrLen;
+ }
+
+ /**
+ * Return the previous distance between the two pointers forming the gesture
+ * in progress.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpan() {
+ if (mPrevLen == -1) {
+ final float pvx = mPrevFingerDiffX;
+ final float pvy = mPrevFingerDiffY;
+ mPrevLen = FloatMath.sqrt(pvx * pvx + pvy * pvy);
+ }
+ return mPrevLen;
+ }
+
+ /**
+ * Return the scaling factor from the previous scale event to the current
+ * event. This value is defined as ({@link #getCurrentSpan()} /
+ * {@link #getPreviousSpan()}).
+ *
+ * @return The current scaling factor.
+ */
+ public float getScaleFactor() {
+ if (mScaleFactor == -1) {
+ mScaleFactor = getCurrentSpan() / getPreviousSpan();
+ }
+ return mScaleFactor;
+ }
+
+ /**
+ * Return the time difference in milliseconds between the previous accepted
+ * scaling event and the current scaling event.
+ *
+ * @return Time difference since the last scaling event in milliseconds.
+ */
+ public long getTimeDelta() {
+ return mTimeDelta;
+ }
+
+ /**
+ * Return the event time of the current event being processed.
+ *
+ * @return Current event time in milliseconds.
+ */
+ public long getEventTime() {
+ return mCurrEvent.getEventTime();
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/Scroller.java b/src/com/pursuer/reader/easyrss/view/Scroller.java
new file mode 100644
index 0000000..42ccfd4
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/Scroller.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+/**
+ * This class encapsulates scrolling. The duration of the scroll can be passed
+ * in the constructor and specifies the maximum time that the scrolling
+ * animation should take. Past this time, the scrolling is automatically moved
+ * to its final stage and computeScrollOffset() will always return false to
+ * indicate that scrolling is over.
+ */
+public class Scroller {
+ private int mMode;
+
+ private int mStartX;
+ private int mStartY;
+ private int mFinalX;
+ private int mFinalY;
+
+ private int mMinX;
+ private int mMaxX;
+ private int mMinY;
+ private int mMaxY;
+
+ private int mCurrX;
+ private int mCurrY;
+ private long mStartTime;
+ private int mDuration;
+ private float mDurationReciprocal;
+ private float mDeltaX;
+ private float mDeltaY;
+ private boolean mFinished;
+ private Interpolator mInterpolator;
+
+ private float mCoeffX = 0.0f;
+ private float mCoeffY = 1.0f;
+ private float mVelocity;
+
+ private static final int DEFAULT_DURATION = 250;
+ private static final int SCROLL_MODE = 0;
+ private static final int FLING_MODE = 1;
+
+ private final float mDeceleration;
+
+ private static float sViscousFluidScale;
+ private static float sViscousFluidNormalize;
+
+ static {
+ // This controls the viscous fluid effect (how much of it)
+ sViscousFluidScale = 8.0f;
+ // must be set to 1.0 (used in viscousFluid())
+ sViscousFluidNormalize = 1.0f;
+ sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
+ }
+
+ /**
+ * Create a Scroller with the default duration and interpolator.
+ */
+ public Scroller(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Create a Scroller with the specified interpolator. If the interpolator is
+ * null, the default (viscous) interpolator will be used.
+ */
+ public Scroller(Context context, Interpolator interpolator) {
+ mFinished = true;
+ mInterpolator = interpolator;
+ final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+ mDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2)
+ * 39.37f // inch/meter
+ * ppi // pixels per inch
+ * ViewConfiguration.getScrollFriction();
+ }
+
+ /**
+ *
+ * Returns whether the scroller has finished scrolling.
+ *
+ * @return True if the scroller has finished scrolling, false otherwise.
+ */
+ public final boolean isFinished() {
+ return mFinished;
+ }
+
+ /**
+ * Force the finished field to a particular value.
+ *
+ * @param finished
+ * The new finished value.
+ */
+ public final void forceFinished(boolean finished) {
+ mFinished = finished;
+ }
+
+ /**
+ * Returns how long the scroll event will take, in milliseconds.
+ *
+ * @return The duration of the scroll in milliseconds.
+ */
+ public final int getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Returns the current X offset in the scroll.
+ *
+ * @return The new X offset as an absolute distance from the origin.
+ */
+ public final int getCurrX() {
+ return mCurrX;
+ }
+
+ /**
+ * Returns the current Y offset in the scroll.
+ *
+ * @return The new Y offset as an absolute distance from the origin.
+ */
+ public final int getCurrY() {
+ return mCurrY;
+ }
+
+ /**
+ * @hide Returns the current velocity.
+ *
+ * @return The original velocity less the deceleration. Result may be
+ * negative.
+ */
+ public float getCurrVelocity() {
+ return mVelocity - mDeceleration * timePassed() / 2000.0f;
+ }
+
+ /**
+ * Returns the start X offset in the scroll.
+ *
+ * @return The start X offset as an absolute distance from the origin.
+ */
+ public final int getStartX() {
+ return mStartX;
+ }
+
+ /**
+ * Returns the start Y offset in the scroll.
+ *
+ * @return The start Y offset as an absolute distance from the origin.
+ */
+ public final int getStartY() {
+ return mStartY;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final X offset as an absolute distance from the origin.
+ */
+ public final int getFinalX() {
+ return mFinalX;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final Y offset as an absolute distance from the origin.
+ */
+ public final int getFinalY() {
+ return mFinalY;
+ }
+
+ /**
+ * Call this when you want to know the new location. If it returns true, the
+ * animation is not yet finished. loc will be altered to provide the new
+ * location.
+ */
+ public boolean computeScrollOffset() {
+ if (mFinished) {
+ return false;
+ }
+
+ final int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+
+ if (timePassed < mDuration) {
+ switch (mMode) {
+ case SCROLL_MODE:
+ float x = (float) timePassed * mDurationReciprocal;
+
+ if (mInterpolator == null) {
+ x = viscousFluid(x);
+ } else {
+ x = mInterpolator.getInterpolation(x);
+ }
+
+ mCurrX = mStartX + Math.round(x * mDeltaX);
+ mCurrY = mStartY + Math.round(x * mDeltaY);
+ break;
+ case FLING_MODE:
+ final float timePassedSeconds = timePassed / 1000.0f;
+ final float distance = (mVelocity * timePassedSeconds)
+ - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);
+
+ mCurrX = mStartX + Math.round(distance * mCoeffX);
+ // Pin to mMinX <= mCurrX <= mMaxX
+ mCurrX = Math.min(mCurrX, mMaxX);
+ mCurrX = Math.max(mCurrX, mMinX);
+
+ mCurrY = mStartY + Math.round(distance * mCoeffY);
+ // Pin to mMinY <= mCurrY <= mMaxY
+ mCurrY = Math.min(mCurrY, mMaxY);
+ mCurrY = Math.max(mCurrY, mMinY);
+
+ if (mCurrX == mFinalX && mCurrY == mFinalY) {
+ mFinished = true;
+ }
+
+ break;
+ }
+ } else {
+ mCurrX = mFinalX;
+ mCurrY = mFinalY;
+ mFinished = true;
+ }
+ return true;
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ * The scroll will use the default value of 250 milliseconds for the
+ * duration.
+ *
+ * @param startX
+ * Starting horizontal scroll offset in pixels. Positive numbers
+ * will scroll the content to the left.
+ * @param startY
+ * Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx
+ * Horizontal distance to travel. Positive numbers will scroll
+ * the content to the left.
+ * @param dy
+ * Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy) {
+ startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ *
+ * @param startX
+ * Starting horizontal scroll offset in pixels. Positive numbers
+ * will scroll the content to the left.
+ * @param startY
+ * Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx
+ * Horizontal distance to travel. Positive numbers will scroll
+ * the content to the left.
+ * @param dy
+ * Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ * @param duration
+ * Duration of the scroll in milliseconds.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+ mMode = SCROLL_MODE;
+ mFinished = false;
+ mDuration = duration;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mStartX = startX;
+ mStartY = startY;
+ mFinalX = startX + dx;
+ mFinalY = startY + dy;
+ mDeltaX = dx;
+ mDeltaY = dy;
+ mDurationReciprocal = 1.0f / (float) mDuration;
+ }
+
+ /**
+ * Start scrolling based on a fling gesture. The distance travelled will
+ * depend on the initial velocity of the fling.
+ *
+ * @param startX
+ * Starting point of the scroll (X)
+ * @param startY
+ * Starting point of the scroll (Y)
+ * @param velocityX
+ * Initial velocity of the fling (X) measured in pixels per
+ * second.
+ * @param velocityY
+ * Initial velocity of the fling (Y) measured in pixels per
+ * second
+ * @param minX
+ * Minimum X value. The scroller will not scroll past this point.
+ * @param maxX
+ * Maximum X value. The scroller will not scroll past this point.
+ * @param minY
+ * Minimum Y value. The scroller will not scroll past this point.
+ * @param maxY
+ * Maximum Y value. The scroller will not scroll past this point.
+ */
+ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
+ mMode = FLING_MODE;
+ mFinished = false;
+
+ final float velocity = (float) Math.hypot(velocityX, velocityY);
+
+ mVelocity = velocity;
+ mDuration = (int) (1000 * velocity / mDeceleration); // Duration is in
+ // milliseconds
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mStartX = startX;
+ mStartY = startY;
+
+ mCoeffX = velocity == 0 ? 1.0f : velocityX / velocity;
+ mCoeffY = velocity == 0 ? 1.0f : velocityY / velocity;
+
+ final int totalDistance = (int) ((velocity * velocity) / (2 * mDeceleration));
+
+ mMinX = minX;
+ mMaxX = maxX;
+ mMinY = minY;
+ mMaxY = maxY;
+
+ mFinalX = startX + Math.round(totalDistance * mCoeffX);
+ // Pin to mMinX <= mFinalX <= mMaxX
+ mFinalX = Math.min(mFinalX, mMaxX);
+ mFinalX = Math.max(mFinalX, mMinX);
+
+ mFinalY = startY + Math.round(totalDistance * mCoeffY);
+ // Pin to mMinY <= mFinalY <= mMaxY
+ mFinalY = Math.min(mFinalY, mMaxY);
+ mFinalY = Math.max(mFinalY, mMinY);
+ }
+
+ static float viscousFluid(float x) {
+ x *= sViscousFluidScale;
+ if (x < 1.0f) {
+ x -= (1.0f - (float) Math.exp(-x));
+ } else {
+ final float start = 0.36787944117f; // 1/e == exp(-1)
+ x = 1.0f - (float) Math.exp(1.0f - x);
+ x = start + x * (1.0f - start);
+ }
+ x *= sViscousFluidNormalize;
+ return x;
+ }
+
+ /**
+ * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+ * aborting the animating cause the scroller to move to the final x and y
+ * position
+ *
+ * @see #forceFinished(boolean)
+ */
+ public void abortAnimation() {
+ mCurrX = mFinalX;
+ mCurrY = mFinalY;
+ mFinished = true;
+ }
+
+ /**
+ * Extend the scroll animation. This allows a running animation to scroll
+ * further and longer, when used with {@link #setFinalX(int)} or
+ * {@link #setFinalY(int)}.
+ *
+ * @param extend
+ * Additional time to scroll in milliseconds.
+ * @see #setFinalX(int)
+ * @see #setFinalY(int)
+ */
+ public void extendDuration(int extend) {
+ final int passed = timePassed();
+ mDuration = passed + extend;
+ mDurationReciprocal = 1.0f / (float) mDuration;
+ mFinished = false;
+ }
+
+ /**
+ * Returns the time elapsed since the beginning of the scrolling.
+ *
+ * @return The elapsed time in milliseconds.
+ */
+ public int timePassed() {
+ return (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+ }
+
+ /**
+ * Sets the final position (X) for this scroller.
+ *
+ * @param newX
+ * The new X offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalY(int)
+ */
+ public void setFinalX(int newX) {
+ mFinalX = newX;
+ mDeltaX = mFinalX - mStartX;
+ mFinished = false;
+ }
+
+ /**
+ * Sets the final position (Y) for this scroller.
+ *
+ * @param newY
+ * The new Y offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalX(int)
+ */
+ public void setFinalY(int newY) {
+ mFinalY = newY;
+ mDeltaY = mFinalY - mStartY;
+ mFinished = false;
+ }
+}
\ No newline at end of file
diff --git a/src/com/pursuer/reader/easyrss/view/StaticInterpolator.java b/src/com/pursuer/reader/easyrss/view/StaticInterpolator.java
new file mode 100644
index 0000000..205e7c5
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/StaticInterpolator.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.view.animation.Interpolator;
+
+public class StaticInterpolator implements Interpolator {
+ float staticValue;
+
+ public StaticInterpolator() {
+ staticValue = 0;
+ }
+
+ public StaticInterpolator(final float staticValue) {
+ this.staticValue = staticValue;
+ }
+
+ @Override
+ public float getInterpolation(final float duration) {
+ return staticValue;
+ }
+
+ public float getStaticValue() {
+ return staticValue;
+ }
+
+ public void setStaticValue(final float staticValue) {
+ this.staticValue = staticValue;
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/TouchImageView.java b/src/com/pursuer/reader/easyrss/view/TouchImageView.java
new file mode 100755
index 0000000..aceec7b
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/TouchImageView.java
@@ -0,0 +1,251 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+
+public class TouchImageView extends ImageView {
+ private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ @Override
+ public boolean onScale(final ScaleGestureDetector detector) {
+ setScale(detector.getScaleFactor() * savedScale, detector.getFocusX(), detector.getFocusY());
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(final ScaleGestureDetector detector) {
+ mode = STATE_ZOOM;
+ return true;
+ }
+ }
+
+ private static final int CLICK_DISTANCE = 10;
+ // We can be in one of these 3 states
+ private static final int STATE_NONE = 0;
+ private static final int STATE_DRAG = 1;
+ private static final int STATE_ZOOM = 2;
+
+ final private Matrix matrix = new Matrix();
+ private OnScaleChangedListener onScaleChangedListener;
+ private int mode = STATE_NONE;
+ private PointF last = new PointF();
+ private PointF start = new PointF();
+ private float minScale = 1.0f;
+ private float maxScale = 2.0f;
+ private float savedScale = 1.0f;
+ private float width, height;
+ private float bmWidth, bmHeight;
+ private boolean autoFillScreen;
+ final private ScaleGestureDetector mScaleDetector;
+
+ public TouchImageView(final Context context) {
+ super(context);
+
+ this.mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
+ init();
+ }
+
+ public TouchImageView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ this.mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
+ init();
+ }
+
+ public float getMaxScale() {
+ return maxScale;
+ }
+
+ public float getMinScale() {
+ return minScale;
+ }
+
+ public OnScaleChangedListener getOnScaleChangedListener() {
+ return onScaleChangedListener;
+ }
+
+ public float getScale() {
+ return savedScale;
+ }
+
+ private void init() {
+ autoFillScreen = false;
+ setClickable(true);
+ matrix.setTranslate(1f, 1f);
+ setImageMatrix(matrix);
+ setScaleType(ScaleType.MATRIX);
+ setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(final View view, final MotionEvent event) {
+ mScaleDetector.onTouchEvent(event);
+
+ final float m[] = new float[9];
+ matrix.getValues(m);
+ final float x = m[Matrix.MTRANS_X];
+ final float y = m[Matrix.MTRANS_Y];
+ final PointF curr = new PointF(event.getX(), event.getY());
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ last.set(event.getX(), event.getY());
+ start.set(last);
+ mode = STATE_DRAG;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mode == STATE_DRAG) {
+ float deltaX = curr.x - last.x;
+ float deltaY = curr.y - last.y;
+ final float scaleWidth = Math.round(bmWidth * savedScale);
+ final float scaleHeight = Math.round(bmHeight * savedScale);
+ if (scaleWidth < width) {
+ deltaX = 0;
+ } else if (x + deltaX > 0) {
+ deltaX = -x;
+ } else if (x + deltaX < -scaleWidth + width) {
+ deltaX = -(x + scaleWidth - width);
+ }
+ if (scaleHeight < height) {
+ deltaY = 0;
+ } else if (y + deltaY > 0) {
+ deltaY = -y;
+ } else if (y + deltaY < -scaleHeight + height) {
+ deltaY = -(y + scaleHeight - height);
+ }
+ matrix.postTranslate(deltaX, deltaY);
+ last.set(curr.x, curr.y);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ mode = STATE_NONE;
+ int xDiff = (int) Math.abs(curr.x - start.x);
+ int yDiff = (int) Math.abs(curr.y - start.y);
+ if (xDiff < CLICK_DISTANCE && yDiff < CLICK_DISTANCE) {
+ performClick();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ mode = STATE_NONE;
+ break;
+ }
+ setImageMatrix(matrix);
+ invalidate();
+ return true;
+ }
+ });
+ }
+
+ public boolean isAutoFillScreen() {
+ return autoFillScreen;
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ width = MeasureSpec.getSize(widthMeasureSpec);
+ height = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (autoFillScreen) {
+ float scale = 1.0f;
+ scale = Math.min(scale, width / bmWidth);
+ scale = Math.min(scale, height / bmHeight);
+ if (scale < minScale) {
+ minScale = scale;
+ }
+ savedScale = scale;
+ }
+ final float scaledWidth = Math.round(bmWidth * savedScale);
+ final float scaledHeight = Math.round(bmHeight * savedScale);
+ matrix.setScale(savedScale, savedScale);
+ matrix.postTranslate((width - scaledWidth) / 2.0f, (height - scaledHeight) / 2.0f);
+ setImageMatrix(matrix);
+ }
+
+ public void setAutoFillScreen(final boolean autoFillScreen) {
+ this.autoFillScreen = autoFillScreen;
+ }
+
+ @Override
+ public void setImageBitmap(final Bitmap bitmap) {
+ super.setImageBitmap(bitmap);
+ if (bitmap != null) {
+ bmWidth = bitmap.getWidth();
+ bmHeight = bitmap.getHeight();
+ }
+ }
+
+ public void setMaxScale(final float maxScale) {
+ this.maxScale = maxScale;
+ }
+
+ public void setMaxZoom(final float x) {
+ maxScale = x;
+ }
+
+ public void setMinScale(final float minScale) {
+ this.minScale = minScale;
+ }
+
+ public void setOnScaleChangedListener(final OnScaleChangedListener onScaleChangedListener) {
+ this.onScaleChangedListener = onScaleChangedListener;
+ }
+
+ public void setScale(final float scale) {
+ setScale(scale, width / 2.0f, height / 2.0f);
+ }
+
+ public void setScale(float scale, final float posX, final float posY) {
+ if (scale > maxScale) {
+ scale = maxScale;
+ } else if (scale < minScale) {
+ scale = minScale;
+ }
+ matrix.postScale(scale / savedScale, scale / savedScale, posX, posY);
+ savedScale = scale;
+ final float m[] = new float[9];
+ matrix.getValues(m);
+ final float x = m[Matrix.MTRANS_X];
+ final float y = m[Matrix.MTRANS_Y];
+ final float scaledWidth = Math.round(bmWidth * savedScale);
+ final float scaledHeight = Math.round(bmHeight * savedScale);
+ final float offX, offY;
+ if (scaledWidth < width) {
+ offX = (width - scaledWidth) / 2.0f - x;
+ } else if (x > 0) {
+ offX = -x;
+ } else if (x < -scaledWidth + width) {
+ offX = -scaledWidth + width - x;
+ } else {
+ offX = 0f;
+ }
+ if (scaledHeight < height) {
+ offY = (height - scaledHeight) / 2.0f - y;
+ } else if (y > 0) {
+ offY = -y;
+ } else if (y < -scaledHeight + height) {
+ offY = -scaledHeight + height - y;
+ } else {
+ offY = 0f;
+ }
+ matrix.postTranslate(offX, offY);
+ setImageMatrix(matrix);
+ invalidate();
+ if (onScaleChangedListener != null) {
+ onScaleChangedListener.onScaleChanged(this, savedScale);
+ }
+ }
+}
diff --git a/src/com/pursuer/reader/easyrss/view/ViewCtrlListener.java b/src/com/pursuer/reader/easyrss/view/ViewCtrlListener.java
new file mode 100644
index 0000000..79c33a0
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/ViewCtrlListener.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+public interface ViewCtrlListener {
+ void onBackNeeded();
+
+ void onImageViewRequired(String imgPath);
+
+ void onItemListSelected(String uid, int viewType);
+
+ void onItemSelected(String uid);
+
+ void onLogin(boolean succeeded);
+
+ void onLogoutRequired();
+
+ void onWebsiteViewSelected(String uid, boolean isMobilized);
+
+ void onReloadRequired(boolean showSettings);
+
+ void onSettingsSelected();
+
+ void onSyncRequired();
+}
diff --git a/src/com/pursuer/reader/easyrss/view/ViewManager.java b/src/com/pursuer/reader/easyrss/view/ViewManager.java
new file mode 100644
index 0000000..af88043
--- /dev/null
+++ b/src/com/pursuer/reader/easyrss/view/ViewManager.java
@@ -0,0 +1,154 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Pursuer (http://pursuer.me).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Contributors:
+ * Pursuer - initial API and implementation
+ ******************************************************************************/
+
+package com.pursuer.reader.easyrss.view;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.pursuer.reader.easyrss.ImageViewCtrl;
+import com.pursuer.reader.easyrss.R;
+import com.pursuer.reader.easyrss.SettingsViewCtrl;
+
+import android.app.Activity;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Animation.AnimationListener;
+import android.widget.ViewFlipper;
+
+public class ViewManager {
+ final private Activity activity;
+ final private List viewCtrls;
+ final private ViewFlipper flipper;
+
+ public ViewManager(final Activity activity) {
+ this.viewCtrls = new ArrayList();
+ this.activity = activity;
+ this.flipper = (ViewFlipper) activity.findViewById(R.id.GlobalFlipper);
+ }
+
+ public void clearViews() {
+ for (int i = 0; i < viewCtrls.size(); i++) {
+ viewCtrls.get(i).onDestory();
+ }
+ flipper.removeAllViews();
+ viewCtrls.clear();
+ }
+
+ public int getLastViewResId() {
+ return viewCtrls.get(viewCtrls.size() - 1).getResId();
+ }
+
+ public AbsViewCtrl getTopView() {
+ return viewCtrls.isEmpty() ? null : viewCtrls.get(viewCtrls.size() - 1);
+ }
+
+ public int getViewCount() {
+ return viewCtrls.size();
+ }
+
+ public void popView() {
+ popView(-1, -1);
+ }
+
+ public void popView(final Animation inAnimation, final Animation outAnimation) {
+ if (viewCtrls.isEmpty()) {
+ return;
+ }
+ flipper.setInAnimation(inAnimation);
+
+ final AbsViewCtrl lastView = viewCtrls.get(viewCtrls.size() - 1);
+ flipper.setOutAnimation(outAnimation);
+ if (outAnimation == null) {
+ flipper.showPrevious();
+ lastView.onDeactivate();
+ lastView.onDestory();
+ } else {
+ outAnimation.setAnimationListener(new AnimationListener() {
+ @Override
+ public void onAnimationEnd(final Animation animation) {
+ if (!viewCtrls.isEmpty()) {
+ viewCtrls.get(viewCtrls.size() - 1).getView().disableCache();
+ }
+ lastView.onDestory();
+ }
+
+ @Override
+ public void onAnimationRepeat(final Animation animation) {
+ // TODO empty method
+ }
+
+ @Override
+ public void onAnimationStart(final Animation animation) {
+ // TODO empty method
+ }
+ });
+ flipper.showPrevious();
+ lastView.onDeactivate();
+ }
+ viewCtrls.remove(lastView);
+ flipper.removeView((View) lastView.getView());
+ if (!viewCtrls.isEmpty()) {
+ viewCtrls.get(viewCtrls.size() - 1).onActivate();
+ }
+ }
+
+ public void popView(final int inAnimation, final int outAnimation) {
+ final Animation inAnim = (inAnimation == -1) ? (null) : (AnimationUtils.loadAnimation(activity, inAnimation));
+ final Animation outAnim = (outAnimation == -1) ? (null)
+ : (AnimationUtils.loadAnimation(activity, outAnimation));
+ popView(inAnim, outAnim);
+ }
+
+ public void pushView(final AbsViewCtrl view) {
+ pushView(view, -1, -1);
+ }
+
+ public void setStaticAnimation(final Animation inAnimation, final Animation outAnimation) {
+ final int size = viewCtrls.size();
+ viewCtrls.get(size - 2).getView().setAnimation(inAnimation);
+ viewCtrls.get(size - 1).getView().setAnimation(outAnimation);
+ flipper.invalidate();
+ }
+
+ public void restoreTopView() {
+ final AbsViewCtrl topView = viewCtrls.get(viewCtrls.size() - 1);
+ if (!(topView instanceof SettingsViewCtrl) && !(topView instanceof ImageViewCtrl)) {
+ topView.getView().disableCache();
+ }
+ }
+
+ public void pushView(final AbsViewCtrl view, final int inAnimation, final int outAnimation) {
+ if (!viewCtrls.isEmpty()) {
+ final AbsViewCtrl topView = viewCtrls.get(viewCtrls.size() - 1);
+ if (!(topView instanceof SettingsViewCtrl) && !(topView instanceof ImageViewCtrl)) {
+ topView.getView().enableCache();
+ }
+ topView.onDeactivate();
+ }
+ flipper.addView((View) view.getView());
+ if (inAnimation == -1) {
+ flipper.setInAnimation(null);
+ } else {
+ flipper.setInAnimation(AnimationUtils.loadAnimation(activity, inAnimation));
+ }
+ if (outAnimation == -1) {
+ flipper.setOutAnimation(null);
+ } else {
+ flipper.setOutAnimation(AnimationUtils.loadAnimation(activity, outAnimation));
+ }
+ flipper.showNext();
+ viewCtrls.add(view);
+ view.onCreate();
+ view.onActivate();
+ }
+}
diff --git a/src/org/htmlcleaner/BaseToken.java b/src/org/htmlcleaner/BaseToken.java
new file mode 100644
index 0000000..200aae0
--- /dev/null
+++ b/src/org/htmlcleaner/BaseToken.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright 2011 Zheng Sun
+ *
+ * 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.
+ ******************************************************************************/
+
+/* Copyright (c) 2006-2007, Vladimir Nikic
+ All rights reserved.
+
+ Redistribution and use of this software in source and binary forms,
+ with or without modification, are permitted provided that the following
+ conditions are met:
+
+ * Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the
+ following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the
+ following disclaimer in the documentation and/or other
+ materials provided with the distribution.
+
+ * The name of HtmlCleaner may not be used to endorse or promote
+ products derived from this software without specific prior
+ written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+ You can contact Vladimir Nikic by sending e-mail to
+ nikic_vladimir@yahoo.com. Please include the word "HtmlCleaner" in the
+ subject line.
+ */
+
+package org.htmlcleaner;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ *
+ * Base token interface. Tokens are individual entities recognized by HTML
+ * parser.
+ *
+ */
+public interface BaseToken {
+ void serialize(Serializer serializer, Writer writer) throws IOException;
+}
diff --git a/src/org/htmlcleaner/BrowserCompactXmlSerializer.java b/src/org/htmlcleaner/BrowserCompactXmlSerializer.java
new file mode 100644
index 0000000..ee5e518
--- /dev/null
+++ b/src/org/htmlcleaner/BrowserCompactXmlSerializer.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * Copyright 2011 Zheng Sun
+ *
+ * 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.
+ ******************************************************************************/
+
+/* Copyright (c) 2006-2007, Vladimir Nikic
+ All rights reserved.
+
+ Redistribution and use of this software in source and binary forms,
+ with or without modification, are permitted provided that the following
+ conditions are met:
+
+ * Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the
+ following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the
+ following disclaimer in the documentation and/or other
+ materials provided with the distribution.
+
+ * The name of HtmlCleaner may not be used to endorse or promote
+ products derived from this software without specific prior
+ written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+ You can contact Vladimir Nikic by sending e-mail to
+ nikic_vladimir@yahoo.com. Please include the word "HtmlCleaner" in the
+ subject line.
+ */
+
+package org.htmlcleaner;
+
+import java.io.Writer;
+import java.io.IOException;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ *
+ * Broswer compact XML serializer - creates resulting XML by stripping
+ * whitespaces wherever possible, but preserving single whitespace where at
+ * least one exists. This behaviour is well suited for web-browsers, which
+ * usualy treat multiple whitespaces as single one, but make diffrence between
+ * single whitespace and empty text.
+ *
+ */
+public class BrowserCompactXmlSerializer extends XmlSerializer {
+ public BrowserCompactXmlSerializer(final CleanerProperties props) {
+ super(props);
+ }
+
+ protected void serialize(final TagNode tagNode, final Writer writer) throws IOException {
+ serializeOpenTag(tagNode, writer, false);
+
+ final List