package de.danoeh.antennapod.core.feed;
import android.database.Cursor;
import android.net.Uri;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.asynctask.ImageResource;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.ShownotesProvider;
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import de.danoeh.antennapod.core.util.flattr.FlattrThing;
/**
* Data Object for a XML message
*
* @author daniel
*/
public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, ImageResource {
/** tag that indicates this item is in the queue */
public static final String TAG_QUEUE = "Queue";
/** tag that indicates this item is in favorites */
public static final String TAG_FAVORITE = "Favorite";
/**
* The id/guid that can be found in the rss/atom feed. Might not be set.
*/
private String itemIdentifier;
private String title;
/**
* The description of a feeditem.
*/
private String description;
/**
* The content of the content-encoded tag of a feeditem.
*/
private String contentEncoded;
private String link;
private Date pubDate;
private FeedMedia media;
private Feed feed;
private long feedId;
private int state;
public final static int NEW = -1;
public final static int UNPLAYED = 0;
public final static int PLAYED = 1;
private String paymentLink;
private FlattrStatus flattrStatus;
/**
* Is true if the database contains any chapters that belong to this item. This attribute is only
* written once by DBReader on initialization.
* The FeedItem might still have a non-null chapters value. In this case, the list of chapters
* has not been saved in the database yet.
* */
private final boolean hasChapters;
/**
* The list of chapters of this item. This might be null even if there are chapters of this item
* in the database. The 'hasChapters' attribute should be used to check if this item has any chapters.
* */
private List<Chapter> chapters;
private FeedImage image;
/*
* 0: auto download disabled
* 1: auto download enabled (default)
* > 1: auto download enabled, (approx.) timestamp of the last failed attempt
* where last digit denotes the number of failed attempts
*/
private long autoDownload = 1;
/**
* Any tags assigned to this item
*/
private Set<String> tags = new HashSet<>();
public FeedItem() {
this.state = UNPLAYED;
this.flattrStatus = new FlattrStatus();
this.hasChapters = false;
}
/**
* This constructor is used by DBReader.
* */
public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId,
FlattrStatus flattrStatus, boolean hasChapters, FeedImage image, int state,
String itemIdentifier, long autoDownload) {
this.id = id;
this.title = title;
this.link = link;
this.pubDate = pubDate;
this.paymentLink = paymentLink;
this.feedId = feedId;
this.flattrStatus = flattrStatus;
this.hasChapters = hasChapters;
this.image = image;
this.state = state;
this.itemIdentifier = itemIdentifier;
this.autoDownload = autoDownload;
}
/**
* This constructor should be used for creating test objects.
*/
public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed) {
this.id = id;
this.title = title;
this.itemIdentifier = itemIdentifier;
this.link = link;
this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
this.state = state;
this.feed = feed;
this.flattrStatus = new FlattrStatus();
this.hasChapters = false;
}
/**
* This constructor should be used for creating test objects involving chapter marks.
*/
public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed, boolean hasChapters) {
this.id = id;
this.title = title;
this.itemIdentifier = itemIdentifier;
this.link = link;
this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
this.state = state;
this.feed = feed;
this.flattrStatus = new FlattrStatus();
this.hasChapters = hasChapters;
}
public static FeedItem fromCursor(Cursor cursor) {
int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
int indexPubDate = cursor.getColumnIndex(PodDBAdapter.KEY_PUBDATE);
int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
int indexFeedId = cursor.getColumnIndex(PodDBAdapter.KEY_FEED);
int indexFlattrStatus = cursor.getColumnIndex(PodDBAdapter.KEY_FLATTR_STATUS);
int indexHasChapters = cursor.getColumnIndex(PodDBAdapter.KEY_HAS_CHAPTERS);
int indexRead = cursor.getColumnIndex(PodDBAdapter.KEY_READ);
int indexItemIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_ITEM_IDENTIFIER);
int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD);
long id = cursor.getInt(indexId);
assert(id > 0);
String title = cursor.getString(indexTitle);
String link = cursor.getString(indexLink);
Date pubDate = new Date(cursor.getLong(indexPubDate));
String paymentLink = cursor.getString(indexPaymentLink);
long feedId = cursor.getLong(indexFeedId);
boolean hasChapters = cursor.getInt(indexHasChapters) > 0;
FlattrStatus flattrStatus = new FlattrStatus(cursor.getLong(indexFlattrStatus));
int state = cursor.getInt(indexRead);
String itemIdentifier = cursor.getString(indexItemIdentifier);
long autoDownload = cursor.getLong(indexAutoDownload);
return new FeedItem(id, title, link, pubDate, paymentLink, feedId, flattrStatus,
hasChapters, null, state, itemIdentifier, autoDownload);
}
public void updateFromOther(FeedItem other) {
super.updateFromOther(other);
if (other.title != null) {
title = other.title;
}
if (other.getDescription() != null) {
description = other.getDescription();
}
if (other.getContentEncoded() != null) {
contentEncoded = other.contentEncoded;
}
if (other.link != null) {
link = other.link;
}
if (other.pubDate != null && other.pubDate != pubDate) {
pubDate = other.pubDate;
}
if (other.media != null) {
if (media == null) {
setMedia(other.media);
// reset to new if feed item did link to a file before
setNew();
} else if (media.compareWithOther(other.media)) {
media.updateFromOther(other.media);
}
}
if (other.paymentLink != null) {
paymentLink = other.paymentLink;
}
if (other.chapters != null) {
if (!hasChapters) {
chapters = other.chapters;
}
}
if (image == null) {
image = other.image;
}
}
/**
* Returns the value that uniquely identifies this FeedItem. If the
* itemIdentifier attribute is not null, it will be returned. Else it will
* try to return the title. If the title is not given, it will use the link
* of the entry.
*/
public String getIdentifyingValue() {
if (itemIdentifier != null && !itemIdentifier.isEmpty()) {
return itemIdentifier;
} else if (title != null && !title.isEmpty()) {
return title;
} else {
return link;
}
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
public Date getPubDate() {
if (pubDate != null) {
return (Date) pubDate.clone();
} else {
return null;
}
}
public void setPubDate(Date pubDate) {
if (pubDate != null) {
this.pubDate = (Date) pubDate.clone();
} else {
this.pubDate = null;
}
}
public FeedMedia getMedia() {
return media;
}
/**
* Sets the media object of this FeedItem. If the given
* FeedMedia object is not null, it's 'item'-attribute value
* will also be set to this item.
*/
public void setMedia(FeedMedia media) {
this.media = media;
if (media != null && media.getItem() != this) {
media.setItem(this);
}
}
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public boolean isNew() {
return state == NEW;
}
public void setNew() {
state = NEW;
}
public boolean isPlayed() {
return state == PLAYED;
}
public void setPlayed(boolean played) {
if(played) {
state = PLAYED;
} else {
state = UNPLAYED;
}
}
private boolean isInProgress() {
return (media != null && media.isInProgress());
}
public String getContentEncoded() {
return contentEncoded;
}
public void setContentEncoded(String contentEncoded) {
this.contentEncoded = contentEncoded;
}
public FlattrStatus getFlattrStatus() {
return flattrStatus;
}
public String getPaymentLink() {
return paymentLink;
}
public void setPaymentLink(String paymentLink) {
this.paymentLink = paymentLink;
}
public List<Chapter> getChapters() {
return chapters;
}
public void setChapters(List<Chapter> chapters) {
this.chapters = chapters;
}
public String getItemIdentifier() {
return itemIdentifier;
}
public void setItemIdentifier(String itemIdentifier) {
this.itemIdentifier = itemIdentifier;
}
public boolean hasMedia() {
return media != null;
}
private boolean isPlaying() {
return media != null && media.isPlaying();
}
@Override
public Callable<String> loadShownotes() {
return () -> {
if (contentEncoded == null || description == null) {
DBReader.loadExtraInformationOfFeedItem(FeedItem.this);
}
return (contentEncoded != null) ? contentEncoded : description;
};
}
@Override
public Uri getImageUri() {
if(media != null && media.hasEmbeddedPicture()) {
return media.getImageUri();
} else if (image != null) {
return image.getImageUri();
} else if (feed != null) {
return feed.getImageUri();
} else {
return null;
}
}
public enum State {
UNREAD, IN_PROGRESS, READ, PLAYING
}
public State getState() {
if (hasMedia()) {
if (isPlaying()) {
return State.PLAYING;
}
if (isInProgress()) {
return State.IN_PROGRESS;
}
}
return (isPlayed() ? State.READ : State.UNREAD);
}
public long getFeedId() {
return feedId;
}
public void setFeedId(long feedId) {
this.feedId = feedId;
}
/**
* Returns the image of this item or the image of the feed if this item does
* not have its own image.
*/
public FeedImage getImage() {
return (hasItemImage()) ? image : feed.getImage();
}
public void setImage(FeedImage image) {
this.image = image;
if (image != null) {
image.setOwner(this);
}
}
/**
* Returns true if this FeedItem has its own image, false otherwise.
*/
public boolean hasItemImage() {
return image != null;
}
/**
* Returns true if this FeedItem has its own image and the image has been downloaded.
*/
public boolean hasItemImageDownloaded() {
return image != null && image.isDownloaded();
}
@Override
public String getHumanReadableIdentifier() {
return title;
}
public boolean hasChapters() {
return hasChapters;
}
public void setAutoDownload(boolean autoDownload) {
this.autoDownload = autoDownload ? 1 : 0;
}
public boolean getAutoDownload() {
return this.autoDownload > 0;
}
public int getFailedAutoDownloadAttempts() {
if (autoDownload <= 1) {
return 0;
}
int failedAttempts = (int)(autoDownload % 10);
if (failedAttempts == 0) {
failedAttempts = 10;
}
return failedAttempts;
}
public boolean isAutoDownloadable() {
if (media == null || media.isPlaying() || media.isDownloaded() || autoDownload == 0) {
return false;
}
if (autoDownload == 1) {
return true;
}
int failedAttempts = getFailedAutoDownloadAttempts();
double magicValue = 1.767; // 1.767^(10[=#maxNumAttempts]-1) = 168 hours / 7 days
int millisecondsInHour = 3600000;
long waitingTime = (long) (Math.pow(magicValue, failedAttempts - 1) * millisecondsInHour);
long grace = TimeUnit.MINUTES.toMillis(5);
return System.currentTimeMillis() > (autoDownload + waitingTime - grace);
}
/**
* @return true if the item has this tag
*/
public boolean isTagged(String tag) { return tags.contains(tag); }
/**
* @param tag adds this tag to the item. NOTE: does NOT persist to the database
*/
public void addTag(String tag) { tags.add(tag); }
/**
* @param tag the to remove
*/
public void removeTag(String tag) { tags.remove(tag); }
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}