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') 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') 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') 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') 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 --- core/build.gradle | 1 + .../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 +++++++++----------- 6 files changed, 98 insertions(+), 57 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java (limited to 'core') diff --git a/core/build.gradle b/core/build.gradle index 710378a18..c327f194c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -45,4 +45,5 @@ dependencies { compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0' compile 'com.squareup.okio:okio:1.2.0' compile 'com.nineoldandroids:library:2.4.0' + compile 'de.greenrobot:eventbus:2.4.0' } 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') 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') 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 From 76b6ae654b8b23334c977e4f56fd9157ea982585 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Mon, 6 Apr 2015 18:13:44 +0200 Subject: Refactorings --- .../java/com/aocate/media/AndroidMediaPlayer.java | 4 +- .../core/preferences/UserPreferences.java | 66 +++++++++++++--------- .../playback/PlaybackServiceTaskManager.java | 6 +- 3 files changed, 45 insertions(+), 31 deletions(-) (limited to 'core') diff --git a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java index 17ee74a13..7c2ea3d61 100644 --- a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java +++ b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java @@ -14,13 +14,13 @@ package com.aocate.media; -import java.io.IOException; - import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import android.util.Log; +import java.io.IOException; + public class AndroidMediaPlayer extends MediaPlayerImpl { private final static String AMP_TAG = "AocateAndroidMediaPlayer"; 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 022c03ca7..69507eaef 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 @@ -58,7 +58,8 @@ public class UserPreferences implements private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; - private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; + private static final String PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"; + private static final String PREF_REWIND_SECS = "prefRewindSecs"; private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront"; @@ -93,7 +94,8 @@ public class UserPreferences implements private String playbackSpeed; private String[] playbackSpeedArray; private boolean pauseForFocusLoss; - private int seekDeltaSecs; + private int fastForwardSecs; + private int rewindSecs; private boolean isFreshInstall; private int notifyPriority; private boolean persistNotify; @@ -109,8 +111,7 @@ public class UserPreferences implements * @throws IllegalArgumentException if context is null */ public static void createInstance(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating new instance of UserPreferences"); + Log.d(TAG, "Creating new instance of UserPreferences"); Validate.notNull(context); instance = new UserPreferences(context); @@ -157,7 +158,8 @@ public class UserPreferences implements playbackSpeedArray = readPlaybackSpeedArray(sp.getString( PREF_PLAYBACK_SPEED_ARRAY, null)); pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); + fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30); + rewindSecs = sp.getInt(PREF_REWIND_SECS, 30); if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { notifyPriority = NotificationCompat.PRIORITY_MAX; } @@ -343,9 +345,14 @@ public class UserPreferences implements return instance.playbackSpeedArray; } - public static int getSeekDeltaMs() { + public static int getFastFowardSecs() { instanceAvailable(); - return 1000 * instance.seekDeltaSecs; + return instance.fastForwardSecs; + } + + public static int getRewindSecs() { + instanceAvailable(); + return instance.rewindSecs; } /** @@ -429,8 +436,10 @@ public class UserPreferences implements PREF_PLAYBACK_SPEED_ARRAY, null)); } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) { pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - } else if (key.equals(PREF_SEEK_DELTA_SECS)) { - seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); + } else if (key.equals(PREF_FAST_FORWARD_SECS)) { + fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30); + } else if (key.equals(PREF_REWIND_SECS)) { + rewindSecs = sp.getInt(PREF_REWIND_SECS, 30); } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) { pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); } else if (key.equals(PREF_UNPAUSE_ON_HEADSET_RECONNECT)) { @@ -450,6 +459,18 @@ public class UserPreferences implements } } + public static void setPrefFastForwardSecs(int secs) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); + editor.putInt(PREF_FAST_FORWARD_SECS, secs); + editor.commit(); + } + + public static void setPrefRewindSecs(int secs) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); + editor.putInt(PREF_REWIND_SECS, secs); + editor.commit(); + } + public static void setPlaybackSpeed(String speed) { PreferenceManager.getDefaultSharedPreferences(instance.context).edit() .putString(PREF_PLAYBACK_SPEED, speed).apply(); @@ -524,8 +545,7 @@ public class UserPreferences implements .getDefaultSharedPreferences(context.getApplicationContext()); String strDir = prefs.getString(PREF_DATA_FOLDER, null); if (strDir == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Using default data folder"); + Log.d(TAG, "Using default data folder"); return context.getExternalFilesDir(type); } else { File dataDir = new File(strDir); @@ -556,8 +576,7 @@ public class UserPreferences implements if (!typeDir.exists()) { if (dataDir.canWrite()) { if (!typeDir.mkdir()) { - Log.e(TAG, "Could not create data folder named " - + type); + Log.e(TAG, "Could not create data folder named " + type); return null; } } @@ -569,8 +588,7 @@ public class UserPreferences implements } public static void setDataFolder(String dir) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Result from DirectoryChooser: " + dir); + Log.d(TAG, "Result from DirectoryChooser: " + dir); instanceAvailable(); SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(instance.context); @@ -607,16 +625,13 @@ public class UserPreferences implements IMPORT_DIR); if (importDir != null) { if (importDir.exists()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Import directory already exists"); + Log.d(TAG, "Import directory already exists"); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating import directory"); + Log.d(TAG, "Creating import directory"); importDir.mkdir(); } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Could not access external storage."); + Log.d(TAG, "Could not access external storage."); } } @@ -625,8 +640,7 @@ public class UserPreferences implements */ public static void restartUpdateAlarm(long triggerAtMillis, long intervalMillis) { instanceAvailable(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Restarting update alarm."); + Log.d(TAG, "Restarting update alarm."); AlarmManager alarmManager = (AlarmManager) instance.context .getSystemService(Context.ALARM_SERVICE); PendingIntent updateIntent = PendingIntent.getBroadcast( @@ -635,11 +649,9 @@ public class UserPreferences implements if (intervalMillis != 0) { alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtMillis, intervalMillis, updateIntent); - if (BuildConfig.DEBUG) - Log.d(TAG, "Changed alarm to new interval"); + Log.d(TAG, "Changed alarm to new interval"); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Automatic update was deactivated"); + Log.d(TAG, "Automatic update was deactivated"); } } 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 cde03adea..fc73c9446 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 @@ -19,8 +19,10 @@ 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 de.greenrobot.event.EventBus; + /** * Manages the background tasks of PlaybackSerivce, i.e. * the sleep timer, the position saver, the widget updater and @@ -147,9 +149,9 @@ public class PlaybackServiceTaskManager { positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); - if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); + Log.d(TAG, "Started PositionSaver"); } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); + Log.d(TAG, "Call to startPositionSaver was ignored."); } } -- cgit v1.2.3 From fd30ec8189a1d9b47adfa31a9fe7dbc2085061f6 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Mon, 6 Apr 2015 18:32:06 +0200 Subject: Set fast forward and rewind time --- .../core/service/playback/PlaybackService.java | 9 ++-- .../playback/PlaybackServiceMediaPlayer.java | 21 ++++----- .../core/util/playback/PlaybackController.java | 50 +++++----------------- core/src/main/res/values/arrays.xml | 4 +- core/src/main/res/values/strings.xml | 4 +- 5 files changed, 28 insertions(+), 60 deletions(-) (limited to 'core') 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 43c345fec..cab63891b 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 @@ -344,11 +344,11 @@ public class PlaybackService extends Service { break; case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs()); + mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_REWIND: - mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs()); + mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); break; case KeyEvent.KEYCODE_MEDIA_STOP: if (status == PlayerStatus.PLAYING) { @@ -481,9 +481,8 @@ public class PlaybackService extends Service { } Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED); - statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); + // statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); sendBroadcast(statusUpdate); - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); updateWidget(); refreshRemoteControlClientState(newInfo); bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); @@ -626,7 +625,6 @@ public class PlaybackService extends Service { prepareImmediately = startWhenPrepared = true; } else { Log.d(TAG, "No more episodes available to play"); - prepareImmediately = startWhenPrepared = false; stopForeground(true); stopWidgetUpdater(); @@ -933,7 +931,6 @@ public class PlaybackService extends Service { // Auto flattr if (isAutoFlattrable(media) && (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - 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); 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 f0acc3531..448ab05b9 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 @@ -449,15 +449,15 @@ public class PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - if (stream) { - // statusBeforeSeeking = playerStatus; - // setPlayerStatus(PlayerStatus.SEEKING, media); + if (!stream) { + statusBeforeSeeking = playerStatus; + setPlayerStatus(PlayerStatus.SEEKING, media); } mediaPlayer.seekTo(t); } else if (playerStatus == PlayerStatus.INITIALIZED) { media.setPosition(t); - startWhenPrepared.set(true); + startWhenPrepared.set(false); prepare(); } playerLock.unlock(); @@ -534,20 +534,20 @@ public class PlaybackServiceMediaPlayer { * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. */ public int getPosition() { - if (!playerLock.tryLock()) { - return INVALID_TIME; - } + playerLock.lock(); int retVal = INVALID_TIME; if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { + || playerStatus == PlayerStatus.PREPARED + || playerStatus == PlayerStatus.SEEKING) { retVal = mediaPlayer.getCurrentPosition(); } else if (media != null && media.getPosition() > 0) { retVal = media.getPosition(); } playerLock.unlock(); + Log.d(TAG, "getPosition() -> " + retVal); return retVal; } @@ -735,6 +735,7 @@ public class PlaybackServiceMediaPlayer { int state; if (playerStatus != null) { + Log.d(TAG, "playerStatus: " + playerStatus.toString()); switch (playerStatus) { case PLAYING: state = PlaybackStateCompat.STATE_PLAYING; @@ -1095,13 +1096,13 @@ public class PlaybackServiceMediaPlayer { @Override public void onFastForward() { super.onFastForward(); - seekDelta(UserPreferences.getSeekDeltaMs()); + seekDelta(UserPreferences.getFastFowardSecs() * 1000); } @Override public void onRewind() { super.onRewind(); - seekDelta(-UserPreferences.getSeekDeltaMs()); + seekDelta(-UserPreferences.getRewindSecs() * 1000); } @Override 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 17c752bb6..a0d12d3e7 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 @@ -32,13 +32,11 @@ 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; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.core.service.playback.PlayerStatus; @@ -174,8 +172,7 @@ public abstract class PlaybackController { * as the arguments of the launch intent. */ private void bindToService() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Trying to connect to service"); + Log.d(TAG, "Trying to connect to service"); AsyncTask intentLoader = new AsyncTask() { @Override protected Intent doInBackground(Void... voids) { @@ -211,8 +208,7 @@ public abstract class PlaybackController { * played media or null if no last played media could be found. */ private Intent getPlayLastPlayedMediaIntent() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Trying to restore last played media"); + Log.d(TAG, "Trying to restore last played media"); SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(activity.getApplicationContext()); long currentlyPlayingMedia = PlaybackPreferences @@ -240,8 +236,7 @@ public abstract class PlaybackController { return serviceIntent; } } - if (BuildConfig.DEBUG) - Log.d(TAG, "No last played media found"); + Log.d(TAG, "No last played media found"); return null; } @@ -253,8 +248,7 @@ public abstract class PlaybackController { || (positionObserverFuture != null && positionObserverFuture .isDone()) || positionObserverFuture == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting up position observer"); + Log.d(TAG, "Setting up position observer"); positionObserver = new MediaPositionObserver(); positionObserverFuture = schedExecutor.scheduleWithFixedDelay( positionObserver, MediaPositionObserver.WAITING_INTERVALL, @@ -266,8 +260,7 @@ public abstract class PlaybackController { private void cancelPositionObserver() { if (positionObserverFuture != null) { boolean result = positionObserverFuture.cancel(true); - if (BuildConfig.DEBUG) - Log.d(TAG, "PositionObserver cancelled. Result: " + result); + Log.d(TAG, "PositionObserver cancelled. Result: " + result); } } @@ -295,8 +288,7 @@ public abstract class PlaybackController { protected BroadcastReceiver statusUpdate = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received statusUpdate Intent."); + Log.d(TAG, "Received statusUpdate Intent."); if (isConnectedToPlaybackService()) { PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); status = info.playerStatus; @@ -353,8 +345,7 @@ public abstract class PlaybackController { } } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Bad arguments. Won't handle intent"); + Log.d(TAG, "Bad arguments. Won't handle intent"); } } else { bindToService(); @@ -425,6 +416,7 @@ public abstract class PlaybackController { pauseResource = R.drawable.ic_av_pause_circle_outline_80dp; } + Log.d(TAG, "status: " + status.toString()); switch (status) { case ERROR: @@ -470,6 +462,7 @@ public abstract class PlaybackController { updatePlayButtonAppearance(playResource, playText); break; case SEEKING: + onPositionObserverUpdate(); postStatusMsg(R.string.player_seeking_msg); break; case INITIALIZED: @@ -505,8 +498,7 @@ public abstract class PlaybackController { * information has to be refreshed */ void queryService() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Querying service info"); + Log.d(TAG, "Querying service info"); if (playbackService != null) { status = playbackService.getStatus(); media = playbackService.getPlayable(); @@ -614,28 +606,6 @@ public abstract class PlaybackController { }; } - public OnClickListener newOnRevButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(-UserPreferences.getSeekDeltaMs()); - } - } - }; - } - - public OnClickListener newOnFFButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(UserPreferences.getSeekDeltaMs()); - } - } - }; - } - public boolean serviceAvailable() { return playbackService != null; } diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 4bb29ac85..979653c79 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -10,7 +10,7 @@ - + 5 10 15 @@ -18,7 +18,7 @@ 30 45 60 - + Manual diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index bdb3ad606..a2ab068fd 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -268,8 +268,8 @@ Change the login information for your gpodder.net account. Playback Speeds Customize the speeds available for variable speed audio playback - Seek time - Seek this many seconds when rewinding or fast-forwarding + Fast forward time + Rewind time Set hostname Use default host Expand Notification -- cgit v1.2.3 From 78a5700ded9f6cd1ac31e99203d628cc5a585b88 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 10 Apr 2015 14:30:42 +0200 Subject: Some additional logging --- .../java/de/danoeh/antennapod/core/preferences/UserPreferences.java | 2 ++ 1 file changed, 2 insertions(+) (limited to 'core') 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 69507eaef..8e4ab1a49 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 @@ -460,12 +460,14 @@ public class UserPreferences implements } public static void setPrefFastForwardSecs(int secs) { + Log.d(TAG, "setPrefFastForwardSecs(" + secs +")"); SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); editor.putInt(PREF_FAST_FORWARD_SECS, secs); editor.commit(); } public static void setPrefRewindSecs(int secs) { + Log.d(TAG, "setPrefRewindSecs(" + secs +")"); SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); editor.putInt(PREF_REWIND_SECS, secs); editor.commit(); -- cgit v1.2.3 From cf7738effe4230e2fbfc54dc327bc17b2ab1fc4c Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 10 Apr 2015 20:37:18 +0200 Subject: LoudnessEnhancer for Android 4.4+ --- .../main/java/com/aocate/media/AndroidMediaPlayer.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'core') diff --git a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java index 17ee74a13..c0aeba722 100644 --- a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java +++ b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java @@ -14,13 +14,15 @@ package com.aocate.media; -import java.io.IOException; - import android.content.Context; import android.media.MediaPlayer; +import android.media.audiofx.LoudnessEnhancer; import android.net.Uri; +import android.os.Build; import android.util.Log; +import java.io.IOException; + public class AndroidMediaPlayer extends MediaPlayerImpl { private final static String AMP_TAG = "AocateAndroidMediaPlayer"; @@ -205,6 +207,15 @@ public class AndroidMediaPlayer extends MediaPlayerImpl { Log.d(AMP_TAG, " ++++++++++++++++++++++++++++++++ Setting prepared listener to this.onPreparedListener"); mp.setOnPreparedListener(this.onPreparedListener); mp.setOnSeekCompleteListener(this.onSeekCompleteListener); + + // loudness enhancer + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + int audioSession = mp.getAudioSessionId(); + LoudnessEnhancer effect = new LoudnessEnhancer(audioSession); + effect.setTargetGain(600); // amplify up to 6 dB + effect.setEnabled(true); + Log.d(AMP_TAG, "Loudness enhancer enabled"); + } } @Override -- cgit v1.2.3 From 07ce9579fb61174ce173e84daebe60e493bd725c Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 11 Apr 2015 11:12:33 +0200 Subject: Refactoring --- .../core/service/download/HttpDownloader.java | 39 ++++++++-------------- 1 file changed, 13 insertions(+), 26 deletions(-) (limited to 'core') diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index 7abb6df5e..2cfea1f25 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -2,7 +2,6 @@ package de.danoeh.antennapod.core.service.download; import android.util.Log; -import com.squareup.okhttp.Credentials; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; @@ -18,19 +17,20 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URI; import java.net.UnknownHostException; import java.util.Date; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URIUtil; +import okio.ByteString; public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; @@ -99,13 +99,11 @@ public class HttpDownloader extends Downloader { Response response = httpClient.newCall(httpReq.build()).execute(); responseBody = response.body(); - String contentEncodingHeader = response.header("Content-Encoding"); + boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); - final boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); + Log.d(TAG, "Response code is " + response.code()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Response code is " + response.code()); if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled"); @@ -151,22 +149,18 @@ public class HttpDownloader extends Downloader { out = new RandomAccessFile(destination, "rw"); } - byte[] buffer = new byte[BUFFER_SIZE]; int count = 0; request.setStatusMsg(R.string.download_running); - if (BuildConfig.DEBUG) - Log.d(TAG, "Getting size of download"); + Log.d(TAG, "Getting size of download"); request.setSize(responseBody.contentLength() + request.getSoFar()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Size is " + request.getSize()); + Log.d(TAG, "Size is " + request.getSize()); if (request.getSize() < 0) { request.setSize(DownloadStatus.SIZE_UNKNOWN); } long freeSpace = StorageUtils.getFreeSpaceAvailable(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Free space is " + freeSpace); + Log.d(TAG, "Free space is " + freeSpace); if (request.getSize() != DownloadStatus.SIZE_UNKNOWN && request.getSize() > freeSpace) { @@ -174,8 +168,7 @@ public class HttpDownloader extends Downloader { return; } - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting download"); + Log.d(TAG, "Starting download"); while (!cancelled && (count = connection.read(buffer)) != -1) { out.write(buffer, 0, count); @@ -226,15 +219,12 @@ public class HttpDownloader extends Downloader { } private void onSuccess() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was successful"); + Log.d(TAG, "Download was successful"); result.setSuccessful(); } private void onFail(DownloadError reason, String reasonDetailed) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Download failed"); - } + Log.d(TAG, "Download failed"); result.setFailed(reason, reasonDetailed); if (request.isDeleteOnFailure()) { cleanup(); @@ -242,8 +232,7 @@ public class HttpDownloader extends Downloader { } private void onCancelled() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was cancelled"); + Log.d(TAG, "Download was cancelled"); result.setCancelled(); cleanup(); } @@ -256,12 +245,10 @@ public class HttpDownloader extends Downloader { File dest = new File(request.getDestination()); if (dest.exists()) { boolean rc = dest.delete(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + rc); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "cleanup() didn't delete file: does not exist."); + Log.d(TAG, "cleanup() didn't delete file: does not exist."); } } } -- cgit v1.2.3 From a34acb71d117a153606f0af26c1a297e7e524455 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 11 Apr 2015 11:13:26 +0200 Subject: Add feed: remember credentials after selecting feed, try UTF-8 for HTTP basic authentication --- .../core/service/download/HttpDownloader.java | 34 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) (limited to 'core') diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index 2cfea1f25..f7ce38cd5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -81,11 +81,12 @@ public class HttpDownloader extends Downloader { if (userInfo != null) { String[] parts = userInfo.split(":"); if (parts.length == 2) { - String credentials = Credentials.basic(parts[0], parts[1]); + String credentials = encodeCredentials(parts[0], parts[1], "ISO-8859-1"); httpReq.header("Authorization", credentials); } } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { - String credentials = Credentials.basic(request.getUsername(), request.getPassword()); + String credentials = encodeCredentials(request.getUsername(), request.getPassword(), + "ISO-8859-1"); httpReq.header("Authorization", credentials); } @@ -104,6 +105,24 @@ public class HttpDownloader extends Downloader { Log.d(TAG, "Response code is " + response.code()); + if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoding"); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + String credentials = encodeCredentials(parts[0], parts[1], "UTF-8"); + httpReq.header("Authorization", credentials); + } + } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + String credentials = encodeCredentials(request.getUsername(), request.getPassword(), + "UTF-8"); + httpReq.header("Authorization", credentials); + } + response = httpClient.newCall(httpReq.build()).execute(); + responseBody = response.body(); + contentEncodingHeader = response.header("Content-Encoding"); + isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); + } if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled"); @@ -253,4 +272,15 @@ public class HttpDownloader extends Downloader { } } + private static String encodeCredentials(String username, String password, String charset) { + try { + String credentials = username + ":" + password; + byte[] bytes = credentials.getBytes(charset); + String encoded = ByteString.of(bytes).base64(); + return "Basic " + encoded; + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } + } -- cgit v1.2.3 From 9bd6bcf9d3f0114c907ac2fad0a95d1106121a25 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 11 Apr 2015 23:46:33 +0200 Subject: Picasso can handle basic authentication --- .../antennapod/core/asynctask/PicassoProvider.java | 52 +++++++++++++++++++++- .../core/service/download/HttpDownloader.java | 2 +- .../danoeh/antennapod/core/storage/DBReader.java | 29 ++++++++++++ .../antennapod/core/storage/PodDBAdapter.java | 14 ++++++ 4 files changed, 95 insertions(+), 2 deletions(-) (limited to 'core') diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java index b6ece6dc8..e91ae3322 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java @@ -6,8 +6,12 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaMetadataRetriever; import android.net.Uri; +import android.text.TextUtils; import android.util.Log; +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Response; import com.squareup.picasso.Cache; import com.squareup.picasso.LruCache; import com.squareup.picasso.OkHttpDownloader; @@ -22,13 +26,18 @@ import org.apache.commons.lang3.StringUtils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.HttpURLConnection; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import de.danoeh.antennapod.core.service.download.HttpDownloader; +import de.danoeh.antennapod.core.storage.DBReader; + /** * Provides access to Picasso instances. */ public class PicassoProvider { + private static final String TAG = "PicassoProvider"; private static final boolean DEBUG = false; @@ -56,10 +65,12 @@ public class PicassoProvider { if (picassoSetup) { return; } + OkHttpClient client = new OkHttpClient(); + client.networkInterceptors().add(new BasicAuthenticationInterceptor(appContext)); Picasso picasso = new Picasso.Builder(appContext) .indicatorsEnabled(DEBUG) .loggingEnabled(DEBUG) - .downloader(new OkHttpDownloader(appContext)) + .downloader(new OkHttpDownloader(client)) .addRequestHandler(new MediaRequestHandler(appContext)) .executor(getExecutorService()) .memoryCache(getMemoryCache(appContext)) @@ -75,6 +86,45 @@ public class PicassoProvider { picassoSetup = true; } + private static class BasicAuthenticationInterceptor implements Interceptor { + + private final Context context; + + public BasicAuthenticationInterceptor(Context context) { + this.context = context; + } + + @Override + public Response intercept(Chain chain) throws IOException { + com.squareup.okhttp.Request request = chain.request(); + String url = request.urlString(); + // add authentication + String authentication = DBReader.getImageAuthentication(context, url); + if(TextUtils.isEmpty(authentication) == false) { + String[] auth = authentication.split(":"); + String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1"); + com.squareup.okhttp.Request newRequest = request + .newBuilder() + .addHeader("Authorization", credentials) + .build(); + Response response = chain.proceed(newRequest); + if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8"); + newRequest = request + .newBuilder() + .addHeader("Authorization", credentials) + .build(); + return chain.proceed(newRequest); + } else { + return response; + } + } + else { // no authentication required + return chain.proceed(request); + } + } + } + private static class MediaRequestHandler extends RequestHandler { final Context context; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index f7ce38cd5..ac0fe8036 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -272,7 +272,7 @@ public class HttpDownloader extends Downloader { } } - private static String encodeCredentials(String username, String password, String charset) { + public static String encodeCredentials(String username, String password, String charset) { try { String credentials = username + ":" + password; byte[] bytes = credentials.getBytes(charset); 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 a7c98c7c6..aafe4071a 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 @@ -711,6 +711,35 @@ public final class DBReader { return item; } + /** + * Returns credentials based on image URL + * + * @param context A context that is used for opening a database connection. + * @param imageUrl The URL of the image + * @return Credentials in format ":", empty String if no authorization given + */ + public static String getImageAuthentication(final Context context, final String imageUrl) { + Log.d(TAG, "Loading credentials for image with URL " + imageUrl); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + String credentials = getImageAuthentication(context, imageUrl, adapter); + adapter.close(); + return credentials; + + } + + static String getImageAuthentication(final Context context, final String imageUrl, PodDBAdapter adapter) { + String credentials = null; + Cursor cursor = adapter.getImageAuthenticationCursor(imageUrl); + if (cursor.moveToFirst()) { + String username = cursor.getString(0); + String password = cursor.getString(1); + return username + ":" + password; + } + return ""; + } + /** * Loads a specific FeedItem from the database. * 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 f518a4f5f..229f3d034 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 @@ -1146,6 +1146,20 @@ public class PodDBAdapter { return db.rawQuery(query, null); } + public Cursor getImageAuthenticationCursor(final String imageUrl) { + final String query = "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + + TABLE_NAME_FEED_IMAGES + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + TABLE_NAME_FEEDS + "." + KEY_IMAGE + " WHERE " + + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "' UNION SELECT " + + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEED_IMAGES + " INNER JOIN " + + TABLE_NAME_FEED_ITEMS + " ON " + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "'"; + Log.d(TAG, "Query: " + query); + 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); -- cgit v1.2.3 From e5c03079b6e5e1f73bfccaf02fbdeba4560b7e89 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sun, 12 Apr 2015 01:40:09 +0200 Subject: Another fix... --- core/build.gradle | 6 +-- .../antennapod/core/asynctask/PicassoProvider.java | 45 ++++++++++++---------- .../antennapod/core/gpoddernet/GpodnetService.java | 7 +++- 3 files changed, 33 insertions(+), 25 deletions(-) (limited to 'core') diff --git a/core/build.gradle b/core/build.gradle index c327f194c..61978e898 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -40,9 +40,9 @@ dependencies { compile 'commons-io:commons-io:2.4' compile 'com.jayway.android.robotium:robotium-solo:5.2.1' compile 'org.jsoup:jsoup:1.7.3' - compile 'com.squareup.picasso:picasso:2.4.0' - compile 'com.squareup.okhttp:okhttp:2.2.0' - compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0' + compile 'com.squareup.picasso:picasso:2.5.2' + compile 'com.squareup.okhttp:okhttp:2.3.0' + compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0' compile 'com.squareup.okio:okio:1.2.0' compile 'com.nineoldandroids:library:2.4.0' compile 'de.greenrobot:eventbus:2.4.0' diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java index e91ae3322..0b637c682 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java @@ -66,7 +66,7 @@ public class PicassoProvider { return; } OkHttpClient client = new OkHttpClient(); - client.networkInterceptors().add(new BasicAuthenticationInterceptor(appContext)); + client.interceptors().add(new BasicAuthenticationInterceptor(appContext)); Picasso picasso = new Picasso.Builder(appContext) .indicatorsEnabled(DEBUG) .loggingEnabled(DEBUG) @@ -98,29 +98,32 @@ public class PicassoProvider { public Response intercept(Chain chain) throws IOException { com.squareup.okhttp.Request request = chain.request(); String url = request.urlString(); - // add authentication String authentication = DBReader.getImageAuthentication(context, url); - if(TextUtils.isEmpty(authentication) == false) { - String[] auth = authentication.split(":"); - String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1"); - com.squareup.okhttp.Request newRequest = request + + if(TextUtils.isEmpty(authentication)) { + Log.d(TAG, "no credentials for '" + url + "'"); + return chain.proceed(request); + } + + // add authentication + String[] auth = authentication.split(":"); + String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1"); + com.squareup.okhttp.Request newRequest = request + .newBuilder() + .addHeader("Authorization", credentials) + .build(); + Log.d(TAG, "Basic authentication with ISO-8859-1 encoding"); + Response response = chain.proceed(newRequest); + if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8"); + newRequest = request .newBuilder() .addHeader("Authorization", credentials) .build(); - Response response = chain.proceed(newRequest); - if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { - credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8"); - newRequest = request - .newBuilder() - .addHeader("Authorization", credentials) - .build(); - return chain.proceed(newRequest); - } else { - return response; - } - } - else { // no authentication required - return chain.proceed(request); + Log.d(TAG, "Basic authentication with UTF-8 encoding"); + return chain.proceed(newRequest); + } else { + return response; } } } @@ -140,7 +143,7 @@ public class PicassoProvider { } @Override - public Result load(Request data) throws IOException { + public Result load(Request data, int networkPolicy) throws IOException { Bitmap bitmap = null; MediaMetadataRetriever mmr = null; try { 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 db242c3bc..23d89a3a3 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 @@ -726,7 +726,12 @@ public class GpodnetService { Validate.notNull(body); ByteArrayOutputStream outputStream; - int contentLength = (int) body.contentLength(); + int contentLength = 0; + try { + contentLength = (int) body.contentLength(); + } catch (IOException ignore) { + // ignore + } if (contentLength > 0) { outputStream = new ByteArrayOutputStream(contentLength); } else { -- cgit v1.2.3 From 15d09932da213edbc43f5c0733108a51e1a397ad Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sun, 12 Apr 2015 13:44:00 +0200 Subject: Coherent item cover order --- .../antennapod/core/asynctask/PicassoProvider.java | 8 +-- .../de/danoeh/antennapod/core/feed/FeedItem.java | 6 +- .../de/danoeh/antennapod/core/feed/FeedMedia.java | 72 +++++++++++++--------- 3 files changed, 47 insertions(+), 39 deletions(-) (limited to 'core') diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java index 0b637c682..4f2d5b204 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java @@ -162,13 +162,7 @@ public class PicassoProvider { } if (bitmap == null) { - // check for fallback Uri - String fallbackParam = data.uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK); - - if (fallbackParam != null) { - Uri fallback = Uri.parse(fallbackParam); - bitmap = decodeStreamFromFile(data, fallback); - } + Log.wtf(TAG, "THIS SHOULD NEVER EVER HAPPEN!!"); } return new Result(bitmap, Picasso.LoadedFrom.DISK); 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 4fd7a184c..b74875175 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 @@ -315,10 +315,10 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr @Override public Uri getImageUri() { - if (hasItemImageDownloaded()) { - return image.getImageUri(); - } else if (hasMedia()) { + if(media.hasEmbeddedPicture()) { return media.getImageUri(); + } else if (hasItemImageDownloaded()) { + return image.getImageUri(); } else if (feed != null) { return feed.getImageUri(); } else { 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 93f826894..3dda291fa 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 @@ -2,9 +2,11 @@ package de.danoeh.antennapod.core.feed; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; import java.util.Date; import java.util.List; @@ -34,6 +36,7 @@ public class FeedMedia extends FeedFile implements Playable { private String mime_type; private volatile FeedItem item; private Date playbackCompletionDate; + private boolean hasEmbeddedPicture; /* Used for loading item when restoring from parcel. */ private long itemID; @@ -50,6 +53,7 @@ public class FeedMedia extends FeedFile implements Playable { long size, String mime_type, String file_url, String download_url, boolean downloaded, Date playbackCompletionDate, int played_duration) { super(file_url, download_url, downloaded); + checkEmbeddedPicture(); this.id = id; this.item = item; this.duration = duration; @@ -61,12 +65,6 @@ public class FeedMedia extends FeedFile implements Playable { ? null : (Date) playbackCompletionDate.clone(); } - public FeedMedia(long id, FeedItem item) { - super(); - this.id = id; - this.item = item; - } - @Override public String getHumanReadableIdentifier() { if (item != null && item.getTitle() != null) { @@ -227,18 +225,16 @@ public class FeedMedia extends FeedFile implements Playable { return (this.position > 0); } - public FeedImage getImage() { - if (item != null) { - return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage(); - } - return null; - } - @Override public int describeContents() { return 0; } + public boolean hasEmbeddedPicture() { + Log.d(TAG, "hasEmbeddedPicture() -> " + hasEmbeddedPicture); + return this.hasEmbeddedPicture; + } + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); @@ -415,28 +411,46 @@ public class FeedMedia extends FeedFile implements Playable { @Override public Uri getImageUri() { - final Uri feedImgUri = getFeedImageUri(); - - if (localFileAvailable()) { + if (hasEmbeddedPicture) { Uri.Builder builder = new Uri.Builder(); - builder.scheme(SCHEME_MEDIA) - .encodedPath(getLocalMediaUrl()); - if (feedImgUri != null) { - builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString()); - } + builder.scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()); return builder.build(); - } else if (item.hasItemImageDownloaded()) { - return item.getImage().getImageUri(); } else { - return feedImgUri; + return item.getImageUri(); } } - private Uri getFeedImageUri() { - if (item != null && item.getFeed() != null) { - return item.getFeed().getImageUri(); - } else { - return null; + @Override + public void setDownloaded(boolean downloaded) { + super.setDownloaded(downloaded); + checkEmbeddedPicture(); + } + + @Override + public void setFile_url(String file_url) { + super.setFile_url(file_url); + checkEmbeddedPicture(); + } + + private void checkEmbeddedPicture() { + Log.d(TAG, "checkEmbeddedPicture()"); + if (!localFileAvailable()) { + hasEmbeddedPicture = false; + return; + } + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + try { + mmr.setDataSource(getLocalMediaUrl()); + byte[] image = mmr.getEmbeddedPicture(); + if(image != null) { + hasEmbeddedPicture = true; + } + else { + hasEmbeddedPicture = false; + } + } catch (Exception e) { + e.printStackTrace(); + hasEmbeddedPicture = false; } } } -- cgit v1.2.3 From 512ce6ad51d71c576b4c81246db39de98c9fb73a Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 18 Apr 2015 16:16:45 +0200 Subject: Small fix --- core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java | 3 +++ 1 file changed, 3 insertions(+) (limited to 'core') 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 index 6622eab73..fa41fb664 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.util; +import android.util.Log; + import org.apache.commons.lang3.StringUtils; import java.text.ParsePosition; @@ -96,6 +98,7 @@ public class DateUtils { return result; } } + Log.d(TAG, "Could not parse '" + date + "'"); return null; } -- cgit v1.2.3 From 85ace6fb0180940450075990b35bbff6edf52184 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 17 Apr 2015 11:12:05 +0200 Subject: Layout changes and optimizations --- core/src/main/res/values/dimens.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'core') diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml index 81a55142a..c46537b3e 100644 --- a/core/src/main/res/values/dimens.xml +++ b/core/src/main/res/values/dimens.xml @@ -16,10 +16,10 @@ 64dp 100dp 132dp - 42dp + 40dp 48dp 280dp - 56dp + 48dp 14dp 16dp -- cgit v1.2.3 From 158821c0e1953b72dc89bc5896684de3ba1ce1fb Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 17 Apr 2015 11:12:54 +0200 Subject: Preference: Change nav drawer Preference --- .../core/preferences/UserPreferences.java | 23 ++++++++++++++++++++++ core/src/main/res/values/arrays.xml | 10 ++++++++++ core/src/main/res/values/strings.xml | 2 ++ 3 files changed, 35 insertions(+) (limited to 'core') 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 8e4ab1a49..34cc91e63 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 @@ -16,6 +16,8 @@ import org.json.JSONException; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -63,6 +65,7 @@ public class UserPreferences implements private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront"; + public static final String PREF_HIDDEN_DRAWER_ITEMS = "prefHiddenDrawerItems"; // TODO: Make this value configurable private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f; @@ -99,6 +102,7 @@ public class UserPreferences implements private boolean isFreshInstall; private int notifyPriority; private boolean persistNotify; + private List hiddenDrawerItems; private UserPreferences(Context context) { this.context = context; @@ -167,6 +171,7 @@ public class UserPreferences implements notifyPriority = NotificationCompat.PRIORITY_DEFAULT; } persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); + hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ',')); } private int readThemeValue(String valueFromPrefs) { @@ -355,6 +360,11 @@ public class UserPreferences implements return instance.rewindSecs; } + public static List getHiddenDrawerItems() { + instanceAvailable(); + return new ArrayList(instance.hiddenDrawerItems); + } + /** * Returns the capacity of the episode cache. This method will return the * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to @@ -456,6 +466,8 @@ public class UserPreferences implements } } else if (key.equals(PREF_PERSISTENT_NOTIFICATION)) { persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); + } else if (key.equals(PREF_HIDDEN_DRAWER_ITEMS)) { + hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ',')); } } @@ -532,6 +544,17 @@ public class UserPreferences implements instance.autoFlattrPlayedDurationThreshold = autoFlattrThreshold; } + public static void setHiddenDrawerItems(Context context, List items) { + instanceAvailable(); + String str = StringUtils.join(items, ','); + PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()) + .edit() + .putString(PREF_HIDDEN_DRAWER_ITEMS, str) + .commit(); + instance.hiddenDrawerItems = items; + } + + /** * Return the folder where the app stores all of its data. This method will * return the standard data folder if none has been set by the user. diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 979653c79..d5ff84978 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -126,4 +126,14 @@ 0 1 + + + @string/queue_label + @string/new_episodes_label + @string/all_episodes_label + @string/downloads_label + @string/playback_history_label + @string/add_feed_label + + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index a2ab068fd..6a911e887 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -245,6 +245,8 @@ Configure automatic flattring User Interface Select theme + Change navigation drawer + Change which items appear in the navigation drawer. Change the appearance of AntennaPod. Automatic download Configure the automatic download of episodes. -- cgit v1.2.3 From ea628037ca3f9de84744ce0d28d8af7fc06a11dd Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 17 Apr 2015 11:13:27 +0200 Subject: Minor changes --- core/src/main/res/values/arrays.xml | 1 + 1 file changed, 1 insertion(+) (limited to 'core') diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index d5ff84978..ccf14d329 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -118,6 +118,7 @@ 0 + @string/pref_theme_title_light @string/pref_theme_title_dark -- cgit v1.2.3 From ded0006913768fa43390816f4fd0abc214a67718 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 17 Apr 2015 11:20:58 +0200 Subject: Add longclick Drawer Preference dialog --- core/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) (limited to 'core') diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 6a911e887..2882d06c1 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Open menu Close menu + Drawer Preferences Open in browser -- cgit v1.2.3 From fc124a5f1a9b7c6ccc4167ffae3a5897baae71ca Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 17 Apr 2015 13:15:38 +0200 Subject: Adjust size of all episode icon --- .../main/res/drawable-hdpi/ic_feed_grey600_24dp.png | Bin 1601 -> 2359 bytes .../src/main/res/drawable-hdpi/ic_feed_white_24dp.png | Bin 1367 -> 1663 bytes .../main/res/drawable-mdpi/ic_feed_grey600_24dp.png | Bin 1018 -> 1566 bytes .../src/main/res/drawable-mdpi/ic_feed_white_24dp.png | Bin 875 -> 1156 bytes .../main/res/drawable-xhdpi/ic_feed_grey600_24dp.png | Bin 2223 -> 3200 bytes .../main/res/drawable-xhdpi/ic_feed_white_24dp.png | Bin 1933 -> 2314 bytes .../main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png | Bin 3265 -> 4754 bytes .../main/res/drawable-xxhdpi/ic_feed_white_24dp.png | Bin 2884 -> 3406 bytes 8 files changed, 0 insertions(+), 0 deletions(-) (limited to 'core') diff --git a/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png index 46be3e14e..0c3bb0757 100755 Binary files a/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png and b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png index 3d57127f5..667300129 100755 Binary files a/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png and b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png index 79f082610..d46b325d8 100755 Binary files a/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png and b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png index 15a4b16bf..ac94476c2 100755 Binary files a/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png and b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png index 5cb0262ee..b25d64863 100755 Binary files a/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png and b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png index 5f34b0492..3c3e74c1d 100755 Binary files a/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png and b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png index 01ef2ee4d..aacf24d28 100755 Binary files a/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png and b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png index 6dd465852..625dbaa1f 100755 Binary files a/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png and b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png differ -- cgit v1.2.3 From 4aa68e74e099f7a18ee339a70d8a7ec0fdb6dffa Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 17 Apr 2015 13:16:14 +0200 Subject: Bux fixes, tests --- .../java/de/danoeh/antennapod/core/preferences/UserPreferences.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'core') 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 34cc91e63..2c863d378 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 @@ -546,12 +546,12 @@ public class UserPreferences implements public static void setHiddenDrawerItems(Context context, List items) { instanceAvailable(); + instance.hiddenDrawerItems = items; String str = StringUtils.join(items, ','); PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()) .edit() .putString(PREF_HIDDEN_DRAWER_ITEMS, str) .commit(); - instance.hiddenDrawerItems = items; } -- cgit v1.2.3 From 7829ddc94e8ddeb3f7e307205c07439e0f322dcd Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Tue, 21 Apr 2015 17:34:02 +0200 Subject: Show URL in feed info, layout optimizations --- core/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) (limited to 'core') diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index a2ab068fd..f3741e89f 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -48,6 +48,7 @@ Cancel Author Language + URL Settings Picture Error -- cgit v1.2.3 From f35ac3e9e3ecb256dc8d0fa2e9e4462dd587f5a0 Mon Sep 17 00:00:00 2001 From: Tom Hennen Date: Sun, 26 Apr 2015 16:57:19 -0400 Subject: Fixes bug with undo toast. After adding EventBus whenever you move an episode or attempt to swip to remove (but don't actually remove) the undo toast for removing an episode would be displayed. This is now resolved. --- core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java | 2 +- core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'core') 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 index 9f1eec754..c8497f509 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java @@ -8,7 +8,7 @@ import java.util.List; public class QueueEvent { public enum Action { - ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED + ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED } public final Action action; 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 bd0cfee5b..8a5becac6 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 @@ -608,8 +608,7 @@ public class DBWriter { adapter.setQueue(queue); if (broadcastUpdate) { - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item)); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, to)); + EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.MOVED, item, to)); } } -- cgit v1.2.3 From 8050372ba0c0d0ee9f03535866a7ddf0e7c4e744 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Tue, 28 Apr 2015 16:49:41 +0200 Subject: Add ability to retry failed downloads in the download log --- .../core/service/download/DownloadService.java | 7 ++ .../danoeh/antennapod/core/storage/DBReader.java | 91 +++++++++++++++++----- .../antennapod/core/storage/PodDBAdapter.java | 8 ++ 3 files changed, 85 insertions(+), 21 deletions(-) (limited to 'core') 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 d5f17c099..84822666c 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 @@ -939,6 +939,13 @@ public class DownloadService extends Service { if (successful) { + // we create a 'successful' download log if the feed's last refresh failed + List log = DBReader.getFeedDownloadLog(DownloadService.this, feed); + if(log.size() > 0 && log.get(0).isSuccessful() == false) { + saveDownloadStatus(new DownloadStatus(feed, + feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, successful, + reasonDetailed)); + } return Pair.create(request, result); } else { numberOfDownloads.decrementAndGet(); 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 a7c98c7c6..4aa133e72 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 @@ -327,6 +327,21 @@ public final class DBReader { return feed; } + private static DownloadStatus extractDownloadStatusFromCursorRow(final Cursor cursor) { + long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + long feedfileId = cursor.getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); + int feedfileType = cursor.getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); + boolean successful = cursor.getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; + int reason = cursor.getInt(PodDBAdapter.KEY_REASON_INDEX); + String reasonDetailed = cursor.getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); + String title = cursor.getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); + Date completionDate = new Date(cursor.getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)); + + return new DownloadStatus(id, title, feedfileId, + feedfileType, successful, DownloadError.fromCode(reason), completionDate, + reasonDetailed); + } + private static FeedItem getMatchingItemForMedia(long itemId, List items) { @@ -565,27 +580,7 @@ public final class DBReader { if (logCursor.moveToFirst()) { do { - long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - - long feedfileId = logCursor - .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); - int feedfileType = logCursor - .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); - boolean successful = logCursor - .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; - int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX); - String reasonDetailed = logCursor - .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); - String title = logCursor - .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); - Date completionDate = new Date( - logCursor - .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX) - ); - downloadLog.add(new DownloadStatus(id, title, feedfileId, - feedfileType, successful, DownloadError.fromCode(reason), completionDate, - reasonDetailed)); - + downloadLog.add(extractDownloadStatusFromCursorRow(logCursor)); } while (logCursor.moveToNext()); } logCursor.close(); @@ -593,6 +588,60 @@ public final class DBReader { return downloadLog; } + /** + * Loads the download log for a particular feed from the database. + * + * @param context A context that is used for opening a database connection. + * @param feed Feed for which the download log is loaded + * @return A list with DownloadStatus objects that represent the feed's download log, + * newest events first. + */ + public static List getFeedDownloadLog(Context context, Feed feed) { + Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + feed.toString() + ")"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feed.getId()); + List downloadLog = new ArrayList( + cursor.getCount()); + + if (cursor.moveToFirst()) { + do { + downloadLog.add(extractDownloadStatusFromCursorRow(cursor)); + } while (cursor.moveToNext()); + } + cursor.close(); + Collections.sort(downloadLog, new DownloadStatusComparator()); + return downloadLog; + } + + /** + * Loads the download log for a particular feed media from the database. + * + * @param context A context that is used for opening a database connection. + * @param media Feed media for which the download log is loaded + * @return A list with DownloadStatus objects that represent the feed media's download log, + * newest events first. + */ + public static List getFeedMediaDownloadLog(Context context, FeedMedia media) { + Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + media.toString() + ")"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getDownloadLog(FeedMedia.FEEDFILETYPE_FEEDMEDIA, media.getId()); + List downloadLog = new ArrayList( + cursor.getCount()); + + if (cursor.moveToFirst()) { + do { + downloadLog.add(extractDownloadStatusFromCursorRow(cursor)); + } while (cursor.moveToNext()); + } + cursor.close(); + Collections.sort(downloadLog, new DownloadStatusComparator()); + return downloadLog; + } + /** * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about 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 f518a4f5f..8d11f7778 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 @@ -977,6 +977,14 @@ public class PodDBAdapter { return c; } + public final Cursor getDownloadLog(final int feedFileType, final long feedFileId) { + final String query = "SELECT * FROM " + TABLE_NAME_DOWNLOAD_LOG + + " WHERE " + KEY_FEEDFILE + "=" + feedFileId + " AND " + KEY_FEEDFILETYPE + "=" + feedFileType + + " ORDER BY " + KEY_ID + " DESC"; + Cursor c = db.rawQuery(query, null); + return c; + } + public final Cursor getDownloadLogCursor(final int limit) { Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null, null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit); -- cgit v1.2.3 From 21ca881572dfda73b4513821be48fbef7f5d92e9 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Wed, 29 Apr 2015 13:28:56 +0200 Subject: Change icon for failed download(s) report --- .../danoeh/antennapod/core/service/download/DownloadService.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'core') 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 d5f17c099..6703913cc 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 @@ -520,9 +520,7 @@ public class DownloadService extends Service { /** * Creates a notification at the end of the service lifecycle to notify the * user about the number of completed downloads. A report will only be - * created if the number of successfully downloaded feeds is bigger than 1 - * or if there is at least one failed download which is not an image or if - * there is at least one downloaded media file. + * created if there is at least one failed download excluding images */ private void updateReport() { // check if report should be created @@ -556,10 +554,10 @@ public class DownloadService extends Service { getString(R.string.download_report_content), successfulDownloads, failedDownloads) ) - .setSmallIcon(R.drawable.stat_notify_sync) + .setSmallIcon(R.drawable.stat_notify_sync_error) .setLargeIcon( BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync) + R.drawable.stat_notify_sync_error) ) .setContentIntent( ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(this) -- cgit v1.2.3 From fd49d491b72b9261b4ebe3957955c194046f6c32 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Wed, 29 Apr 2015 13:49:08 +0200 Subject: Changed download report icon, ticker and content title --- .../de/danoeh/antennapod/core/service/download/DownloadService.java | 4 ++-- core/src/main/res/values/strings.xml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'core') 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 6703913cc..b3381b4e3 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 @@ -520,7 +520,7 @@ public class DownloadService extends Service { /** * Creates a notification at the end of the service lifecycle to notify the * user about the number of completed downloads. A report will only be - * created if there is at least one failed download excluding images + * created if there is at least one failed download excluding images */ private void updateReport() { // check if report should be created @@ -548,7 +548,7 @@ public class DownloadService extends Service { .setTicker( getString(R.string.download_report_title)) .setContentTitle( - getString(R.string.download_report_title)) + getString(R.string.download_report_content_title)) .setContentText( String.format( getString(R.string.download_report_content), diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index f162bb353..c0c2f06de 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -128,7 +128,8 @@ Authentication error Cancel all downloads Download cancelled - Downloads completed + Downloads completed with error(s) + Download report Malformed URL IO Error Request error -- cgit v1.2.3 From 90afaeb0be3d211afb5d97c5c925d50ad73fb98b Mon Sep 17 00:00:00 2001 From: Tom Hennen Date: Sat, 2 May 2015 09:47:15 -0400 Subject: unregister recivers --- .../de/danoeh/antennapod/core/service/playback/PlaybackService.java | 2 ++ 1 file changed, 2 insertions(+) (limited to 'core') 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 cab63891b..3f6769ee4 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 @@ -254,6 +254,8 @@ public class PlaybackService extends Service { unregisterReceiver(bluetoothStateUpdated); unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); + unregisterReceiver(pausePlayCurrentEpisodeReceiver); + unregisterReceiver(pauseResumeCurrentEpisodeReceiver); mediaPlayer.shutdown(); taskManager.shutdown(); } -- cgit v1.2.3 From c4e2161ad68010c739b87f036b7dfdcbb98f698e Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 2 May 2015 19:54:43 +0200 Subject: Generate lock open and lock closed icons --- .../res/drawable-hdpi/ic_lock_closed_grey600_24dp.png | Bin 0 -> 366 bytes .../main/res/drawable-hdpi/ic_lock_closed_white_24dp.png | Bin 0 -> 358 bytes .../main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png | Bin 0 -> 362 bytes .../main/res/drawable-hdpi/ic_lock_open_white_24dp.png | Bin 0 -> 356 bytes .../res/drawable-mdpi/ic_lock_closed_grey600_24dp.png | Bin 0 -> 242 bytes .../main/res/drawable-mdpi/ic_lock_closed_white_24dp.png | Bin 0 -> 237 bytes .../main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png | Bin 0 -> 242 bytes .../main/res/drawable-mdpi/ic_lock_open_white_24dp.png | Bin 0 -> 238 bytes .../res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png | Bin 0 -> 430 bytes .../main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png | Bin 0 -> 421 bytes .../main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png | Bin 0 -> 427 bytes .../main/res/drawable-xhdpi/ic_lock_open_white_24dp.png | Bin 0 -> 420 bytes .../res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png | Bin 0 -> 630 bytes .../res/drawable-xxhdpi/ic_lock_closed_white_24dp.png | Bin 0 -> 621 bytes .../res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png | Bin 0 -> 627 bytes .../main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png | Bin 0 -> 621 bytes .../res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png | Bin 0 -> 854 bytes .../res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png | Bin 0 -> 839 bytes .../res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png | Bin 0 -> 850 bytes .../main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png | Bin 0 -> 838 bytes core/src/main/res/values/attrs.xml | 2 ++ core/src/main/res/values/styles.xml | 8 ++++++++ 22 files changed, 10 insertions(+) create mode 100644 core/src/main/res/drawable-hdpi/ic_lock_closed_grey600_24dp.png create mode 100644 core/src/main/res/drawable-hdpi/ic_lock_closed_white_24dp.png create mode 100644 core/src/main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png create mode 100644 core/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png create mode 100644 core/src/main/res/drawable-mdpi/ic_lock_closed_grey600_24dp.png create mode 100644 core/src/main/res/drawable-mdpi/ic_lock_closed_white_24dp.png create mode 100644 core/src/main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png create mode 100644 core/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_lock_closed_white_24dp.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png create mode 100644 core/src/main/res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png create mode 100644 core/src/main/res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png create mode 100644 core/src/main/res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png create mode 100644 core/src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png (limited to 'core') diff --git a/core/src/main/res/drawable-hdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_closed_grey600_24dp.png new file mode 100644 index 000000000..b6dba1002 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_lock_closed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_closed_white_24dp.png new file mode 100644 index 000000000..5c60ab08a Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_lock_closed_white_24dp.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png new file mode 100644 index 000000000..a99e9f2b6 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_lock_open_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png new file mode 100644 index 000000000..61c623ce2 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_closed_grey600_24dp.png new file mode 100644 index 000000000..f1627ce34 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_lock_closed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_closed_white_24dp.png new file mode 100644 index 000000000..8f18d11e6 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_lock_closed_white_24dp.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png new file mode 100644 index 000000000..ada8d3be4 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_lock_open_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png new file mode 100644 index 000000000..72d01c406 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png new file mode 100644 index 000000000..ca35f6d0a Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_lock_closed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png new file mode 100644 index 000000000..01fb55ca1 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_lock_closed_white_24dp.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png new file mode 100644 index 000000000..11d9a4b8b Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_lock_open_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png new file mode 100644 index 000000000..01ca4b56c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png new file mode 100644 index 000000000..311a7fa13 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_white_24dp.png new file mode 100644 index 000000000..39a163843 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_lock_closed_white_24dp.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png new file mode 100644 index 000000000..c0552d564 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_lock_open_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png new file mode 100644 index 000000000..46852d54f Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png differ diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png new file mode 100644 index 000000000..e41d5b9ee Binary files /dev/null and b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png new file mode 100644 index 000000000..2376b7334 Binary files /dev/null and b/core/src/main/res/drawable-xxxhdpi/ic_lock_closed_white_24dp.png differ diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png new file mode 100644 index 000000000..c281784dd Binary files /dev/null and b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_grey600_24dp.png differ diff --git a/core/src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png new file mode 100644 index 000000000..25ea3ab99 Binary files /dev/null and b/core/src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png differ diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index 368921f76..caedd7673 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -37,6 +37,8 @@ + + diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index 4ac4a79fd..080203745 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -42,6 +42,8 @@ @drawable/ic_fast_forward_grey600_36dp @drawable/ic_fast_rewind_grey600_36dp @drawable/ic_settings_grey600_24dp + @drawable/ic_lock_open_grey600_24dp + @drawable/ic_lock_closed_grey600_24dp