diff options
Diffstat (limited to 'core/src/main/java/de')
19 files changed, 1068 insertions, 324 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 69e96c503..93f826894 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -12,6 +12,7 @@ import java.util.concurrent.Callable; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.ChapterUtils; @@ -146,6 +147,11 @@ public class FeedMedia extends FeedFile implements Playable { } + public boolean hasAlmostEnded() { + int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); + return this.position >= this.duration - smartMarkAsPlayedSecs * 1000; + } + @Override public int getTypeAsInt() { return FEEDFILETYPE_FEEDMEDIA; diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java index a353c984a..db242c3bc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -45,6 +45,9 @@ import javax.security.auth.x500.X500Principal; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; @@ -537,6 +540,85 @@ public class GpodnetService { } /** + * Updates the episode actions + * <p/> + * This method requires authentication. + * + * @param episodeActions Collection of episode actions. + * @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse} + * for details. + * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null. + * @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there + * is an authentication error. + */ + public GpodnetEpisodeActionPostResponse uploadEpisodeActions(Collection<GpodnetEpisodeAction> episodeActions) + throws GpodnetServiceException { + + Validate.notNull(episodeActions); + + String username = GpodnetPreferences.getUsername(); + + try { + URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/episodes/%s.json", username), null).toURL(); + + final JSONArray list = new JSONArray(); + for(GpodnetEpisodeAction episodeAction : episodeActions) { + JSONObject obj = episodeAction.writeToJSONObject(); + if(obj != null) { + list.put(obj); + } + } + + RequestBody body = RequestBody.create(JSON, list.toString()); + Request.Builder request = new Request.Builder().post(body).url(url); + + final String response = executeRequest(request); + return GpodnetEpisodeActionPostResponse.fromJSONObject(response); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + } + + /** + * Returns all subscription changes of a specific device. + * <p/> + * This method requires authentication. + * + * @param timestamp A timestamp that can be used to receive all changes since a + * specific point in time. + * @throws IllegalArgumentException If username or deviceId is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public GpodnetEpisodeActionGetResponse getEpisodeChanges(long timestamp) throws GpodnetServiceException { + + String username = GpodnetPreferences.getUsername(); + + String params = String.format("since=%d", timestamp); + String path = String.format("/api/2/episodes/%s.json", + username); + try { + URL url = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params, + null).toURL(); + Request.Builder request = new Request.Builder().url(url); + + String response = executeRequest(request); + JSONObject json = new JSONObject(response); + return readEpisodeActionsFromJSONObject(json); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException | MalformedURLException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + } + + + /** * Logs in a specific user. This method must be called if any of the methods * that require authentication is used. * @@ -773,4 +855,24 @@ public class GpodnetService { long timestamp = object.getLong("timestamp"); return new GpodnetSubscriptionChange(added, removed, timestamp); } + + private GpodnetEpisodeActionGetResponse readEpisodeActionsFromJSONObject( + JSONObject object) throws JSONException { + Validate.notNull(object); + + List<GpodnetEpisodeAction> episodeActions = new ArrayList<GpodnetEpisodeAction>(); + + long timestamp = object.getLong("timestamp"); + JSONArray jsonActions = object.getJSONArray("actions"); + for(int i=0; i < jsonActions.length(); i++) { + JSONObject jsonAction = jsonActions.getJSONObject(i); + GpodnetEpisodeAction episodeAction = GpodnetEpisodeAction.readFromJSONObject(jsonAction); + if(episodeAction != null) { + episodeActions.add(episodeAction); + } + } + return new GpodnetEpisodeActionGetResponse(episodeActions, timestamp); + } + + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java new file mode 100644 index 000000000..0c431d60b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java @@ -0,0 +1,315 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + + +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.util.DateUtils; + +public class GpodnetEpisodeAction { + + private static final String TAG = "GpodnetEpisodeAction"; + + public enum Action { + NEW, DOWNLOAD, PLAY, DELETE + } + + private final String podcast; + private final String episode; + private final String deviceId; + private final Action action; + private final Date timestamp; + private final int started; + private final int position; + private final int total; + + private GpodnetEpisodeAction(Builder builder) { + this.podcast = builder.podcast; + this.episode = builder.episode; + this.action = builder.action; + this.deviceId = builder.deviceId; + this.timestamp = builder.timestamp; + this.started = builder.started; + this.position = builder.position; + this.total = builder.total; + } + + /** + * Creates an episode action object from a String representation. The representation includes + * all mandatory and optional attributes + * + * @param s String representation (output from {@link #writeToString()}) + * @return episode action object, or null if s is invalid + */ + public static GpodnetEpisodeAction readFromString(String s) { + String[] fields = s.split("\t"); + if(fields.length != 8) { + return null; + } + String podcast = fields[0]; + String episode = fields[1]; + String deviceId = fields[2]; + try { + Action action = Action.valueOf(fields[3]); + GpodnetEpisodeAction result = new Builder(podcast, episode, action) + .deviceId(deviceId) + .timestamp(new Date(Long.valueOf(fields[4]))) + .started(Integer.valueOf(fields[5])) + .position(Integer.valueOf(fields[6])) + .total(Integer.valueOf(fields[7])) + .build(); + return result; + } catch(IllegalArgumentException e) { + Log.e(TAG, "readFromString(" + s + "): " + e.getMessage()); + return null; + } + } + + /** + * Create an episode action object from JSON representation. Mandatory fields are "podcast", + * "episode" and "action". + * + * @param object JSON representation + * @return episode action object, or null if mandatory values are missing + */ + public static GpodnetEpisodeAction readFromJSONObject(JSONObject object) { + String podcast = object.optString("podcast", null); + String episode = object.optString("episode", null); + String actionString = object.optString("action", null); + if(StringUtils.isEmpty(podcast) || StringUtils.isEmpty(episode) || StringUtils.isEmpty(actionString)) { + return null; + } + GpodnetEpisodeAction.Action action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase()); + String deviceId = object.optString("device", ""); + GpodnetEpisodeAction.Builder builder = new GpodnetEpisodeAction.Builder(podcast, episode, action) + .deviceId(deviceId); + String utcTimestamp = object.optString("timestamp", null); + if(StringUtils.isNotEmpty(utcTimestamp)) { + builder.timestamp(DateUtils.parse(utcTimestamp)); + } + if(action == GpodnetEpisodeAction.Action.PLAY) { + int started = object.optInt("started", -1); + int position = object.optInt("position", -1); + int total = object.optInt("total", -1); + if(started >= 0 && position > 0 && total > 0) { + builder + .started(started) + .position(position) + .total(total); + } + } + return builder.build(); + } + + public String getPodcast() { + return this.podcast; + } + + public String getEpisode() { + return this.episode; + } + + public String getDeviceId() { + return this.deviceId; + } + + public Action getAction() { + return this.action; + } + + public String getActionString() { + return this.action.name().toLowerCase(); + } + + public Date getTimestamp() { + return this.timestamp; + } + + /** + * Returns the position (in seconds) at which the client started playback + * + * @return start position (in seconds) + */ + public int getStarted() { + return this.started; + } + + /** + * Returns the position (in seconds) at which the client stopped playback + * + * @return stop position (in seconds) + */ + public int getPosition() { + return this.position; + } + + /** + * Returns the total length of the file in seconds. + * + * @return total length in seconds + */ + public int getTotal() { + return this.total; + } + + @Override + public boolean equals(Object o) { + if(o == null) return false; + if(this == o) return true; + if(this.getClass() != o.getClass()) return false; + GpodnetEpisodeAction that = (GpodnetEpisodeAction)o; + return new EqualsBuilder() + .append(this.podcast, that.podcast) + .append(this.episode, that.episode) + .append(this.deviceId, that.deviceId) + .append(this.action, that.action) + .append(this.timestamp, that.timestamp) + .append(this.started, that.started) + .append(this.position, that.position) + .append(this.total, that.total) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(this.podcast) + .append(this.episode) + .append(this.deviceId) + .append(this.action) + .append(this.timestamp) + .append(this.started) + .append(this.position) + .append(this.total) + .toHashCode(); + } + + public String writeToString() { + StringBuilder result = new StringBuilder(); + result.append(this.podcast).append("\t"); + result.append(this.episode).append("\t"); + result.append(this.deviceId).append("\t"); + result.append(this.action).append("\t"); + result.append(this.timestamp.getTime()).append("\t"); + result.append(String.valueOf(this.started)).append("\t"); + result.append(String.valueOf(this.position)).append("\t"); + result.append(String.valueOf(this.total)); + return result.toString(); + } + + /** + * Returns a JSON object representation of this object + * + * @return JSON object representation, or null if the object is invalid + */ + public JSONObject writeToJSONObject() { + JSONObject obj = new JSONObject(); + try { + obj.putOpt("podcast", this.podcast); + obj.putOpt("episode", this.episode); + obj.put("device", this.deviceId); + obj.put("action", this.getActionString()); + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + obj.put("timestamp",formatter.format(this.timestamp)); + if (this.getAction() == Action.PLAY) { + obj.put("started", this.started); + obj.put("position", this.position); + obj.put("total", this.total); + } + } catch(JSONException e) { + Log.e(TAG, "writeToJSONObject(): " + e.getMessage()); + return null; + } + return obj; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + public static class Builder { + + // mandatory + private final String podcast; + private final String episode; + private final Action action; + + // optional + private String deviceId = ""; + private Date timestamp; + private int started = -1; + private int position = -1; + private int total = -1; + + public Builder(FeedItem item, Action action) { + this(item.getFeed().getDownload_url(), item.getItemIdentifier(), action); + } + + public Builder(String podcast, String episode, Action action) { + this.podcast = podcast; + this.episode = episode; + this.action = action; + } + + public Builder deviceId(String deviceId) { + this.deviceId = deviceId; + return this; + } + + public Builder currentDeviceId() { + return deviceId(GpodnetPreferences.getDeviceID()); + } + + public Builder timestamp(Date timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder currentTimestamp() { + return timestamp(new Date()); + } + + public Builder started(int seconds) { + if(action == Action.PLAY) { + this.started = seconds; + } + return this; + } + + public Builder position(int seconds) { + if(action == Action.PLAY) { + this.position = seconds; + } + return this; + } + + public Builder total(int seconds) { + if(action == Action.PLAY) { + this.total = seconds; + } + return this; + } + + public GpodnetEpisodeAction build() { + return new GpodnetEpisodeAction(this); + } + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java new file mode 100644 index 000000000..50420f0a3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + +public class GpodnetEpisodeActionGetResponse { + + private final List<GpodnetEpisodeAction> episodeActions; + private final long timestamp; + + public GpodnetEpisodeActionGetResponse(List<GpodnetEpisodeAction> episodeActions, long timestamp) { + Validate.notNull(episodeActions); + this.episodeActions = episodeActions; + this.timestamp = timestamp; + } + + public List<GpodnetEpisodeAction> getEpisodeActions() { + return this.episodeActions; + } + + public long getTimestamp() { + return this.timestamp; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java new file mode 100644 index 000000000..e06a88d5c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class GpodnetEpisodeActionPostResponse { + + /** + * timestamp/ID that can be used for requesting changes since this upload. + */ + public final long timestamp; + + /** + * URLs that should be updated. The key of the map is the original URL, the value of the map + * is the sanitized URL. + */ + public final Map<String, String> updatedUrls; + + public GpodnetEpisodeActionPostResponse(long timestamp, Map<String, String> updatedUrls) { + this.timestamp = timestamp; + this.updatedUrls = updatedUrls; + } + + /** + * Creates a new GpodnetUploadChangesResponse-object from a JSON object that was + * returned by an uploadChanges call. + * + * @throws org.json.JSONException If the method could not parse the JSONObject. + */ + public static GpodnetEpisodeActionPostResponse fromJSONObject(String objectString) throws JSONException { + final JSONObject object = new JSONObject(objectString); + final long timestamp = object.getLong("timestamp"); + Map<String, String> updatedUrls = new HashMap<String, String>(); + JSONArray urls = object.getJSONArray("update_urls"); + for (int i = 0; i < urls.length(); i++) { + JSONArray urlPair = urls.getJSONArray(i); + updatedUrls.put(urlPair.getString(0), urlPair.getString(1)); + } + return new GpodnetEpisodeActionPostResponse(timestamp, updatedUrls); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} + diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java index af04df017..2e08396ae 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java @@ -4,14 +4,20 @@ import android.content.Context; import android.content.SharedPreferences; import android.util.Log; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.service.GpodnetSyncService; /** @@ -28,9 +34,11 @@ public class GpodnetPreferences { public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; - public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + public static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + public static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_episode_actions_sync_timestamp"; public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added"; public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed"; + public static final String PREF_SYNC_EPISODE_ACTIONS = "de.danoeh.antennapod.preferences.gpoddernet.sync_queued_episode_actions"; private static String username; private static String password; @@ -41,10 +49,15 @@ public class GpodnetPreferences { private static Set<String> addedFeeds; private static Set<String> removedFeeds; + private static ReentrantLock episodeActionListLock = new ReentrantLock(); + private static List<GpodnetEpisodeAction> queuedEpisodeActions; + /** * Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges. */ - private static long lastSyncTimestamp; + private static long lastSubscriptionSyncTimestamp; + + private static long lastEpisodeActionsSyncTimeStamp; private static boolean preferencesLoaded = false; @@ -58,9 +71,11 @@ public class GpodnetPreferences { username = prefs.getString(PREF_GPODNET_USERNAME, null); password = prefs.getString(PREF_GPODNET_PASSWORD, null); deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); - lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0); + lastSubscriptionSyncTimestamp = prefs.getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0); + lastEpisodeActionsSyncTimeStamp = prefs.getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0); addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, "")); removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, "")); + queuedEpisodeActions = readEpisodeActionsFromString(prefs.getString(PREF_SYNC_EPISODE_ACTIONS, "")); hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST)); preferencesLoaded = true; @@ -115,14 +130,24 @@ public class GpodnetPreferences { writePreference(PREF_GPODNET_DEVICEID, deviceID); } - public static long getLastSyncTimestamp() { + public static long getLastSubscriptionSyncTimestamp() { + ensurePreferencesLoaded(); + return lastSubscriptionSyncTimestamp; + } + + public static void setLastSubscriptionSyncTimestamp(long timestamp) { + GpodnetPreferences.lastSubscriptionSyncTimestamp = timestamp; + writePreference(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, timestamp); + } + + public static long getLastEpisodeActionsSyncTimestamp() { ensurePreferencesLoaded(); - return lastSyncTimestamp; + return lastEpisodeActionsSyncTimeStamp; } - public static void setLastSyncTimestamp(long lastSyncTimestamp) { - GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp; - writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp); + public static void setLastEpisodeActionsSyncTimestamp(long timestamp) { + GpodnetPreferences.lastEpisodeActionsSyncTimeStamp = timestamp; + writePreference(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp); } public static String getHostname() { @@ -195,7 +220,23 @@ public class GpodnetPreferences { ensurePreferencesLoaded(); removedFeeds.removeAll(removed); writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + + public static void enqueueEpisodeAction(GpodnetEpisodeAction action) { + ensurePreferencesLoaded(); + queuedEpisodeActions.add(action); + writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + } + public static Collection<GpodnetEpisodeAction> getQueuedEpisodeActions() { + ensurePreferencesLoaded(); + return Collections.unmodifiableCollection(queuedEpisodeActions); + } + + public static void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) { + ensurePreferencesLoaded(); + queuedEpisodeActions.removeAll(queued); + writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); } /** @@ -215,7 +256,9 @@ public class GpodnetPreferences { writePreference(PREF_SYNC_ADDED, addedFeeds); removedFeeds.clear(); writePreference(PREF_SYNC_REMOVED, removedFeeds); - setLastSyncTimestamp(0); + queuedEpisodeActions.clear(); + writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + setLastSubscriptionSyncTimestamp(0); } private static Set<String> readListFromString(String s) { @@ -235,6 +278,29 @@ public class GpodnetPreferences { return result.toString().trim(); } + private static List<GpodnetEpisodeAction> readEpisodeActionsFromString(String s) { + String[] lines = s.split("\n"); + List<GpodnetEpisodeAction> result = new ArrayList<GpodnetEpisodeAction>(lines.length); + for(String line : lines) { + if(StringUtils.isNotBlank(line)) { + GpodnetEpisodeAction action = GpodnetEpisodeAction.readFromString(line); + if(action != null) { + result.add(GpodnetEpisodeAction.readFromString(line)); + } + } + } + return result; + } + + private static String writeEpisodeActionsToString(Collection<GpodnetEpisodeAction> c) { + StringBuilder result = new StringBuilder(); + for(GpodnetEpisodeAction item : c) { + result.append(item.writeToString()); + result.append("\n"); + } + return result.toString(); + } + private static String checkGpodnetHostname(String value) { int startIndex = 0; if (value.startsWith("http://")) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 6cb2faba5..022c03ca7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -45,6 +45,7 @@ public class UserPreferences implements public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + public static final String PREF_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs"; public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; public static final String PREF_THEME = "prefTheme"; @@ -79,6 +80,7 @@ public class UserPreferences implements private boolean allowMobileUpdate; private boolean displayOnlyEpisodes; private boolean autoDelete; + private int smartMarkAsPlayedSecs; private boolean autoFlattr; private float autoFlattrPlayedDurationThreshold; private int theme; @@ -137,6 +139,7 @@ public class UserPreferences implements allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); @@ -267,6 +270,11 @@ public class UserPreferences implements return instance.autoDelete; } + public static int getSmartMarkAsPlayedSecs() { + instanceAvailable();; + return instance.smartMarkAsPlayedSecs; + } + public static boolean isAutoFlattr() { instanceAvailable(); return instance.autoFlattr; @@ -372,8 +380,7 @@ public class UserPreferences implements @Override public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Registered change of user preferences. Key: " + key); + Log.d(TAG, "Registered change of user preferences. Key: " + key); if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) { downloadMediaOnWifiOnly = sp.getBoolean( @@ -389,10 +396,10 @@ public class UserPreferences implements updateInterval = readUpdateInterval(sp.getString( PREF_UPDATE_INTERVAL, "0")); ClientConfig.applicationCallbacks.setUpdateInterval(updateInterval); - } else if (key.equals(PREF_AUTO_DELETE)) { autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); - + } else if (key.equals(PREF_SMART_MARK_AS_PLAYED_SECS)) { + smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); } else if (key.equals(PREF_AUTO_FLATTR)) { autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java index 0f2a81dfb..e8eb99fc5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java @@ -9,24 +9,31 @@ import android.content.Intent; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.util.Log; +import android.util.Pair; +import java.util.Collection; +import java.util.Collections; import java.util.Date; -import java.util.LinkedList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Map; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.NetworkUtils; @@ -50,7 +57,7 @@ public class GpodnetSyncService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; if (action != null && action.equals(ACTION_SYNC)) { - Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); + Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); syncWaiterThread.restart(); } else { Log.e(TAG, "Received invalid intent: action argument is null or invalid"); @@ -61,9 +68,8 @@ public class GpodnetSyncService extends Service { @Override public void onDestroy() { super.onDestroy(); - if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy"); + Log.d(TAG, "onDestroy"); syncWaiterThread.interrupt(); - } @Override @@ -79,64 +85,92 @@ public class GpodnetSyncService extends Service { return service; } - private synchronized void syncChanges() { - if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) { - final long timestamp = GpodnetPreferences.getLastSyncTimestamp(); - try { - final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); - GpodnetService service = tryLogin(); - - if (timestamp == 0) { - // first sync: download all subscriptions... - GpodnetSubscriptionChange changes = - service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0); - if (BuildConfig.DEBUG) - Log.d(TAG, "Downloaded subscription changes: " + changes); - processSubscriptionChanges(localSubscriptions, changes); - - // ... then upload all local subscriptions - if (BuildConfig.DEBUG) - Log.d(TAG, "Uploading subscription list: " + localSubscriptions); - GpodnetUploadChangesResponse uploadChangesResponse = - service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Uploading changes response: " + uploadChangesResponse); - GpodnetPreferences.removeAddedFeeds(localSubscriptions); - GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); - GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); - } else { - Set<String> added = GpodnetPreferences.getAddedFeedsCopy(); - Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy(); - - // download remote changes first... - GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp); - if (BuildConfig.DEBUG) - Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); - processSubscriptionChanges(localSubscriptions, subscriptionChanges); - - // ... then upload changes local changes - if (BuildConfig.DEBUG) - Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", - added.toString(), removed)); - GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed); - if (BuildConfig.DEBUG) - Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse); - - GpodnetPreferences.removeAddedFeeds(added); - GpodnetPreferences.removeRemovedFeeds(removed); - GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); - } - clearErrorNotifications(); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - updateErrorNotification(e); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } + + private synchronized void sync() { + if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable(this) == false) { + stopSelf(); + return; } + syncSubscriptionChanges(); + syncEpisodeActions(); stopSelf(); } + private synchronized void syncSubscriptionChanges() { + final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp(); + try { + final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); + GpodnetService service = tryLogin(); + + // first sync: download all subscriptions... + GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), + GpodnetPreferences.getDeviceID(), timestamp); + long lastUpdate = subscriptionChanges.getTimestamp(); + + Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); + processSubscriptionChanges(localSubscriptions, subscriptionChanges); + + Collection<String> added; + Collection<String> removed; + if (timestamp == 0) { + added = localSubscriptions; + GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); + removed = Collections.emptyList(); + } else { + added = GpodnetPreferences.getAddedFeedsCopy(); + removed = GpodnetPreferences.getRemovedFeedsCopy(); + } + if(added.size() > 0 || removed.size() > 0) { + Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", + added, removed)); + GpodnetUploadChangesResponse uploadResponse = service.uploadChanges(GpodnetPreferences.getUsername(), + GpodnetPreferences.getDeviceID(), added, removed); + lastUpdate = uploadResponse.timestamp; + Log.d(TAG, "Upload changes response: " + uploadResponse); + GpodnetPreferences.removeAddedFeeds(added); + GpodnetPreferences.removeRemovedFeeds(removed); + } + GpodnetPreferences.setLastSubscriptionSyncTimestamp(lastUpdate); + clearErrorNotifications(); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + updateErrorNotification(e); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + + private synchronized void syncEpisodeActions() { + final long timestamp = GpodnetPreferences.getLastEpisodeActionsSyncTimestamp(); + Log.d(TAG, "last episode actions sync timestamp: " + timestamp); + try { + GpodnetService service = tryLogin(); + + // download episode actions + GpodnetEpisodeActionGetResponse getResponse = service.getEpisodeChanges(timestamp); + long lastUpdate = getResponse.getTimestamp(); + Log.d(TAG, "Downloaded episode actions: " + getResponse); + processEpisodeActions(getResponse.getEpisodeActions()); + + // upload local + Collection<GpodnetEpisodeAction> episodeActions = GpodnetPreferences.getQueuedEpisodeActions(); + if(episodeActions.size() > 0) { + Log.d(TAG, "Uploading episode actions: " + episodeActions); + GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(episodeActions); + lastUpdate = postResponse.timestamp; + Log.d(TAG, "Upload episode response: " + postResponse); + GpodnetPreferences.removeQueuedEpisodeActions(episodeActions); + } + GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(lastUpdate); + clearErrorNotifications(); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + updateErrorNotification(e); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException { for (String downloadUrl : changes.getAdded()) { if (!localSubscriptions.contains(downloadUrl)) { @@ -149,6 +183,52 @@ public class GpodnetSyncService extends Service { } } + private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> episodeActions) throws DownloadRequestException { + if(episodeActions.size() == 0) { + return; + } + Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>(); + for (GpodnetEpisodeAction episodeAction : episodeActions) { + switch (episodeAction.getAction()) { + case NEW: + FeedItem newItem = DBReader.getFeedItem(this, episodeAction.getPodcast(), episodeAction.getEpisode()); + if(newItem != null) { + DBWriter.markItemRead(this, newItem, false, true); + } else { + Log.i(TAG, "Unknown feed item: " + episodeAction); + } + break; + case DOWNLOAD: + break; + case PLAY: + if(episodeAction.getTimestamp() == null) { + break; + } + Pair key = new Pair(episodeAction.getPodcast(), episodeAction.getEpisode()); + GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key); + if (mostRecent == null) { + mostRecentPlayAction.put(key, episodeAction); + } else if (mostRecent.getTimestamp().before(episodeAction.getTimestamp())) { + mostRecentPlayAction.put(key, episodeAction); + } + break; + case DELETE: + // NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop + break; + } + } + for (GpodnetEpisodeAction episodeAction : mostRecentPlayAction.values()) { + FeedItem playItem = DBReader.getFeedItem(this, episodeAction.getPodcast(), episodeAction.getEpisode()); + if (playItem != null) { + playItem.getMedia().setPosition(episodeAction.getPosition() * 1000); + if(playItem.getMedia().hasAlmostEnded()) { + DBWriter.markItemRead(this, playItem, true, true); + DBWriter.addItemToPlaybackHistory(this, playItem.getMedia()); + } + } + } + } + private void clearErrorNotifications() { NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.cancel(R.id.notification_gpodnet_sync_error); @@ -156,7 +236,7 @@ public class GpodnetSyncService extends Service { } private void updateErrorNotification(GpodnetServiceException exception) { - if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification"); + Log.d(TAG, "Posting error notification"); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); final String title; @@ -186,7 +266,7 @@ public class GpodnetSyncService extends Service { private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { @Override public void onWaitCompleted() { - syncChanges(); + sync(); } }; @@ -209,7 +289,7 @@ public class GpodnetSyncService extends Service { private void reinit() { if (thread != null && thread.isAlive()) { - Log.d(TAG, "Interrupting waiter thread"); + Log.d(TAG, "Interrupting waiter thread"); thread.interrupt(); } thread = new Thread() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 60d463178..d5f17c099 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -60,6 +60,9 @@ import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; @@ -800,6 +803,18 @@ public class DownloadService extends Service { // queue new media files for automatic download for (FeedItem item : savedFeed.getItems()) { + if(item.getPubDate() == null) { + Log.d(TAG, item.toString()); + } + if(item.getImage() != null && item.getImage().isDownloaded() == false) { + item.getImage().setOwner(item); + try { + requester.downloadImage(DownloadService.this, + item.getImage()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { newMediaFiles.add(item.getMedia().getId()); } @@ -1166,6 +1181,15 @@ public class DownloadService extends Service { saveDownloadStatus(status); sendDownloadHandledIntent(); + if(GpodnetPreferences.loggedIn()) { + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD) + .currentDeviceId() + .currentTimestamp() + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + numberOfDownloads.decrementAndGet(); queryDownloadsAsync(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 6f3eedcb2..c1563a0fa 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -43,6 +43,9 @@ import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; @@ -167,6 +170,8 @@ public class PlaybackService extends Service { private PlaybackServiceMediaPlayer mediaPlayer; private PlaybackServiceTaskManager taskManager; + private int startPosition; + private static volatile MediaType currentMediaType = MediaType.UNKNOWN; private final IBinder mBinder = new LocalBinder(); @@ -445,6 +450,37 @@ public class PlaybackService extends Service { } writePlayerStatusPlaybackPreferences(); + final Playable playable = mediaPlayer.getPSMPInfo().playable; + + // Gpodder: send play action + if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position(getCurrentPosition() / 1000) + .total(getDuration() / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + + // if episode is near end [outro playing]: + // mark as read, remove from queue, add to playlist history + // auto delete: see {@link de.danoeh.antennapod.activity.MediaPlayerActivity#onStop()} + if (playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + if(media.hasAlmostEnded()) { + FeedItem item = media.getItem(); + Log.d(TAG, "smart mark as read"); + DBWriter.markItemRead(PlaybackService.this, item, true, false); + DBWriter.removeQueueItem(PlaybackService.this, item.getId(), false); + DBWriter.addItemToPlaybackHistory(PlaybackService.this, media); + // episode should already be flattered, no action required + } + } + break; case STOPPED: @@ -463,6 +499,7 @@ public class PlaybackService extends Service { writePlayerStatusPlaybackPreferences(); setupNotification(newInfo); started = true; + startPosition = mediaPlayer.getPosition(); break; case ERROR: @@ -540,8 +577,8 @@ public class PlaybackService extends Service { if (BuildConfig.DEBUG) Log.d(TAG, "Playback ended"); - final Playable media = mediaPlayer.getPSMPInfo().playable; - if (media == null) { + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (playable == null) { Log.e(TAG, "Cannot end playback: media was null"); return; } @@ -551,13 +588,14 @@ public class PlaybackService extends Service { boolean isInQueue = false; FeedItem nextItem = null; - if (media instanceof FeedMedia) { - FeedItem item = ((FeedMedia) media).getItem(); + if (playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); DBWriter.markItemRead(PlaybackService.this, item, true, true); try { final List<FeedItem> queue = taskManager.getQueue(); - isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); + isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId()); nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); } catch (InterruptedException e) { e.printStackTrace(); @@ -566,21 +604,30 @@ public class PlaybackService extends Service { if (isInQueue) { DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); } - DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + DBWriter.addItemToPlaybackHistory(PlaybackService.this, media); // auto-flattr if enabled if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); } - //Delete episode if enabled + // Delete episode if enabled if(UserPreferences.isAutoDelete()) { - DBWriter.deleteFeedMediaOfItem(PlaybackService.this, item.getMedia().getId()); - - if(BuildConfig.DEBUG) - Log.d(TAG, "Episode Deleted"); + DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); + Log.d(TAG, "Episode Deleted"); } + // gpodder play action + if(GpodnetPreferences.loggedIn()) { + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position(getDuration() / 1000) + .total(getDuration() / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } } // Load next episode if previous episode was in the queue and if there @@ -605,12 +652,10 @@ public class PlaybackService extends Service { final boolean stream; if (playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback of next episode will start immediately."); + Log.d(TAG, "Playback of next episode will start immediately."); prepareImmediately = startWhenPrepared = true; } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No more episodes available to play"); + Log.d(TAG, "No more episodes available to play"); prepareImmediately = startWhenPrepared = false; stopForeground(true); @@ -619,7 +664,7 @@ public class PlaybackService extends Service { writePlaybackPreferencesNoMediaPlaying(); if (nextMedia != null) { - stream = !media.localFileAvailable(); + stream = !playable.localFileAvailable(); mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); @@ -631,8 +676,7 @@ public class PlaybackService extends Service { } public void setSleepTimer(long waitingTime) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds"); taskManager.setSleepTimer(waitingTime); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); @@ -675,8 +719,7 @@ public class PlaybackService extends Service { } private void writePlaybackPreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Writing playback preferences"); + Log.d(TAG, "Writing playback preferences"); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); @@ -918,15 +961,15 @@ public class PlaybackService extends Service { if (BuildConfig.DEBUG) Log.d(TAG, "Saving current position to " + position); if (updatePlayedDuration && playable instanceof FeedMedia) { - FeedMedia m = (FeedMedia) playable; - FeedItem item = m.getItem(); - m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); // Auto flattr - if (isAutoFlattrable(m) && - (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { + if (isAutoFlattrable(media) && + (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { if (BuildConfig.DEBUG) - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) + Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration()) + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); DBTasks.flattrItemIfLoggedIn(this, item); } @@ -1231,7 +1274,26 @@ public class PlaybackService extends Service { public void seekTo(final int t) { + if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING + && GpodnetPreferences.loggedIn()) { + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position(getCurrentPosition() / 1000) + .total(getDuration() / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + } mediaPlayer.seekTo(t); + if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) { + startPosition = t; + } } @@ -1270,10 +1332,9 @@ public class PlaybackService extends Service { return mediaPlayer.getVideoSize(); } - private boolean isAutoFlattrable(Playable p) { - if (p != null && p instanceof FeedMedia) { - FeedMedia media = (FeedMedia) p; - FeedItem item = ((FeedMedia) p).getItem(); + private boolean isAutoFlattrable(FeedMedia media) { + if (media != null) { + FeedItem item = media.getItem(); return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); } else { return false; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 217e6fba5..e10fffc18 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -2,7 +2,6 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; import android.database.Cursor; -import android.database.SQLException; import android.util.Log; import java.util.ArrayList; @@ -680,6 +679,42 @@ public final class DBReader { } + static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { + Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); + FeedItem item = null; + Cursor itemCursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl); + if (itemCursor.moveToFirst()) { + List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor); + if (list.size() > 0) { + item = list.get(0); + loadFeedDataOfFeedItemlist(context, list); + if (item.hasChapters()) { + loadChaptersOfFeedItem(adapter, item); + } + } + } + return item; + } + + /** + * Loads a specific FeedItem from the database. + * + * @param context A context that is used for opening a database connection. + * @param podcastUrl the corresponding feed's url + * @param episodeUrl the feed item's url + * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes + * as well as chapter marks of the FeedItem will also be loaded from the database. + */ + public static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl) { + Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedItem item = getFeedItem(context, podcastUrl, episodeUrl, adapter); + adapter.close(); + return item; + } + /** * Loads additional information about a FeedItem, e.g. shownotes * diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index c5bf89533..53fe5149b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -7,6 +7,7 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.preference.PreferenceManager; import android.util.Log; + import org.shredzone.flattr4j.model.Flattr; import java.io.File; @@ -32,6 +33,8 @@ import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -120,6 +123,15 @@ public class DBWriter { PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); } } + // Gpodder: queue delete action for synchronization + if(GpodnetPreferences.loggedIn()) { + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DELETE) + .currentDeviceId() + .currentTimestamp() + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } } if (BuildConfig.DEBUG) Log.d(TAG, "Deleting File. Result: " + result); @@ -639,18 +651,6 @@ public class DBWriter { return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition); } - /** - * Sets the 'read'-attribute of a FeedItem to the specified value. - * - * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem - * @param read New value of the 'read'-attribute - */ - public static Future<?> markItemRead(final Context context, final long itemId, - final boolean read) { - return markItemRead(context, itemId, read, 0, false); - } - private static Future<?> markItemRead(final Context context, final long itemId, final boolean read, final long mediaId, final boolean resetMediaPosition) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index f72858adc..f518a4f5f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -1120,7 +1120,11 @@ public class PodDBAdapter { return c; } - public final Cursor getFeedItemCursor(final String... ids) { + public final Cursor getFeedItemCursor(final String id) { + return getFeedItemCursor(new String[] { id }); + } + + public final Cursor getFeedItemCursor(final String[] ids) { if (ids.length > IN_OPERATOR_MAXIMUM) { throw new IllegalArgumentException( "number of IDs must not be larger than " @@ -1133,6 +1137,15 @@ public class PodDBAdapter { } + public final Cursor getFeedItemCursor(final String podcastUrl, final String episodeUrl) { + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "='" + + episodeUrl + "' AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "='" + podcastUrl + "'"; + return db.rawQuery(query, null); + } + public int getQueueSize() { final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE); Cursor c = db.rawQuery(query, null); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java index 099593eed..23f76186b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java @@ -3,7 +3,7 @@ package de.danoeh.antennapod.core.syndication.namespace; import org.xml.sax.Attributes; import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.util.DateUtils; public class NSDublinCore extends Namespace { private static final String TAG = "NSDublinCore"; @@ -30,7 +30,7 @@ public class NSDublinCore extends Namespace { String second = secondElement.getName(); if (top.equals(DATE) && second.equals(ITEM)) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseISO8601Date(content)); + DateUtils.parse(content)); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java index 0ca261a0e..6455332be 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java @@ -1,14 +1,16 @@ package de.danoeh.antennapod.core.syndication.namespace; import android.util.Log; + +import org.xml.sax.Attributes; + import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; -import org.xml.sax.Attributes; +import de.danoeh.antennapod.core.util.DateUtils; /** * SAX-Parser for reading RSS-Feeds @@ -129,7 +131,7 @@ public class NSRSS20 extends Namespace { } } else if (top.equals(PUBDATE) && second.equals(ITEM)) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC822Date(content)); + DateUtils.parse(content)); } else if (top.equals(URL) && second.equals(IMAGE) && third != null && third.equals(CHANNEL)) { state.getFeed().getImage().setDownload_url(content); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java index a2e5d0187..64b82100e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java @@ -10,7 +10,7 @@ import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.SimpleChapter; import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.util.DateUtils; public class NSSimpleChapters extends Namespace { private static final String TAG = "NSSimpleChapters"; @@ -33,7 +33,7 @@ public class NSSimpleChapters extends Namespace { try { state.getCurrentItem() .getChapters() - .add(new SimpleChapter(SyndDateUtils + .add(new SimpleChapter(DateUtils .parseTimeString(attributes.getValue(START)), attributes.getValue(TITLE), state.getCurrentItem(), attributes.getValue(HREF))); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java index 3928c65b3..abff5b2db 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java @@ -13,8 +13,8 @@ import de.danoeh.antennapod.core.syndication.namespace.NSITunes; import de.danoeh.antennapod.core.syndication.namespace.NSRSS20; import de.danoeh.antennapod.core.syndication.namespace.Namespace; import de.danoeh.antennapod.core.syndication.namespace.SyndElement; -import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import de.danoeh.antennapod.core.util.DateUtils; public class NSAtom extends Namespace { private static final String TAG = "NSAtom"; @@ -191,12 +191,12 @@ public class NSAtom extends Namespace { if (second.equals(ENTRY) && state.getCurrentItem().getPubDate() == null) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC3339Date(content)); + DateUtils.parse(content)); } } else if (top.equals(PUBLISHED)) { if (second.equals(ENTRY)) { state.getCurrentItem().setPubDate( - SyndDateUtils.parseRFC3339Date(content)); + DateUtils.parse(content)); } } else if (top.equals(IMAGE)) { state.getFeed().setImage(new FeedImage(state.getFeed(), content, null)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java deleted file mode 100644 index a9929d7b1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java +++ /dev/null @@ -1,194 +0,0 @@ -package de.danoeh.antennapod.core.syndication.util; - -import android.util.Log; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import de.danoeh.antennapod.core.BuildConfig; - -/** - * Parses several date formats. - */ -public class SyndDateUtils { - private static final String TAG = "DateUtils"; - - private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z", - "dd MMM yy HH:mm Z"}; - - /** - * RFC 3339 date format for UTC dates. - */ - public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - - /** - * RFC 3339 date format for localtime dates with offset. - */ - public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ"; - - public static final String ISO8601_SHORT = "yyyy-MM-dd"; - - private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC822DATES[0], Locale.US); - } - - }; - - private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC3339UTC, Locale.US); - } - - }; - - private static ThreadLocal<SimpleDateFormat> ISO8601ShortFormatter = new ThreadLocal<SimpleDateFormat>() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(ISO8601_SHORT, Locale.US); - } - - }; - - public static Date parseRFC822Date(String date) { - Date result = null; - if (date.contains("PDT")) { - date = date.replace("PDT", "PST8PDT"); - } - if (date.contains(",")) { - // Remove day of the week - date = date.substring(date.indexOf(",") + 1).trim(); - } - SimpleDateFormat format = RFC822Formatter.get(); - - for (String RFC822DATE : RFC822DATES) { - try { - format.applyPattern(RFC822DATE); - result = format.parse(date); - break; - } catch (ParseException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "ParserException", e); - } - } - if (result == null) { - Log.e(TAG, "Unable to parse feed date correctly:" + date); - } - - return result; - } - - public static Date parseRFC3339Date(String date) { - Date result = null; - SimpleDateFormat format = RFC3339Formatter.get(); - boolean isLocal = date.endsWith("Z"); - if (date.contains(".")) { - // remove secfrac - int fracIndex = date.indexOf("."); - String first = date.substring(0, fracIndex); - String second = null; - if (isLocal) { - second = date.substring(date.length() - 1); - } else { - if (date.contains("+")) { - second = date.substring(date.indexOf("+")); - } else { - second = date.substring(date.indexOf("-")); - } - } - - date = first + second; - } - if (isLocal) { - try { - result = format.parse(date); - } catch (ParseException e) { - e.printStackTrace(); - } - } else { - format.applyPattern(RFC3339LOCAL); - // remove last colon - StringBuffer buf = new StringBuffer(date.length() - 1); - int colonIdx = date.lastIndexOf(':'); - for (int x = 0; x < date.length(); x++) { - if (x != colonIdx) - buf.append(date.charAt(x)); - } - String bufStr = buf.toString(); - try { - result = format.parse(bufStr); - } catch (ParseException e) { - e.printStackTrace(); - Log.e(TAG, "Unable to parse date"); - } finally { - format.applyPattern(RFC3339UTC); - } - - } - - return result; - - } - - public static Date parseISO8601Date(String date) { - if(date.length() > ISO8601_SHORT.length()) { - return parseRFC3339Date(date); - } - Date result = null; - if(date.length() == "YYYYMMDD".length()) { - date = date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6,8); - } - SimpleDateFormat format = ISO8601ShortFormatter.get(); - try { - result = format.parse(date); - } catch (ParseException e) { - e.printStackTrace(); - } - return result; - } - - /** - * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to - * milliseconds. - * - * @throws java.lang.NumberFormatException if the number segments contain invalid numbers. - */ - public static long parseTimeString(final String time) { - String[] parts = time.split(":"); - long result = 0; - int idx = 0; - if (parts.length == 3) { - // string has hours - result += Integer.valueOf(parts[idx]) * 3600000L; - idx++; - } - if (parts.length >= 2) { - result += Integer.valueOf(parts[idx]) * 60000L; - idx++; - result += (Float.valueOf(parts[idx])) * 1000L; - } - return result; - } - - public static String formatRFC822Date(Date date) { - SimpleDateFormat format = RFC822Formatter.get(); - return format.format(date); - } - - public static String formatRFC3339Local(Date date) { - SimpleDateFormat format = RFC3339Formatter.get(); - format.applyPattern(RFC3339LOCAL); - String result = format.format(date); - format.applyPattern(RFC3339UTC); - return result; - } - - public static String formatRFC3339UTC(Date date) { - SimpleDateFormat format = RFC3339Formatter.get(); - format.applyPattern(RFC3339UTC); - return format.format(date); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java new file mode 100644 index 000000000..6622eab73 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -0,0 +1,140 @@ +package de.danoeh.antennapod.core.util; + +import org.apache.commons.lang3.StringUtils; + +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Parses several date formats. + */ +public class DateUtils { + private static final String TAG = "DateUtils"; + + private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z", + "dd MMM yy HH:mm Z"}; + + /** + * RFC 3339 date format for UTC dates. + */ + public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + /** + * RFC 3339 date format for localtime dates with offset. + */ + public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ"; + + public static final String ISO8601_SHORT = "yyyy-MM-dd"; + + private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); + } + + }; + + private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + } + + }; + + public static Date parse(String date) { + if(date == null) { + throw new IllegalArgumentException("Date most not be null"); + } + date = date.replace('/', ' '); + date = date.replace('-', ' '); + if(date.contains(".")) { + int start = date.indexOf('.'); + int current = start+1; + while(current < date.length() && Character.isDigit(date.charAt(current))) { + current++; + } + if(current - start > 4) { + if(current < date.length()-1) { + date = date.substring(0, start + 4) + date.substring(current); + } else { + date = date.substring(0, start + 4); + } + } else if(current - start < 4) { + if(current < date.length()-1) { + date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)) + date.substring(current); + } else { + date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)); + } + + } + } + String[] patterns = { + "dd MMM yy HH:mm:ss Z", + "dd MMM yy HH:mm Z", + "EEE, dd MMM yyyy HH:mm:ss Z", + "EEEE, dd MMM yy HH:mm:ss Z", + "EEE MMM d HH:mm:ss yyyy", + "yyyy MM dd'T'HH:mm:ss", + "yyyy MM dd'T'HH:mm:ss.SSS", + "yyyy MM dd'T'HH:mm:ss.SSS Z", + "yyyy MM dd'T'HH:mm:ssZ", + "yyyy MM dd'T'HH:mm:ss'Z'", + "yyyy MM ddZ", + "yyyy MM dd" + }; + SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); + parser.setLenient(false); + ParsePosition pos = new ParsePosition(0); + for(String pattern : patterns) { + parser.applyPattern(pattern); + pos.setIndex(0); + Date result = parser.parse(date, pos); + if(result != null && pos.getIndex() == date.length()) { + return result; + } + } + return null; + } + + + /** + * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to + * milliseconds. + * + * @throws java.lang.NumberFormatException if the number segments contain invalid numbers. + */ + public static long parseTimeString(final String time) { + String[] parts = time.split(":"); + long result = 0; + int idx = 0; + if (parts.length == 3) { + // string has hours + result += Integer.valueOf(parts[idx]) * 3600000L; + idx++; + } + if (parts.length >= 2) { + result += Integer.valueOf(parts[idx]) * 60000L; + idx++; + result += (Float.valueOf(parts[idx])) * 1000L; + } + return result; + } + + public static String formatRFC822Date(Date date) { + SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); + return format.format(date); + } + + public static String formatRFC3339Local(Date date) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); + return format.format(date); + } + + public static String formatRFC3339UTC(Date date) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + return format.format(date); + } +} |