From 67cc7c98857b3fa0897ab96b2b87f52d99e4de2f Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 28 Mar 2015 10:59:07 +0100 Subject: Sync episode actions with gpodder, smart mark as played * Create episode actions when episodes are downloaded, played, deleted and marked as read * Sync (download and upload) episode actions * MediaPlayerActivity deletes almost completely played episode on close * Improved parsing of datetime strings * Smart mark as played can be disabled or set in the preferences --- .../danoeh/antennapod/core/util/DateUtilsTest.java | 59 ++++ .../de/danoeh/antennapod/core/feed/FeedMedia.java | 6 + .../antennapod/core/gpoddernet/GpodnetService.java | 102 +++++++ .../gpoddernet/model/GpodnetEpisodeAction.java | 315 +++++++++++++++++++++ .../model/GpodnetEpisodeActionGetResponse.java | 34 +++ .../model/GpodnetEpisodeActionPostResponse.java | 53 ++++ .../core/preferences/GpodnetPreferences.java | 84 +++++- .../core/preferences/UserPreferences.java | 15 +- .../core/service/GpodnetSyncService.java | 206 +++++++++----- .../core/service/download/DownloadService.java | 24 ++ .../core/service/playback/PlaybackService.java | 121 ++++++-- .../danoeh/antennapod/core/storage/DBReader.java | 37 ++- .../danoeh/antennapod/core/storage/DBWriter.java | 24 +- .../antennapod/core/storage/PodDBAdapter.java | 15 +- .../core/syndication/namespace/NSDublinCore.java | 4 +- .../core/syndication/namespace/NSRSS20.java | 8 +- .../syndication/namespace/NSSimpleChapters.java | 4 +- .../core/syndication/namespace/atom/NSAtom.java | 6 +- .../core/syndication/util/SyndDateUtils.java | 194 ------------- .../de/danoeh/antennapod/core/util/DateUtils.java | 140 +++++++++ core/src/main/res/values/arrays.xml | 9 + core/src/main/res/values/strings.xml | 10 +- 22 files changed, 1143 insertions(+), 327 deletions(-) create mode 100644 core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java (limited to 'core/src') diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java new file mode 100644 index 000000000..cca753895 --- /dev/null +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java @@ -0,0 +1,59 @@ +package de.danoeh.antennapod.core.util; + + +import android.test.AndroidTestCase; + +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +public class DateUtilsTest extends AndroidTestCase { + + public void testParseDateWithMicroseconds() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + Date expected = new Date(exp.getTimeInMillis() + 963); + Date actual = DateUtils.parse("2015-03-28T13:31:04.963870"); + assertEquals(expected, actual); + } + + public void testParseDateWithCentiseconds() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + Date expected = new Date(exp.getTimeInMillis() + 960); + Date actual = DateUtils.parse("2015-03-28T13:31:04.96"); + assertEquals(expected, actual); + } + + public void testParseDateWithDeciseconds() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + Date expected = new Date(exp.getTimeInMillis() + 900); + Date actual = DateUtils.parse("2015-03-28T13:31:04.9"); + assertEquals(expected.getTime()/1000, actual.getTime()/1000); + assertEquals(900, actual.getTime()%1000); + } + + public void testParseDateWithMicrosecondsAndTimezone() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 963); + Date actual = DateUtils.parse("2015-03-28T13:31:04.963870 +0700"); + assertEquals(expected, actual); + } + + public void testParseDateWithCentisecondsAndTimezone() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 960); + Date actual = DateUtils.parse("2015-03-28T13:31:04.96 +0700"); + assertEquals(expected, actual); + } + + public void testParseDateWithDecisecondsAndTimezone() throws Exception { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 900); + Date actual = DateUtils.parse("2015-03-28T13:31:04.9 +0700"); + assertEquals(expected.getTime()/1000, actual.getTime()/1000); + assertEquals(900, actual.getTime()%1000); + } + +} 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; @@ -536,6 +539,85 @@ public class GpodnetService { } + /** + * Updates the episode actions + *

+ * 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 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. + *

+ * 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 episodeActions = new ArrayList(); + + 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 episodeActions; + private final long timestamp; + + public GpodnetEpisodeActionGetResponse(List episodeActions, long timestamp) { + Validate.notNull(episodeActions); + this.episodeActions = episodeActions; + this.timestamp = timestamp; + } + + public List 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 updatedUrls; + + public GpodnetEpisodeActionPostResponse(long timestamp, Map 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 updatedUrls = new HashMap(); + 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 addedFeeds; private static Set removedFeeds; + private static ReentrantLock episodeActionListLock = new ReentrantLock(); + private static List 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 getQueuedEpisodeActions() { + ensurePreferencesLoaded(); + return Collections.unmodifiableCollection(queuedEpisodeActions); + } + + public static void removeQueuedEpisodeActions(Collection 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 readListFromString(String s) { @@ -235,6 +278,29 @@ public class GpodnetPreferences { return result.toString().trim(); } + private static List readEpisodeActionsFromString(String s) { + String[] lines = s.split("\n"); + List result = new ArrayList(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 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 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()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Uploading changes response: " + uploadChangesResponse); - GpodnetPreferences.removeAddedFeeds(localSubscriptions); - GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); - GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); - } else { - Set added = GpodnetPreferences.getAddedFeedsCopy(); - Set 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 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 added; + Collection 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 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 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 episodeActions) throws DownloadRequestException { + if(episodeActions.size() == 0) { + return; + } + Map, GpodnetEpisodeAction> mostRecentPlayAction = new HashMap, 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 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 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 RFC822Formatter = new ThreadLocal() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC822DATES[0], Locale.US); - } - - }; - - private static ThreadLocal RFC3339Formatter = new ThreadLocal() { - @Override - protected SimpleDateFormat initialValue() { - return new SimpleDateFormat(RFC3339UTC, Locale.US); - } - - }; - - private static ThreadLocal ISO8601ShortFormatter = new ThreadLocal() { - @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 RFC822Formatter = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); + } + + }; + + private static ThreadLocal RFC3339Formatter = new ThreadLocal() { + @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); + } +} diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 9b9079021..48ab40d61 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -1,6 +1,15 @@ + + off + 15 + 30 + 45 + 60 + + + 5 10 diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 186651224..bdb3ad606 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -98,9 +98,9 @@ Stream Remove Remove episode - Mark as read - Mark as unread - Marked as read + Mark as played + Mark as unplayed + Marked as played Add to Queue Remove from Queue Visit Website @@ -220,6 +220,8 @@ Jump to next queue item when playback completes Delete episode when playback completes Auto Delete + Mark episodes as played even if less than a certain amount of seconds of playing time is still left + Smart mark as played Playback Network Update interval @@ -277,6 +279,8 @@ Android versions before 4.1 do not support expanded notifications. Add new episodes to the front of the queue. Enqueue at front. + Disabled + Enable automatic flattring -- cgit v1.2.3 From 3d19b939b125d36d3ac0ef3e828eabf403aa03d6 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Thu, 2 Apr 2015 11:47:20 +0200 Subject: Dismiss remote play actions if queued play actions for that episode are more recent --- .../core/preferences/GpodnetPreferences.java | 9 ++- .../core/service/GpodnetSyncService.java | 66 +++++++++++++--------- 2 files changed, 44 insertions(+), 31 deletions(-) (limited to 'core/src') 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 2e08396ae..cfdd0c5d6 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 @@ -49,7 +49,6 @@ public class GpodnetPreferences { private static Set addedFeeds; private static Set removedFeeds; - private static ReentrantLock episodeActionListLock = new ReentrantLock(); private static List queuedEpisodeActions; /** @@ -222,18 +221,18 @@ public class GpodnetPreferences { writePreference(PREF_SYNC_REMOVED, removedFeeds); } - public static void enqueueEpisodeAction(GpodnetEpisodeAction action) { + public static synchronized void enqueueEpisodeAction(GpodnetEpisodeAction action) { ensurePreferencesLoaded(); queuedEpisodeActions.add(action); writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); } - public static Collection getQueuedEpisodeActions() { + public static List getQueuedEpisodeActions() { ensurePreferencesLoaded(); - return Collections.unmodifiableCollection(queuedEpisodeActions); + return Collections.unmodifiableList(queuedEpisodeActions); } - public static void removeQueuedEpisodeActions(Collection queued) { + public static synchronized void removeQueuedEpisodeActions(Collection queued) { ensurePreferencesLoaded(); queuedEpisodeActions.removeAll(queued); writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); 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 e8eb99fc5..e39197387 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 @@ -150,16 +150,18 @@ public class GpodnetSyncService extends Service { GpodnetEpisodeActionGetResponse getResponse = service.getEpisodeChanges(timestamp); long lastUpdate = getResponse.getTimestamp(); Log.d(TAG, "Downloaded episode actions: " + getResponse); - processEpisodeActions(getResponse.getEpisodeActions()); + List remoteActions = getResponse.getEpisodeActions(); - // upload local - Collection episodeActions = GpodnetPreferences.getQueuedEpisodeActions(); - if(episodeActions.size() > 0) { - Log.d(TAG, "Uploading episode actions: " + episodeActions); - GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(episodeActions); + List localActions = GpodnetPreferences.getQueuedEpisodeActions(); + processEpisodeActions(localActions, remoteActions); + + // upload local actions + if(localActions.size() > 0) { + Log.d(TAG, "Uploading episode actions: " + localActions); + GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(localActions); lastUpdate = postResponse.timestamp; Log.d(TAG, "Upload episode response: " + postResponse); - GpodnetPreferences.removeQueuedEpisodeActions(episodeActions); + GpodnetPreferences.removeQueuedEpisodeActions(localActions); } GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(lastUpdate); clearErrorNotifications(); @@ -183,33 +185,45 @@ public class GpodnetSyncService extends Service { } } - private synchronized void processEpisodeActions(List episodeActions) throws DownloadRequestException { - if(episodeActions.size() == 0) { + private synchronized void processEpisodeActions(List localActions, List remoteActions) throws DownloadRequestException { + if(remoteActions.size() == 0) { return; } - Map, GpodnetEpisodeAction> mostRecentPlayAction = new HashMap, GpodnetEpisodeAction>(); - for (GpodnetEpisodeAction episodeAction : episodeActions) { - switch (episodeAction.getAction()) { + Map, GpodnetEpisodeAction> localMostRecentPlayAction = new HashMap, GpodnetEpisodeAction>(); + Map, GpodnetEpisodeAction> remoteMostRecentPlayAction = new HashMap, GpodnetEpisodeAction>(); + // make sure more recent local actions are not overwritten by older remote actions + for(GpodnetEpisodeAction action : localActions) { + Pair key = new Pair(action.getPodcast(), action.getEpisode()); + GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key); + if (mostRecent == null) { + localMostRecentPlayAction.put(key, action); + } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { + localMostRecentPlayAction.put(key, action); + } + } + for (GpodnetEpisodeAction action : remoteActions) { + switch (action.getAction()) { case NEW: - FeedItem newItem = DBReader.getFeedItem(this, episodeAction.getPodcast(), episodeAction.getEpisode()); + FeedItem newItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode()); if(newItem != null) { DBWriter.markItemRead(this, newItem, false, true); } else { - Log.i(TAG, "Unknown feed item: " + episodeAction); + Log.i(TAG, "Unknown feed item: " + action); } 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); + Pair key = new Pair(action.getPodcast(), action.getEpisode()); + GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key); + if(localMostRecent == null || + localMostRecent.getTimestamp().before(action.getTimestamp())) { + GpodnetEpisodeAction mostRecent = remoteMostRecentPlayAction.get(key); + if (mostRecent == null) { + remoteMostRecentPlayAction.put(key, action); + } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { + remoteMostRecentPlayAction.put(key, action); + } } break; case DELETE: @@ -217,10 +231,10 @@ public class GpodnetSyncService extends Service { break; } } - for (GpodnetEpisodeAction episodeAction : mostRecentPlayAction.values()) { - FeedItem playItem = DBReader.getFeedItem(this, episodeAction.getPodcast(), episodeAction.getEpisode()); + for (GpodnetEpisodeAction action : remoteMostRecentPlayAction.values()) { + FeedItem playItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode()); if (playItem != null) { - playItem.getMedia().setPosition(episodeAction.getPosition() * 1000); + playItem.getMedia().setPosition(action.getPosition() * 1000); if(playItem.getMedia().hasAlmostEnded()) { DBWriter.markItemRead(this, playItem, true, true); DBWriter.addItemToPlaybackHistory(this, playItem.getMedia()); -- cgit v1.2.3 From fbf1d8373c4e6bd38a73af7e8ff0abe932da0ad8 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sun, 5 Apr 2015 19:48:57 +0200 Subject: Minor changes: Log, import order, small refactorings --- .../antennapod/core/feed/EventDistributor.java | 14 +- .../de/danoeh/antennapod/core/feed/FeedItem.java | 8 + .../core/service/playback/PlaybackService.java | 100 +++------ .../playback/PlaybackServiceTaskManager.java | 15 +- .../core/storage/APCleanupAlgorithm.java | 4 +- .../danoeh/antennapod/core/storage/DBReader.java | 31 ++- .../de/danoeh/antennapod/core/storage/DBTasks.java | 6 +- .../danoeh/antennapod/core/storage/DBWriter.java | 70 +++--- .../de/danoeh/antennapod/core/util/LongList.java | 241 +++++++++++++++++++++ .../danoeh/antennapod/core/util/QueueAccess.java | 21 +- .../core/util/playback/PlaybackController.java | 36 +-- 11 files changed, 371 insertions(+), 175 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/LongList.java (limited to 'core/src') diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java index 5a2cfa40e..6655a7522 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -5,8 +5,6 @@ import android.util.Log; import org.apache.commons.lang3.Validate; -import de.danoeh.antennapod.core.BuildConfig; - import java.util.AbstractQueue; import java.util.Observable; import java.util.Observer; @@ -71,23 +69,17 @@ public class EventDistributor extends Observable { private void processEventQueue() { Integer result = 0; - if (BuildConfig.DEBUG) - Log.d(TAG, - "Processing event queue. Number of events: " - + events.size()); + Log.d(TAG, "Processing event queue. Number of events: " + events.size()); for (Integer current = events.poll(); current != null; current = events .poll()) { result |= current; } if (result != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Notifying observers. Data: " + result); + Log.d(TAG, "Notifying observers. Data: " + result); setChanged(); notifyObservers(result); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Event queue didn't contain any new events. Observers will not be notified."); + Log.d(TAG, "Event queue didn't contain any new events. Observers will not be notified."); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java index 5a4d869e7..4fd7a184c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -2,6 +2,9 @@ package de.danoeh.antennapod.core.feed; 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.List; import java.util.concurrent.Callable; @@ -384,4 +387,9 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr public boolean hasChapters() { return hasChapters; } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } } 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 c1563a0fa..43c345fec 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 @@ -36,7 +36,6 @@ import org.apache.commons.lang3.StringUtils; import java.io.IOException; import java.util.List; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Chapter; @@ -184,8 +183,7 @@ public class PlaybackService extends Service { @Override public boolean onUnbind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onUnbind event"); + Log.d(TAG, "Received onUnbind event"); return super.onUnbind(intent); } @@ -219,8 +217,7 @@ public class PlaybackService extends Service { @Override public void onCreate() { super.onCreate(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service created."); + Log.d(TAG, "Service created."); isRunning = true; registerReceiver(headsetDisconnected, new IntentFilter( @@ -247,8 +244,7 @@ public class PlaybackService extends Service { @Override public void onDestroy() { super.onDestroy(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); + Log.d(TAG, "Service is about to be destroyed"); isRunning = false; started = false; currentMediaType = MediaType.UNKNOWN; @@ -264,8 +260,7 @@ public class PlaybackService extends Service { @Override public IBinder onBind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onBind event"); + Log.d(TAG, "Received onBind event"); return mBinder; } @@ -273,8 +268,7 @@ public class PlaybackService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); - if (BuildConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); + Log.d(TAG, "OnStartCommand called"); final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); if (keycode == -1 && playable == null) { @@ -283,14 +277,12 @@ public class PlaybackService extends Service { } if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); stopForeground(true); } else { if (keycode != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received media button event"); + Log.d(TAG, "Received media button event"); handleKeycode(keycode); } else { started = true; @@ -310,8 +302,7 @@ public class PlaybackService extends Service { * Handles media button events */ private void handleKeycode(int keycode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling keycode: " + keycode); + Log.d(TAG, "Handling keycode: " + keycode); final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); final PlayerStatus status = info.playerStatus; switch (keycode) { @@ -381,8 +372,7 @@ public class PlaybackService extends Service { * mediaplayer. */ public void setVideoSurface(SurfaceHolder sh) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting display"); + Log.d(TAG, "Setting display"); mediaPlayer.setVideoSurface(sh); } @@ -465,22 +455,6 @@ public class PlaybackService extends Service { .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: @@ -489,10 +463,8 @@ public class PlaybackService extends Service { break; case PLAYING: - if (BuildConfig.DEBUG) - Log.d(TAG, "Audiofocus successfully requested"); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resuming/Starting playback"); + Log.d(TAG, "Audiofocus successfully requested"); + Log.d(TAG, "Resuming/Starting playback"); taskManager.startPositionSaver(); taskManager.startWidgetUpdater(); @@ -574,8 +546,7 @@ public class PlaybackService extends Service { }; private void endPlayback(boolean playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback ended"); + Log.d(TAG, "Playback ended"); final Playable playable = mediaPlayer.getPSMPInfo().playable; if (playable == null) { @@ -602,7 +573,7 @@ public class PlaybackService extends Service { // isInQueue remains false } if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); + DBWriter.removeQueueItem(PlaybackService.this, item, true); } DBWriter.addItemToPlaybackHistory(PlaybackService.this, media); @@ -643,8 +614,7 @@ public class PlaybackService extends Service { UserPreferences.isFollowQueue(); if (loadNextItem) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading next item in queue"); + Log.d(TAG, "Loading next item in queue"); nextMedia = nextItem.getMedia(); } final boolean prepareImmediately; @@ -770,8 +740,7 @@ public class PlaybackService extends Service { } private void writePlayerStatusPlaybackPreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Writing player status playback preferences"); + Log.d(TAG, "Writing player status playback preferences"); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); @@ -820,8 +789,7 @@ public class PlaybackService extends Service { @Override protected Void doInBackground(Void... params) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting background work"); + Log.d(TAG, "Starting background work"); if (android.os.Build.VERSION.SDK_INT >= 11) { if (info.playable != null) { try { @@ -931,8 +899,7 @@ public class PlaybackService extends Service { notification = notificationBuilder.build(); } startForeground(NOTIFICATION_ID, notification); - if (BuildConfig.DEBUG) - Log.d(TAG, "Notification set up"); + Log.d(TAG, "Notification set up"); } } @@ -958,8 +925,7 @@ public class PlaybackService extends Service { float playbackSpeed = getCurrentPlaybackSpeed(); final Playable playable = mediaPlayer.getPSMPInfo().playable; if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Saving current position to " + position); + Log.d(TAG, "Saving current position to " + position); if (updatePlayedDuration && playable instanceof FeedMedia) { FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); @@ -968,8 +934,7 @@ public class PlaybackService extends Service { if (isAutoFlattrable(media) && (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.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); } @@ -1062,8 +1027,7 @@ public class PlaybackService extends Service { editor.apply(); } - if (BuildConfig.DEBUG) - Log.d(TAG, "RemoteControlClient state was refreshed"); + Log.d(TAG, "RemoteControlClient state was refreshed"); } } } @@ -1106,15 +1070,12 @@ public class PlaybackService extends Service { if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { int state = intent.getIntExtra("state", -1); if (state != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset plug event. State is " + state); + Log.d(TAG, "Headset plug event. State is " + state); if (state == UNPLUGGED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset was unplugged during playback."); + Log.d(TAG, "Headset was unplugged during playback."); pauseIfPauseOnDisconnect(); } else if (state == PLUGGED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset was plugged in during playback."); + Log.d(TAG, "Headset was plugged in during playback."); unpauseIfPauseOnDisconnect(); } } else { @@ -1131,8 +1092,7 @@ public class PlaybackService extends Service { int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1); int prevState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, -1); if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received bluetooth connection intent"); + Log.d(TAG, "Received bluetooth connection intent"); unpauseIfPauseOnDisconnect(); } } @@ -1144,8 +1104,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { // sound is about to change, eg. bluetooth -> speaker - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback because audio is becoming noisy"); + Log.d(TAG, "Pausing playback because audio is becoming noisy"); pauseIfPauseOnDisconnect(); } // android.media.AUDIO_BECOMING_NOISY @@ -1191,8 +1150,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); + Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); mediaPlayer.endPlayback(); } } @@ -1202,8 +1160,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { if (StringUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent"); + Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent"); mediaPlayer.resume(); } } @@ -1213,8 +1170,7 @@ public class PlaybackService extends Service { @Override public void onReceive(Context context, Intent intent) { if (StringUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent"); + Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent"); mediaPlayer.pause(false, false); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index 1865afa6f..3e414a8b7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -5,14 +5,21 @@ import android.util.Log; import org.apache.commons.lang3.Validate; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.QueueEvent; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.Playable; - -import java.util.List; -import java.util.concurrent.*; +import de.greenrobot.event.EventBus; /** * Manages the background tasks of PlaybackSerivce, i.e. diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java index 0164e914b..de6c02de7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java @@ -12,7 +12,7 @@ import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.LongList; /** * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod. @@ -24,7 +24,7 @@ public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm { public int performCleanup(Context context, Integer episodeNumber) { List candidates = new ArrayList(); List downloadedItems = DBReader.getDownloadedItems(context); - QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context)); + LongList queue = DBReader.getQueueIDList(context); List delete; for (FeedItem item : downloadedItems) { if (item.hasMedia() && item.getMedia().isDownloaded() 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 e10fffc18..a7c98c7c6 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 @@ -21,6 +21,7 @@ import de.danoeh.antennapod.core.feed.SimpleChapter; import de.danoeh.antennapod.core.feed.VorbisCommentChapter; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; @@ -338,8 +339,7 @@ public final class DBReader { } static List getQueue(Context context, PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting queue"); + Log.d(TAG, "getQueue()"); Cursor itemlistCursor = adapter.getQueueCursor(); List items = extractItemlistFromCursor(adapter, @@ -358,21 +358,21 @@ public final class DBReader { * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. */ - public static List getQueueIDList(Context context) { + public static LongList getQueueIDList(Context context) { PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - List result = getQueueIDList(adapter); + LongList result = getQueueIDList(adapter); adapter.close(); return result; } - static List getQueueIDList(PodDBAdapter adapter) { + static LongList getQueueIDList(PodDBAdapter adapter) { adapter.open(); Cursor queueCursor = adapter.getQueueIDCursor(); - List queueIds = new ArrayList(queueCursor.getCount()); + LongList queueIds = new LongList(queueCursor.getCount()); if (queueCursor.moveToFirst()) { do { queueIds.add(queueCursor.getLong(0)); @@ -382,6 +382,22 @@ public final class DBReader { } + /** + * Return the size of the queue. + * + * @param context A context that is used for opening a database connection. + * @return Size of the queue. + */ + public static int getQueueSize(Context context) { + Log.d(TAG, "getQueueSize()"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + int size = adapter.getQueueSize(); + adapter.close(); + return size; + } + /** * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using * {@link #getQueueIDList(android.content.Context)} instead. @@ -391,8 +407,7 @@ public final class DBReader { * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. */ public static List getQueue(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting queue"); + Log.d(TAG, "getQueue()"); PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index e0e370b0d..9fa17bf72 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -34,7 +34,7 @@ import de.danoeh.antennapod.core.service.GpodnetSyncService; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException; import de.danoeh.antennapod.core.util.flattr.FlattrUtils; @@ -524,8 +524,8 @@ public final class DBTasks { * @param feedItemId ID of the FeedItem */ public static boolean isInQueue(Context context, final long feedItemId) { - List queue = DBReader.getQueueIDList(context); - return QueueAccess.IDListAccess(queue).contains(feedItemId); + LongList queue = DBReader.getQueueIDList(context); + return queue.contains(feedItemId); } private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter, 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 53fe5149b..63c52b488 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 @@ -33,17 +33,19 @@ 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.feed.QueueEvent; 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.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; +import de.greenrobot.event.EventBus; /** * Provides methods for writing data to AntennaPod's database. @@ -126,16 +128,15 @@ public class DBWriter { // Gpodder: queue delete action for synchronization if(GpodnetPreferences.loggedIn()) { FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DELETE) + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE) .currentDeviceId() .currentTimestamp() .build(); GpodnetPreferences.enqueueEpisodeAction(action); } } - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting File. Result: " + result); - EventDistributor.getInstance().sendQueueUpdateBroadcast(); + Log.d(TAG, "Deleting File. Result: " + result); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.DELETED_MEDIA, media.getItem())); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); } } @@ -370,8 +371,7 @@ public class DBWriter { } if (queueModified) { adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, index)); } if (unreadItemsModified && item != null) { adapter.setSingleFeedItem(item); @@ -439,8 +439,7 @@ public class DBWriter { } if (queueModified) { adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue)); } if (unreadItemsModified) { adapter.setFeedItemlist(itemsToSave); @@ -471,7 +470,7 @@ public class DBWriter { adapter.clearQueue(); adapter.close(); - EventDistributor.getInstance().sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.CLEARED)); } }); } @@ -480,11 +479,11 @@ public class DBWriter { * Removes a FeedItem object from the queue. * * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem that should be removed. + * @param item FeedItem that should be removed. * @param performAutoDownload true if an auto-download process should be started after the operation. */ public static Future removeQueueItem(final Context context, - final long itemId, final boolean performAutoDownload) { + final FeedItem item, final boolean performAutoDownload) { return dbExec.submit(new Runnable() { @Override @@ -498,16 +497,14 @@ public class DBWriter { if (queue != null) { boolean queueModified = false; QueueAccess queueAccess = QueueAccess.ItemListAccess(queue); - if (queueAccess.contains(itemId)) { - item = DBReader.getFeedItem(context, itemId); + if (queueAccess.contains(item.getId())) { if (item != null) { - queueModified = queueAccess.remove(itemId); + queueModified = queueAccess.remove(item.getId()); } } if (queueModified) { adapter.setQueue(queue); - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item)); } else { Log.w(TAG, "Queue was not modified by call to removeQueueItem"); } @@ -535,16 +532,13 @@ public class DBWriter { return dbExec.submit(new Runnable() { @Override public void run() { - List queueIdList = DBReader.getQueueIDList(context); - int currentLocation = 0; - for (long id : queueIdList) { - if (id == itemId) { - moveQueueItemHelper(context, currentLocation, 0, broadcastUpdate); - return; - } - currentLocation++; + LongList queueIdList = DBReader.getQueueIDList(context); + int index = queueIdList.indexOf(itemId); + if (index >=0) { + moveQueueItemHelper(context, index, 0, broadcastUpdate); + } else { + Log.e(TAG, "moveQueueItemToTop: item not found"); } - Log.e(TAG, "moveQueueItemToTop: item not found"); } }); } @@ -562,17 +556,14 @@ public class DBWriter { return dbExec.submit(new Runnable() { @Override public void run() { - List queueIdList = DBReader.getQueueIDList(context); - int currentLocation = 0; - for (long id : queueIdList) { - if (id == itemId) { - moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1, - broadcastUpdate); - return; - } - currentLocation++; + LongList queueIdList = DBReader.getQueueIDList(context); + int index = queueIdList.indexOf(itemId); + if(index >= 0) { + moveQueueItemHelper(context, index, queueIdList.size() - 1, + broadcastUpdate); + } else { + Log.e(TAG, "moveQueueItemToBottom: item not found"); } - Log.e(TAG, "moveQueueItemToBottom: item not found"); } }); } @@ -626,8 +617,8 @@ public class DBWriter { adapter.setQueue(queue); if (broadcastUpdate) { - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item)); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, to)); } } @@ -1036,8 +1027,7 @@ public class DBWriter { Collections.sort(queue, comparator); adapter.setQueue(queue); if (broadcastUpdate) { - EventDistributor.getInstance() - .sendQueueUpdateBroadcast(); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.SORTED)); } } else { Log.e(TAG, "sortQueue: Could not load queue"); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java new file mode 100644 index 000000000..f5d0cab0c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java @@ -0,0 +1,241 @@ +package de.danoeh.antennapod.core.util; + +import java.util.Arrays; + +/** + * Fast and memory efficient long list + */ +public final class LongList { + + private long[] values; + private int size; + + /** + * Constructs an empty instance with a default initial capacity. + */ + public LongList() { + this(4); + } + + /** + * Constructs an empty instance. + * + * @param initialCapacity {@code >= 0;} initial capacity of the list + */ + public LongList(int initialCapacity) { + if(initialCapacity < 0) { + throw new IllegalArgumentException("initial capacity must be 0 or higher"); + } + values = new long[initialCapacity]; + size = 0; + } + + @Override + public int hashCode() { + Arrays.hashCode(values); + int hashCode = 1; + for (int i = 0; i < size; i++) { + long value = values[i]; + hashCode = 31 * hashCode + (int)(value ^ (value >>> 32)); + } + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (! (other instanceof LongList)) { + return false; + } + LongList otherList = (LongList) other; + if (size != otherList.size) { + return false; + } + for (int i = 0; i < size; i++) { + if (values[i] != otherList.values[i]) { + return false; + } + } + return true; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(size * 5 + 10); + sb.append("LongList{"); + for (int i = 0; i < size; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(values[i]); + } + sb.append("}"); + return sb.toString(); + } + + /** + * Gets the number of elements in this list. + */ + public int size() { + return size; + } + + /** + * Gets the indicated value. + * + * @param n {@code >= 0, < size();} which element + * @return the indicated element's value + */ + public long get(int n) { + if (n >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(n < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + return values[n]; + } + + /** + * Sets the value at the given index. + * + * @param index the index at which to put the specified object. + * @param value the object to add. + * @return the previous element at the index. + */ + public long set(int index, long value) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + long result = values[index]; + values[index] = value; + return result; + } + + /** + * Adds an element to the end of the list. This will increase the + * list's capacity if necessary. + * + * @param value the value to add + */ + public void add(long value) { + growIfNeeded(); + values[size++] = value; + } + + /** + * Inserts element into specified index, moving elements at and above + * that index up one. May not be used to insert at an index beyond the + * current size (that is, insertion as a last element is legal but + * no further). + * + * @param n {@code >= 0, <=size();} index of where to insert + * @param value value to insert + */ + public void insert(int n, int value) { + if (n > size) { + throw new IndexOutOfBoundsException("n > size()"); + } else if(n < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + + growIfNeeded(); + + System.arraycopy (values, n, values, n+1, size - n); + values[n] = value; + size++; + } + + /** + * Removes value from this list. + * + * @param value value to remove + * return {@code true} if the value was removed, {@code false} otherwise + */ + public boolean remove(long value) { + for (int i = 0; i < size; i++) { + if (values[i] == value) { + size--; + System.arraycopy(values, i+1, values, i, size-i); + return true; + } + } + return false; + } + + /** + * Removes an element at a given index, shifting elements at greater + * indicies down one. + * + * @param index index of element to remove + */ + public void removeIndex(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + size--; + System.arraycopy (values, index + 1, values, index, size - index); + } + + /** + * Increases size of array if needed + */ + private void growIfNeeded() { + if (size == values.length) { + // Resize. + long[] newArray = new long[size * 3 / 2 + 10]; + System.arraycopy(values, 0, newArray, 0, size); + values = newArray; + } + } + + /** + * Returns the index of the given value, or -1 if the value does not + * appear in the list. + * + * @param value value to find + * @return index of value or -1 + */ + public int indexOf(long value) { + for (int i = 0; i < size; i++) { + if (values[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes all values from this list. + */ + public void clear() { + values = new long[4]; + size = 0; + } + + + /** + * Returns true if the given value is contained in the list + * + * @param value value to look for + * @return {@code true} if this list contains {@code value}, {@code false} otherwise + */ + public boolean contains(long value) { + return indexOf(value) >= 0; + } + + /** + * Returns an array with a copy of this list's values + * + * @return array with a copy of this list's values + */ + public long[] toArray() { + return Arrays.copyOf(values, size); + + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java index 8e40ae184..7377b202d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java @@ -1,10 +1,10 @@ package de.danoeh.antennapod.core.util; -import de.danoeh.antennapod.core.feed.FeedItem; - import java.util.Iterator; import java.util.List; +import de.danoeh.antennapod.core.feed.FeedItem; + /** * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List. @@ -25,23 +25,6 @@ public abstract class QueueAccess { public abstract boolean remove(long id); private QueueAccess() { - - } - - public static QueueAccess IDListAccess(final List ids) { - return new QueueAccess() { - @Override - public boolean contains(long id) { - return (ids != null) && ids.contains(id); - } - - @Override - public boolean remove(long id) { - return ids.remove(id); - } - - - }; } public static QueueAccess ItemListAccess(final List items) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index 26dd2ec4c..17c752bb6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -1,7 +1,13 @@ package de.danoeh.antennapod.core.util.playback; import android.app.Activity; -import android.content.*; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.content.res.TypedArray; import android.media.MediaPlayer; import android.os.AsyncTask; @@ -19,6 +25,13 @@ import android.widget.TextView; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Chapter; @@ -33,8 +46,6 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; -import java.util.concurrent.*; - /** * Communicates with the playback service. GUI classes should use this class to * control playback instead of communicating with the PlaybackService directly. @@ -118,8 +129,7 @@ public abstract class PlaybackController { * example in the activity's onStop() method. */ public void release() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Releasing PlaybackController"); + Log.d(TAG, "Releasing PlaybackController"); try { activity.unregisterReceiver(statusUpdate); @@ -177,7 +187,7 @@ public abstract class PlaybackController { boolean bound = false; if (!PlaybackService.started) { if (serviceIntent != null) { - if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service"); + Log.d(TAG, "Calling start service"); activity.startService(serviceIntent); bound = activity.bindService(serviceIntent, mConnection, 0); } else { @@ -186,14 +196,11 @@ public abstract class PlaybackController { handleStatus(); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "PlaybackService is running, trying to connect without start command."); + Log.d(TAG, "PlaybackService is running, trying to connect without start command."); bound = activity.bindService(new Intent(activity, PlaybackService.class), mConnection, 0); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Result for service binding: " + bound); + Log.d(TAG, "Result for service binding: " + bound); } }; intentLoader.execute(); @@ -272,8 +279,7 @@ public abstract class PlaybackController { .getService(); if (!released) { queryService(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Connection to Service established"); + Log.d(TAG, "Connection to Service established"); } else { Log.i(TAG, "Connection to playback service has been established, but controller has already been released"); } @@ -282,9 +288,7 @@ public abstract class PlaybackController { @Override public void onServiceDisconnected(ComponentName name) { playbackService = null; - if (BuildConfig.DEBUG) - Log.d(TAG, "Disconnected from Service"); - + Log.d(TAG, "Disconnected from Service"); } }; -- cgit v1.2.3 From b2a50983efe865b473e8be590deedee5a62113b7 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sun, 5 Apr 2015 19:53:21 +0200 Subject: smart mark as played: media player activity, player widget, when media is changed --- .../playback/PlaybackServiceMediaPlayer.java | 82 ++++++++++++---------- 1 file changed, 43 insertions(+), 39 deletions(-) (limited to 'core/src') diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java index b7c02011d..f0acc3531 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -24,11 +24,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; -import de.danoeh.antennapod.core.BuildConfig; 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.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.playback.AudioPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.core.util.playback.Playable; @@ -91,7 +93,7 @@ public class PlaybackServiceMediaPlayer { new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); + Log.d(TAG, "Rejected execution of runnable"); } } ); @@ -137,7 +139,7 @@ public class PlaybackServiceMediaPlayer { public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Validate.notNull(playable); - if (BuildConfig.DEBUG) Log.d(TAG, "Play media object."); + Log.d(TAG, "playMediaObject(...)"); executor.submit(new Runnable() { @Override public void run() { @@ -164,16 +166,16 @@ public class PlaybackServiceMediaPlayer { */ private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Validate.notNull(playable); - if (!playerLock.isHeldByCurrentThread()) + if (!playerLock.isHeldByCurrentThread()) { throw new IllegalStateException("method requires playerLock"); + } if (media != null) { if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) && playerStatus == PlayerStatus.PLAYING) { // episode is already playing -> ignore method call - if (BuildConfig.DEBUG) - Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); + Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); return; } else { // stop playback of this episode @@ -184,6 +186,23 @@ public class PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING) { setPlayerStatus(PlayerStatus.PAUSED, media); } + + // smart mark as played + if(media != null && media instanceof FeedMedia) { + FeedMedia oldMedia = (FeedMedia) media; + if(oldMedia.hasAlmostEnded()) { + Log.d(TAG, "smart mark as read"); + FeedItem item = oldMedia.getItem(); + DBWriter.markItemRead(context, item, true, false); + DBWriter.removeQueueItem(context, item, false); + DBWriter.addItemToPlaybackHistory(context, oldMedia); + if (UserPreferences.isAutoDelete()) { + Log.d(TAG, "Delete " + oldMedia.toString()); + DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId()); + } + } + } + setPlayerStatus(PlayerStatus.INDETERMINATE, null); } } @@ -281,11 +300,10 @@ public class PlaybackServiceMediaPlayer { media.onPlaybackStart(); } else { - if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); + Log.e(TAG, "Failed to request audio focus"); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); + Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); } } @@ -307,8 +325,7 @@ public class PlaybackServiceMediaPlayer { playerLock.lock(); releaseWifiLockIfNecessary(); if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback."); + Log.d(TAG, "Pausing playback."); mediaPlayer.pause(); setPlayerStatus(PlayerStatus.PAUSED, media); @@ -320,8 +337,7 @@ public class PlaybackServiceMediaPlayer { reinit(); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); } playerLock.unlock(); @@ -342,8 +358,7 @@ public class PlaybackServiceMediaPlayer { playerLock.lock(); if (playerStatus == PlayerStatus.INITIALIZED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Preparing media player"); + Log.d(TAG, "Preparing media player"); setPlayerStatus(PlayerStatus.PREPARING, media); try { mediaPlayer.prepare(); @@ -370,8 +385,7 @@ public class PlaybackServiceMediaPlayer { throw new IllegalStateException("Player is not in PREPARING state"); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Resource prepared"); + Log.d(TAG, "Resource prepared"); if (mediaType == MediaType.VIDEO) { VideoPlayer vp = (VideoPlayer) mediaPlayer; @@ -383,8 +397,7 @@ public class PlaybackServiceMediaPlayer { } if (media.getDuration() == 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting duration of media"); + Log.d(TAG, "Setting duration of media"); media.setDuration(mediaPlayer.getDuration()); } setPlayerStatus(PlayerStatus.PREPARED, media); @@ -412,8 +425,7 @@ public class PlaybackServiceMediaPlayer { } else if (mediaPlayer != null) { mediaPlayer.reset(); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); } playerLock.unlock(); } @@ -567,8 +579,7 @@ public class PlaybackServiceMediaPlayer { if (media != null && media.getMediaType() == MediaType.AUDIO) { if (mediaPlayer.canSetSpeed()) { mediaPlayer.setPlaybackSpeed((float) speed); - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback speed was set to " + speed); + Log.d(TAG, "Playback speed was set to " + speed); callback.playbackSpeedChanged(speed); } } @@ -651,8 +662,7 @@ public class PlaybackServiceMediaPlayer { @Override public void run() { playerLock.lock(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting video surface"); + Log.d(TAG, "Resetting video surface"); mediaPlayer.setDisplay(null); reinit(); playerLock.unlock(); @@ -716,7 +726,7 @@ public class PlaybackServiceMediaPlayer { private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { Validate.notNull(newStatus); - if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); + Log.d(TAG, "Setting player status to " + newStatus); this.playerStatus = newStatus; this.media = newMedia; @@ -788,17 +798,15 @@ public class PlaybackServiceMediaPlayer { // If there is an incoming call, playback should be paused permanently TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); final int callState = (tm != null) ? tm.getCallState() : 0; - if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState); + Log.d(TAG, "Call state: " + callState); Log.i(TAG, "Call state:" + callState); if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); + Log.d(TAG, "Lost audio focus"); pause(true, false); callback.shouldStop(); } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); + Log.d(TAG, "Gained audio focus"); if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now resume(); } else { // we ducked => raise audio level back @@ -808,22 +816,19 @@ public class PlaybackServiceMediaPlayer { } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { if (playerStatus == PlayerStatus.PLAYING) { if (!UserPreferences.shouldPauseForFocusLoss()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, 0); pausedBecauseOfTransientAudiofocusLoss = false; } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } @@ -873,8 +878,7 @@ public class PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.STOPPED, null); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); + Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); } playerLock.unlock(); -- cgit v1.2.3 From 8850c09920723559895062050163817979bcd8d2 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sun, 5 Apr 2015 20:49:11 +0200 Subject: QueueEvents, Queue shows undobar on removal of an item --- .../antennapod/core/feed/EventDistributor.java | 5 --- .../de/danoeh/antennapod/core/feed/QueueEvent.java | 51 ++++++++++++++++++++++ .../playback/PlaybackServiceTaskManager.java | 17 +++----- .../danoeh/antennapod/core/storage/DBWriter.java | 34 ++++++++------- .../core/util/gui/UndoBarController.java | 47 +++++++++----------- 5 files changed, 97 insertions(+), 57 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java (limited to 'core/src') diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java index 6655a7522..20a85d43f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -24,7 +24,6 @@ public class EventDistributor extends Observable { public static final int FEED_LIST_UPDATE = 1; public static final int UNREAD_ITEMS_UPDATE = 2; - public static final int QUEUE_UPDATE = 4; public static final int DOWNLOADLOG_UPDATE = 8; public static final int PLAYBACK_HISTORY_UPDATE = 16; public static final int DOWNLOAD_QUEUED = 32; @@ -97,10 +96,6 @@ public class EventDistributor extends Observable { addEvent(UNREAD_ITEMS_UPDATE); } - public void sendQueueUpdateBroadcast() { - addEvent(QUEUE_UPDATE); - } - public void sendFeedUpdateBroadcast() { addEvent(FEED_LIST_UPDATE); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java new file mode 100644 index 000000000..9f1eec754 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java @@ -0,0 +1,51 @@ +package de.danoeh.antennapod.core.feed; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + +public class QueueEvent { + + public enum Action { + ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED + } + + public final Action action; + public final FeedItem item; + public final int position; + public final List items; + + public QueueEvent(Action action) { + this(action, null, null, -1); + } + + public QueueEvent(Action action, FeedItem item) { + this(action, item, null, -1); + } + + public QueueEvent(Action action, FeedItem item, int position) { + this(action, item, null, position); + } + + public QueueEvent(Action action, List items) { + this(action, null, items, -1); + } + + private QueueEvent(Action action, FeedItem item, List items, int position) { + this.action = action; + this.item = item; + this.items = items; + this.position = position; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("item", item) + .append("items", items) + .append("position", position) + .toString(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index 3e414a8b7..cde03adea 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -76,18 +76,13 @@ public class PlaybackServiceTaskManager { } }); loadQueue(); - EventDistributor.getInstance().register(eventDistributorListener); + EventBus.getDefault().register(this); } - private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { - cancelQueueLoader(); - loadQueue(); - } - } - }; + public void onEvent(QueueEvent event) { + cancelQueueLoader(); + loadQueue(); + } private synchronized boolean isQueueLoaderActive() { return queueFuture != null && !queueFuture.isDone(); @@ -319,7 +314,7 @@ public class PlaybackServiceTaskManager { * execution of this method. */ public synchronized void shutdown() { - EventDistributor.getInstance().unregister(eventDistributorListener); + EventBus.getDefault().unregister(this); cancelAllTasks(); schedExecutor.shutdown(); } 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 63c52b488..bd0cfee5b 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 @@ -41,7 +41,6 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; @@ -350,8 +349,7 @@ public class DBWriter { public void run() { final PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - final List queue = DBReader - .getQueue(context, adapter); + final List queue = DBReader.getQueue(context, adapter); FeedItem item = null; if (queue != null) { @@ -490,21 +488,14 @@ public class DBWriter { public void run() { final PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - final List queue = DBReader - .getQueue(context, adapter); - FeedItem item = null; + final List queue = DBReader.getQueue(context, adapter); if (queue != null) { - boolean queueModified = false; - QueueAccess queueAccess = QueueAccess.ItemListAccess(queue); - if (queueAccess.contains(item.getId())) { - if (item != null) { - queueModified = queueAccess.remove(item.getId()); - } - } - if (queueModified) { + int position = queue.indexOf(item); + if(position >= 0) { + queue.remove(position); adapter.setQueue(queue); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item)); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item, position)); } else { Log.w(TAG, "Queue was not modified by call to removeQueueItem"); } @@ -628,6 +619,19 @@ public class DBWriter { adapter.close(); } + /** + * 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); + } + + /** * Sets the 'read'-attribute of a FeedItem to the specified value. * diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java index 0e03bc8b4..e947dc5d0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java @@ -1,9 +1,6 @@ package de.danoeh.antennapod.core.util.gui; -import android.os.Bundle; import android.os.Handler; -import android.os.Parcelable; -import android.text.TextUtils; import android.view.View; import android.widget.TextView; @@ -16,23 +13,36 @@ import de.danoeh.antennapod.core.R; import static com.nineoldandroids.view.ViewPropertyAnimator.animate; -public class UndoBarController { +public class UndoBarController { private View mBarView; private TextView mMessageView; private ViewPropertyAnimator mBarAnimator; private Handler mHideHandler = new Handler(); - private UndoListener mUndoListener; + private UndoListener mUndoListener; // State objects - private Parcelable mUndoToken; + private T mUndoToken; private CharSequence mUndoMessage; - public interface UndoListener { - void onUndo(Parcelable token); + public interface UndoListener { + /** + * This callback function is called when the undo button is pressed + * + * @param token + */ + void onUndo(T token); + + /** + * + * This callback function is called when the bar fades out without button press + * + * @param token + */ + void onHide(T token); } - public UndoBarController(View undoBarView, UndoListener undoListener) { + public UndoBarController(View undoBarView, UndoListener undoListener) { mBarView = undoBarView; mBarAnimator = animate(mBarView); mUndoListener = undoListener; @@ -50,7 +60,7 @@ public class UndoBarController { hideUndoBar(true); } - public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) { + public void showUndoBar(boolean immediate, CharSequence message, T undoToken) { mUndoToken = undoToken; mUndoMessage = message; mMessageView.setText(mUndoMessage); @@ -96,26 +106,11 @@ public class UndoBarController { } } - public void onSaveInstanceState(Bundle outState) { - outState.putCharSequence("undo_message", mUndoMessage); - outState.putParcelable("undo_token", mUndoToken); - } - - public void onRestoreInstanceState(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mUndoMessage = savedInstanceState.getCharSequence("undo_message"); - mUndoToken = savedInstanceState.getParcelable("undo_token"); - - if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) { - showUndoBar(true, mUndoMessage, mUndoToken); - } - } - } - private Runnable mHideRunnable = new Runnable() { @Override public void run() { hideUndoBar(false); + mUndoListener.onHide(mUndoToken); } }; } -- cgit v1.2.3 From e8a4bd7c116087caf254317d8b8834c4e6e74cdc Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sun, 5 Apr 2015 23:45:28 +0200 Subject: ProGuard config, small fix --- core/src/main/res/values/arrays.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'core/src') diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 48ab40d61..4bb29ac85 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -2,7 +2,7 @@ - off + 0 15 30 45 -- cgit v1.2.3 From 0b4b328324489e68ada274faeb751ac79d7cca96 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Mon, 6 Apr 2015 00:26:08 +0200 Subject: EventBus license, fixed NPE with undobar onHide(), no smarking on closing of audio player activity --- .../de/danoeh/antennapod/core/util/gui/UndoBarController.java | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'core/src') diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java index e947dc5d0..23d8cf7c7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java @@ -83,6 +83,14 @@ public class UndoBarController { } } + public boolean isShowing() { + return mBarView.getVisibility() == View.VISIBLE; + } + + public void close() { + mHideHandler.post(mHideRunnable); + } + public void hideUndoBar(boolean immediate) { mHideHandler.removeCallbacks(mHideRunnable); if (immediate) { -- cgit v1.2.3