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
This commit is contained in:
parent
a8d434a803
commit
67cc7c9885
|
@ -1,15 +1,17 @@
|
|||
package de.test.antennapod.util.syndication.feedgenerator;
|
||||
|
||||
import android.util.Xml;
|
||||
import de.danoeh.antennapod.core.feed.Feed;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import de.danoeh.antennapod.core.feed.Feed;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.util.DateUtils;
|
||||
|
||||
/**
|
||||
* Creates Atom feeds. See FeedGenerator for more information.
|
||||
*/
|
||||
|
@ -83,9 +85,9 @@ public class AtomGenerator implements FeedGenerator{
|
|||
if (item.getPubDate() != null) {
|
||||
xml.startTag(null, "published");
|
||||
if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) {
|
||||
xml.text(SyndDateUtils.formatRFC3339Local(item.getPubDate()));
|
||||
xml.text(DateUtils.formatRFC3339Local(item.getPubDate()));
|
||||
} else {
|
||||
xml.text(SyndDateUtils.formatRFC3339UTC(item.getPubDate()));
|
||||
xml.text(DateUtils.formatRFC3339UTC(item.getPubDate()));
|
||||
}
|
||||
xml.endTag(null, "published");
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package de.test.antennapod.util.syndication.feedgenerator;
|
|||
import android.util.Xml;
|
||||
import de.danoeh.antennapod.core.feed.Feed;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
|
||||
import de.danoeh.antennapod.core.util.DateUtils;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -79,7 +79,7 @@ public class RSS2Generator implements FeedGenerator{
|
|||
}
|
||||
if (item.getPubDate() != null) {
|
||||
xml.startTag(null, "pubDate");
|
||||
xml.text(SyndDateUtils.formatRFC822Date(item.getPubDate()));
|
||||
xml.text(DateUtils.formatRFC822Date(item.getPubDate()));
|
||||
xml.endTag(null, "pubDate");
|
||||
}
|
||||
if ((flags & FEATURE_WRITE_GUID) != 0) {
|
||||
|
|
|
@ -135,11 +135,9 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
|
|||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "onStop");
|
||||
Log.d(TAG, "onStop()");
|
||||
cancelLoadTask();
|
||||
EventDistributor.getInstance().unregister(contentUpdate);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -12,19 +12,18 @@ import android.util.Log;
|
|||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Window;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener;
|
||||
import android.widget.TextView;
|
||||
|
||||
import de.danoeh.antennapod.BuildConfig;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.util.Converter;
|
||||
import de.danoeh.antennapod.core.util.ShareUtils;
|
||||
import de.danoeh.antennapod.core.util.StorageUtils;
|
||||
|
@ -167,8 +166,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
|
|||
chooseTheme();
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Creating Activity");
|
||||
Log.d(TAG, "onCreate()");
|
||||
StorageUtils.checkStorageAvailability(this);
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
|
||||
|
@ -224,8 +222,18 @@ public abstract class MediaplayerActivity extends ActionBarActivity
|
|||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Activity stopped");
|
||||
Log.d(TAG, "onStop()");
|
||||
|
||||
// delete if auto delete is enabled and less than 3% of episode is left
|
||||
Playable playable = controller.getMedia();
|
||||
if(playable instanceof FeedMedia) {
|
||||
FeedMedia media = (FeedMedia) playable;
|
||||
if(UserPreferences.isAutoDelete() && media.hasAlmostEnded()) {
|
||||
Log.d(TAG, "Delete " + media.toString());
|
||||
DBWriter.deleteFeedMediaOfItem(this, media.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (controller != null) {
|
||||
controller.release();
|
||||
}
|
||||
|
@ -234,8 +242,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
|
|||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Activity destroyed");
|
||||
Log.d(TAG, "onDestroy()");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -358,8 +365,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
|
|||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Resuming Activity");
|
||||
Log.d(TAG, "onResume()");
|
||||
StorageUtils.checkStorageAvailability(this);
|
||||
controller.init();
|
||||
}
|
||||
|
@ -393,8 +399,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
|
|||
}
|
||||
|
||||
private void updateProgressbarPosition(int position, int duration) {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Updating progressbar info");
|
||||
Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration +")");
|
||||
float progress = ((float) position) / duration;
|
||||
sbPosition.setProgress((int) (progress * sbPosition.getMax()));
|
||||
}
|
||||
|
@ -406,8 +411,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
|
|||
* FeedMedia object.
|
||||
*/
|
||||
protected boolean loadMediaInfo() {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Loading media info");
|
||||
Log.d(TAG, "loadMediaInfo()");
|
||||
Playable media = controller.getMedia();
|
||||
if (media != null) {
|
||||
txtvPosition.setText(Converter.getDurationStringLong((media
|
||||
|
|
|
@ -11,6 +11,8 @@ import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
|
|||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
|
||||
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.storage.DownloadRequestException;
|
||||
|
@ -61,6 +63,19 @@ public class DefaultActionButtonCallback implements ActionButtonCallback {
|
|||
} else {
|
||||
if (!item.isRead()) {
|
||||
DBWriter.markItemRead(context, item, true, true);
|
||||
|
||||
if(GpodnetPreferences.loggedIn()) {
|
||||
// gpodder: send played action
|
||||
FeedMedia media = item.getMedia();
|
||||
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.PLAY)
|
||||
.currentDeviceId()
|
||||
.currentTimestamp()
|
||||
.started(media.getDuration() / 1000)
|
||||
.position(media.getDuration() / 1000)
|
||||
.total(media.getDuration() / 1000)
|
||||
.build();
|
||||
GpodnetPreferences.enqueueEpisodeAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@ import android.net.Uri;
|
|||
import de.danoeh.antennapod.core.BuildConfig;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
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.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
|
@ -156,9 +160,27 @@ public class FeedItemMenuHandler {
|
|||
break;
|
||||
case R.id.mark_read_item:
|
||||
DBWriter.markItemRead(context, selectedItem, true, true);
|
||||
if(GpodnetPreferences.loggedIn()) {
|
||||
FeedMedia media = selectedItem.getMedia();
|
||||
GpodnetEpisodeAction actionPlay = new GpodnetEpisodeAction.Builder(selectedItem, Action.PLAY)
|
||||
.currentDeviceId()
|
||||
.currentTimestamp()
|
||||
.started(media.getDuration() / 1000)
|
||||
.position(media.getDuration() / 1000)
|
||||
.total(media.getDuration() / 1000)
|
||||
.build();
|
||||
GpodnetPreferences.enqueueEpisodeAction(actionPlay);
|
||||
}
|
||||
break;
|
||||
case R.id.mark_unread_item:
|
||||
DBWriter.markItemRead(context, selectedItem, false, true);
|
||||
if(GpodnetPreferences.loggedIn()) {
|
||||
GpodnetEpisodeAction actionNew = new GpodnetEpisodeAction.Builder(selectedItem, Action.NEW)
|
||||
.currentDeviceId()
|
||||
.currentTimestamp()
|
||||
.build();
|
||||
GpodnetPreferences.enqueueEpisodeAction(actionNew);
|
||||
}
|
||||
break;
|
||||
case R.id.add_to_queue_item:
|
||||
DBWriter.addQueueItem(context, selectedItem.getId());
|
||||
|
|
|
@ -343,6 +343,7 @@ public class PreferenceController {
|
|||
}
|
||||
});
|
||||
buildUpdateIntervalPreference();
|
||||
buildSmartMarkAsPlayedPreference();
|
||||
buildAutodownloadSelectedNetworsPreference();
|
||||
setSelectedNetworksEnabled(UserPreferences
|
||||
.isEnableAutodownloadWifiFilter());
|
||||
|
@ -403,6 +404,24 @@ public class PreferenceController {
|
|||
|
||||
}
|
||||
|
||||
private void buildSmartMarkAsPlayedPreference() {
|
||||
final Resources res = ui.getActivity().getResources();
|
||||
|
||||
ListPreference pref = (ListPreference) ui.findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS);
|
||||
String[] values = res.getStringArray(
|
||||
R.array.smart_mark_as_played_values);
|
||||
String[] entries = new String[values.length];
|
||||
for (int x = 0; x < values.length; x++) {
|
||||
if(x == 0) {
|
||||
entries[x] = res.getString(R.string.pref_smart_mark_as_played_disabled);
|
||||
} else {
|
||||
Integer v = Integer.parseInt(values[x]);
|
||||
entries[x] = v + " " + res.getString(R.string.time_unit_seconds);
|
||||
}
|
||||
}
|
||||
pref.setEntries(entries);
|
||||
}
|
||||
|
||||
private void setSelectedNetworksEnabled(boolean b) {
|
||||
if (selectedNetworks != null) {
|
||||
for (Preference p : selectedNetworks) {
|
||||
|
@ -430,7 +449,6 @@ public class PreferenceController {
|
|||
.setEnabled(UserPreferences.isEnableAutodownload());
|
||||
}
|
||||
|
||||
|
||||
private void setParallelDownloadsText(int downloads) {
|
||||
final Resources res = ui.getActivity().getResources();
|
||||
|
||||
|
|
|
@ -59,6 +59,13 @@
|
|||
android:key="prefAutoDelete"
|
||||
android:summary="@string/pref_auto_delete_sum"
|
||||
android:title="@string/pref_auto_delete_title"/>
|
||||
<ListPreference
|
||||
android:defaultValue="30"
|
||||
android:entries="@array/smart_mark_as_played_values"
|
||||
android:entryValues="@array/smart_mark_as_played_values"
|
||||
android:key="prefSmartMarkAsPlayedSecs"
|
||||
android:summary="@string/pref_smart_mark_as_played_sum"
|
||||
android:title="@string/pref_smart_mark_as_played_title"/>
|
||||
<Preference
|
||||
android:key="prefPlaybackSpeedLauncher"
|
||||
android:summary="@string/pref_playback_speed_sum"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
* <p/>
|
||||
* This method requires authentication.
|
||||
*
|
||||
* @param episodeActions Collection of episode actions.
|
||||
* @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse}
|
||||
* for details.
|
||||
* @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null.
|
||||
* @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there
|
||||
* is an authentication error.
|
||||
*/
|
||||
public GpodnetEpisodeActionPostResponse uploadEpisodeActions(Collection<GpodnetEpisodeAction> episodeActions)
|
||||
throws GpodnetServiceException {
|
||||
|
||||
Validate.notNull(episodeActions);
|
||||
|
||||
String username = GpodnetPreferences.getUsername();
|
||||
|
||||
try {
|
||||
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
|
||||
"/api/2/episodes/%s.json", username), null).toURL();
|
||||
|
||||
final JSONArray list = new JSONArray();
|
||||
for(GpodnetEpisodeAction episodeAction : episodeActions) {
|
||||
JSONObject obj = episodeAction.writeToJSONObject();
|
||||
if(obj != null) {
|
||||
list.put(obj);
|
||||
}
|
||||
}
|
||||
|
||||
RequestBody body = RequestBody.create(JSON, list.toString());
|
||||
Request.Builder request = new Request.Builder().post(body).url(url);
|
||||
|
||||
final String response = executeRequest(request);
|
||||
return GpodnetEpisodeActionPostResponse.fromJSONObject(response);
|
||||
} catch (JSONException | MalformedURLException | URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
throw new GpodnetServiceException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all subscription changes of a specific device.
|
||||
* <p/>
|
||||
* This method requires authentication.
|
||||
*
|
||||
* @param timestamp A timestamp that can be used to receive all changes since a
|
||||
* specific point in time.
|
||||
* @throws IllegalArgumentException If username or deviceId is null.
|
||||
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
|
||||
*/
|
||||
public GpodnetEpisodeActionGetResponse getEpisodeChanges(long timestamp) throws GpodnetServiceException {
|
||||
|
||||
String username = GpodnetPreferences.getUsername();
|
||||
|
||||
String params = String.format("since=%d", timestamp);
|
||||
String path = String.format("/api/2/episodes/%s.json",
|
||||
username);
|
||||
try {
|
||||
URL url = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params,
|
||||
null).toURL();
|
||||
Request.Builder request = new Request.Builder().url(url);
|
||||
|
||||
String response = executeRequest(request);
|
||||
JSONObject json = new JSONObject(response);
|
||||
return readEpisodeActionsFromJSONObject(json);
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
throw new IllegalStateException(e);
|
||||
} catch (JSONException | MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
throw new GpodnetServiceException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs in a specific user. This method must be called if any of the methods
|
||||
* that require authentication is used.
|
||||
|
@ -773,4 +855,24 @@ public class GpodnetService {
|
|||
long timestamp = object.getLong("timestamp");
|
||||
return new GpodnetSubscriptionChange(added, removed, timestamp);
|
||||
}
|
||||
|
||||
private GpodnetEpisodeActionGetResponse readEpisodeActionsFromJSONObject(
|
||||
JSONObject object) throws JSONException {
|
||||
Validate.notNull(object);
|
||||
|
||||
List<GpodnetEpisodeAction> episodeActions = new ArrayList<GpodnetEpisodeAction>();
|
||||
|
||||
long timestamp = object.getLong("timestamp");
|
||||
JSONArray jsonActions = object.getJSONArray("actions");
|
||||
for(int i=0; i < jsonActions.length(); i++) {
|
||||
JSONObject jsonAction = jsonActions.getJSONObject(i);
|
||||
GpodnetEpisodeAction episodeAction = GpodnetEpisodeAction.readFromJSONObject(jsonAction);
|
||||
if(episodeAction != null) {
|
||||
episodeActions.add(episodeAction);
|
||||
}
|
||||
}
|
||||
return new GpodnetEpisodeActionGetResponse(episodeActions, timestamp);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package de.danoeh.antennapod.core.gpoddernet.model;
|
||||
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GpodnetEpisodeActionGetResponse {
|
||||
|
||||
private final List<GpodnetEpisodeAction> episodeActions;
|
||||
private final long timestamp;
|
||||
|
||||
public GpodnetEpisodeActionGetResponse(List<GpodnetEpisodeAction> episodeActions, long timestamp) {
|
||||
Validate.notNull(episodeActions);
|
||||
this.episodeActions = episodeActions;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public List<GpodnetEpisodeAction> getEpisodeActions() {
|
||||
return this.episodeActions;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package de.danoeh.antennapod.core.gpoddernet.model;
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class GpodnetEpisodeActionPostResponse {
|
||||
|
||||
/**
|
||||
* timestamp/ID that can be used for requesting changes since this upload.
|
||||
*/
|
||||
public final long timestamp;
|
||||
|
||||
/**
|
||||
* URLs that should be updated. The key of the map is the original URL, the value of the map
|
||||
* is the sanitized URL.
|
||||
*/
|
||||
public final Map<String, String> updatedUrls;
|
||||
|
||||
public GpodnetEpisodeActionPostResponse(long timestamp, Map<String, String> updatedUrls) {
|
||||
this.timestamp = timestamp;
|
||||
this.updatedUrls = updatedUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new GpodnetUploadChangesResponse-object from a JSON object that was
|
||||
* returned by an uploadChanges call.
|
||||
*
|
||||
* @throws org.json.JSONException If the method could not parse the JSONObject.
|
||||
*/
|
||||
public static GpodnetEpisodeActionPostResponse fromJSONObject(String objectString) throws JSONException {
|
||||
final JSONObject object = new JSONObject(objectString);
|
||||
final long timestamp = object.getLong("timestamp");
|
||||
Map<String, String> updatedUrls = new HashMap<String, String>();
|
||||
JSONArray urls = object.getJSONArray("update_urls");
|
||||
for (int i = 0; i < urls.length(); i++) {
|
||||
JSONArray urlPair = urls.getJSONArray(i);
|
||||
updatedUrls.put(urlPair.getString(0), urlPair.getString(1));
|
||||
}
|
||||
return new GpodnetEpisodeActionPostResponse(timestamp, updatedUrls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,14 +4,20 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import de.danoeh.antennapod.core.BuildConfig;
|
||||
import de.danoeh.antennapod.core.ClientConfig;
|
||||
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
|
||||
import de.danoeh.antennapod.core.service.GpodnetSyncService;
|
||||
|
||||
/**
|
||||
|
@ -28,9 +34,11 @@ public class GpodnetPreferences {
|
|||
public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname";
|
||||
|
||||
|
||||
public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
|
||||
public static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
|
||||
public static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_episode_actions_sync_timestamp";
|
||||
public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added";
|
||||
public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed";
|
||||
public static final String PREF_SYNC_EPISODE_ACTIONS = "de.danoeh.antennapod.preferences.gpoddernet.sync_queued_episode_actions";
|
||||
|
||||
private static String username;
|
||||
private static String password;
|
||||
|
@ -41,10 +49,15 @@ public class GpodnetPreferences {
|
|||
private static Set<String> addedFeeds;
|
||||
private static Set<String> removedFeeds;
|
||||
|
||||
private static ReentrantLock episodeActionListLock = new ReentrantLock();
|
||||
private static List<GpodnetEpisodeAction> queuedEpisodeActions;
|
||||
|
||||
/**
|
||||
* Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
|
||||
*/
|
||||
private static long lastSyncTimestamp;
|
||||
private static long lastSubscriptionSyncTimestamp;
|
||||
|
||||
private static long lastEpisodeActionsSyncTimeStamp;
|
||||
|
||||
private static boolean preferencesLoaded = false;
|
||||
|
||||
|
@ -58,9 +71,11 @@ public class GpodnetPreferences {
|
|||
username = prefs.getString(PREF_GPODNET_USERNAME, null);
|
||||
password = prefs.getString(PREF_GPODNET_PASSWORD, null);
|
||||
deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
|
||||
lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0);
|
||||
lastSubscriptionSyncTimestamp = prefs.getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0);
|
||||
lastEpisodeActionsSyncTimeStamp = prefs.getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0);
|
||||
addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, ""));
|
||||
removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, ""));
|
||||
queuedEpisodeActions = readEpisodeActionsFromString(prefs.getString(PREF_SYNC_EPISODE_ACTIONS, ""));
|
||||
hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST));
|
||||
|
||||
preferencesLoaded = true;
|
||||
|
@ -115,14 +130,24 @@ public class GpodnetPreferences {
|
|||
writePreference(PREF_GPODNET_DEVICEID, deviceID);
|
||||
}
|
||||
|
||||
public static long getLastSyncTimestamp() {
|
||||
public static long getLastSubscriptionSyncTimestamp() {
|
||||
ensurePreferencesLoaded();
|
||||
return lastSyncTimestamp;
|
||||
return lastSubscriptionSyncTimestamp;
|
||||
}
|
||||
|
||||
public static void setLastSyncTimestamp(long lastSyncTimestamp) {
|
||||
GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp;
|
||||
writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp);
|
||||
public static void setLastSubscriptionSyncTimestamp(long timestamp) {
|
||||
GpodnetPreferences.lastSubscriptionSyncTimestamp = timestamp;
|
||||
writePreference(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, timestamp);
|
||||
}
|
||||
|
||||
public static long getLastEpisodeActionsSyncTimestamp() {
|
||||
ensurePreferencesLoaded();
|
||||
return lastEpisodeActionsSyncTimeStamp;
|
||||
}
|
||||
|
||||
public static void setLastEpisodeActionsSyncTimestamp(long timestamp) {
|
||||
GpodnetPreferences.lastEpisodeActionsSyncTimeStamp = timestamp;
|
||||
writePreference(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp);
|
||||
}
|
||||
|
||||
public static String getHostname() {
|
||||
|
@ -195,7 +220,23 @@ public class GpodnetPreferences {
|
|||
ensurePreferencesLoaded();
|
||||
removedFeeds.removeAll(removed);
|
||||
writePreference(PREF_SYNC_REMOVED, removedFeeds);
|
||||
}
|
||||
|
||||
public static void enqueueEpisodeAction(GpodnetEpisodeAction action) {
|
||||
ensurePreferencesLoaded();
|
||||
queuedEpisodeActions.add(action);
|
||||
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
|
||||
}
|
||||
|
||||
public static Collection<GpodnetEpisodeAction> getQueuedEpisodeActions() {
|
||||
ensurePreferencesLoaded();
|
||||
return Collections.unmodifiableCollection(queuedEpisodeActions);
|
||||
}
|
||||
|
||||
public static void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) {
|
||||
ensurePreferencesLoaded();
|
||||
queuedEpisodeActions.removeAll(queued);
|
||||
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -215,7 +256,9 @@ public class GpodnetPreferences {
|
|||
writePreference(PREF_SYNC_ADDED, addedFeeds);
|
||||
removedFeeds.clear();
|
||||
writePreference(PREF_SYNC_REMOVED, removedFeeds);
|
||||
setLastSyncTimestamp(0);
|
||||
queuedEpisodeActions.clear();
|
||||
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
|
||||
setLastSubscriptionSyncTimestamp(0);
|
||||
}
|
||||
|
||||
private static Set<String> readListFromString(String s) {
|
||||
|
@ -235,6 +278,29 @@ public class GpodnetPreferences {
|
|||
return result.toString().trim();
|
||||
}
|
||||
|
||||
private static List<GpodnetEpisodeAction> readEpisodeActionsFromString(String s) {
|
||||
String[] lines = s.split("\n");
|
||||
List<GpodnetEpisodeAction> result = new ArrayList<GpodnetEpisodeAction>(lines.length);
|
||||
for(String line : lines) {
|
||||
if(StringUtils.isNotBlank(line)) {
|
||||
GpodnetEpisodeAction action = GpodnetEpisodeAction.readFromString(line);
|
||||
if(action != null) {
|
||||
result.add(GpodnetEpisodeAction.readFromString(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String writeEpisodeActionsToString(Collection<GpodnetEpisodeAction> c) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for(GpodnetEpisodeAction item : c) {
|
||||
result.append(item.writeToString());
|
||||
result.append("\n");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static String checkGpodnetHostname(String value) {
|
||||
int startIndex = 0;
|
||||
if (value.startsWith("http://")) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -9,24 +9,31 @@ import android.content.Intent;
|
|||
import android.os.IBinder;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
|
||||
import de.danoeh.antennapod.core.BuildConfig;
|
||||
import de.danoeh.antennapod.core.ClientConfig;
|
||||
import de.danoeh.antennapod.core.R;
|
||||
import de.danoeh.antennapod.core.feed.Feed;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
|
||||
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException;
|
||||
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse;
|
||||
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.storage.DownloadRequestException;
|
||||
import de.danoeh.antennapod.core.storage.DownloadRequester;
|
||||
import de.danoeh.antennapod.core.util.NetworkUtils;
|
||||
|
@ -50,7 +57,7 @@ public class GpodnetSyncService extends Service {
|
|||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null;
|
||||
if (action != null && action.equals(ACTION_SYNC)) {
|
||||
Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
|
||||
Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
|
||||
syncWaiterThread.restart();
|
||||
} else {
|
||||
Log.e(TAG, "Received invalid intent: action argument is null or invalid");
|
||||
|
@ -61,9 +68,8 @@ public class GpodnetSyncService extends Service {
|
|||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy");
|
||||
Log.d(TAG, "onDestroy");
|
||||
syncWaiterThread.interrupt();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -79,64 +85,92 @@ public class GpodnetSyncService extends Service {
|
|||
return service;
|
||||
}
|
||||
|
||||
private synchronized void syncChanges() {
|
||||
if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) {
|
||||
final long timestamp = GpodnetPreferences.getLastSyncTimestamp();
|
||||
try {
|
||||
final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this);
|
||||
GpodnetService service = tryLogin();
|
||||
|
||||
if (timestamp == 0) {
|
||||
// first sync: download all subscriptions...
|
||||
GpodnetSubscriptionChange changes =
|
||||
service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0);
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Downloaded subscription changes: " + changes);
|
||||
processSubscriptionChanges(localSubscriptions, changes);
|
||||
|
||||
// ... then upload all local subscriptions
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Uploading subscription list: " + localSubscriptions);
|
||||
GpodnetUploadChangesResponse uploadChangesResponse =
|
||||
service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>());
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Uploading changes response: " + uploadChangesResponse);
|
||||
GpodnetPreferences.removeAddedFeeds(localSubscriptions);
|
||||
GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy());
|
||||
GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
|
||||
} else {
|
||||
Set<String> added = GpodnetPreferences.getAddedFeedsCopy();
|
||||
Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy();
|
||||
|
||||
// download remote changes first...
|
||||
GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp);
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
|
||||
processSubscriptionChanges(localSubscriptions, subscriptionChanges);
|
||||
|
||||
// ... then upload changes local changes
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
|
||||
added.toString(), removed));
|
||||
GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed);
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse);
|
||||
|
||||
GpodnetPreferences.removeAddedFeeds(added);
|
||||
GpodnetPreferences.removeRemovedFeeds(removed);
|
||||
GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp);
|
||||
}
|
||||
clearErrorNotifications();
|
||||
} catch (GpodnetServiceException e) {
|
||||
e.printStackTrace();
|
||||
updateErrorNotification(e);
|
||||
} catch (DownloadRequestException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
private synchronized void sync() {
|
||||
if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable(this) == false) {
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
syncSubscriptionChanges();
|
||||
syncEpisodeActions();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private synchronized void syncSubscriptionChanges() {
|
||||
final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp();
|
||||
try {
|
||||
final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this);
|
||||
GpodnetService service = tryLogin();
|
||||
|
||||
// first sync: download all subscriptions...
|
||||
GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(),
|
||||
GpodnetPreferences.getDeviceID(), timestamp);
|
||||
long lastUpdate = subscriptionChanges.getTimestamp();
|
||||
|
||||
Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
|
||||
processSubscriptionChanges(localSubscriptions, subscriptionChanges);
|
||||
|
||||
Collection<String> added;
|
||||
Collection<String> removed;
|
||||
if (timestamp == 0) {
|
||||
added = localSubscriptions;
|
||||
GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy());
|
||||
removed = Collections.emptyList();
|
||||
} else {
|
||||
added = GpodnetPreferences.getAddedFeedsCopy();
|
||||
removed = GpodnetPreferences.getRemovedFeedsCopy();
|
||||
}
|
||||
if(added.size() > 0 || removed.size() > 0) {
|
||||
Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
|
||||
added, removed));
|
||||
GpodnetUploadChangesResponse uploadResponse = service.uploadChanges(GpodnetPreferences.getUsername(),
|
||||
GpodnetPreferences.getDeviceID(), added, removed);
|
||||
lastUpdate = uploadResponse.timestamp;
|
||||
Log.d(TAG, "Upload changes response: " + uploadResponse);
|
||||
GpodnetPreferences.removeAddedFeeds(added);
|
||||
GpodnetPreferences.removeRemovedFeeds(removed);
|
||||
}
|
||||
GpodnetPreferences.setLastSubscriptionSyncTimestamp(lastUpdate);
|
||||
clearErrorNotifications();
|
||||
} catch (GpodnetServiceException e) {
|
||||
e.printStackTrace();
|
||||
updateErrorNotification(e);
|
||||
} catch (DownloadRequestException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void syncEpisodeActions() {
|
||||
final long timestamp = GpodnetPreferences.getLastEpisodeActionsSyncTimestamp();
|
||||
Log.d(TAG, "last episode actions sync timestamp: " + timestamp);
|
||||
try {
|
||||
GpodnetService service = tryLogin();
|
||||
|
||||
// download episode actions
|
||||
GpodnetEpisodeActionGetResponse getResponse = service.getEpisodeChanges(timestamp);
|
||||
long lastUpdate = getResponse.getTimestamp();
|
||||
Log.d(TAG, "Downloaded episode actions: " + getResponse);
|
||||
processEpisodeActions(getResponse.getEpisodeActions());
|
||||
|
||||
// upload local
|
||||
Collection<GpodnetEpisodeAction> episodeActions = GpodnetPreferences.getQueuedEpisodeActions();
|
||||
if(episodeActions.size() > 0) {
|
||||
Log.d(TAG, "Uploading episode actions: " + episodeActions);
|
||||
GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(episodeActions);
|
||||
lastUpdate = postResponse.timestamp;
|
||||
Log.d(TAG, "Upload episode response: " + postResponse);
|
||||
GpodnetPreferences.removeQueuedEpisodeActions(episodeActions);
|
||||
}
|
||||
GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(lastUpdate);
|
||||
clearErrorNotifications();
|
||||
} catch (GpodnetServiceException e) {
|
||||
e.printStackTrace();
|
||||
updateErrorNotification(e);
|
||||
} catch (DownloadRequestException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException {
|
||||
for (String downloadUrl : changes.getAdded()) {
|
||||
if (!localSubscriptions.contains(downloadUrl)) {
|
||||
|
@ -149,6 +183,52 @@ public class GpodnetSyncService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> episodeActions) throws DownloadRequestException {
|
||||
if(episodeActions.size() == 0) {
|
||||
return;
|
||||
}
|
||||
Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>();
|
||||
for (GpodnetEpisodeAction episodeAction : episodeActions) {
|
||||
switch (episodeAction.getAction()) {
|
||||
case NEW:
|
||||
FeedItem newItem = DBReader.getFeedItem(this, episodeAction.getPodcast(), episodeAction.getEpisode());
|
||||
if(newItem != null) {
|
||||
DBWriter.markItemRead(this, newItem, false, true);
|
||||
} else {
|
||||
Log.i(TAG, "Unknown feed item: " + episodeAction);
|
||||
}
|
||||
break;
|
||||
case DOWNLOAD:
|
||||
break;
|
||||
case PLAY:
|
||||
if(episodeAction.getTimestamp() == null) {
|
||||
break;
|
||||
}
|
||||
Pair key = new Pair(episodeAction.getPodcast(), episodeAction.getEpisode());
|
||||
GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key);
|
||||
if (mostRecent == null) {
|
||||
mostRecentPlayAction.put(key, episodeAction);
|
||||
} else if (mostRecent.getTimestamp().before(episodeAction.getTimestamp())) {
|
||||
mostRecentPlayAction.put(key, episodeAction);
|
||||
}
|
||||
break;
|
||||
case DELETE:
|
||||
// NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (GpodnetEpisodeAction episodeAction : mostRecentPlayAction.values()) {
|
||||
FeedItem playItem = DBReader.getFeedItem(this, episodeAction.getPodcast(), episodeAction.getEpisode());
|
||||
if (playItem != null) {
|
||||
playItem.getMedia().setPosition(episodeAction.getPosition() * 1000);
|
||||
if(playItem.getMedia().hasAlmostEnded()) {
|
||||
DBWriter.markItemRead(this, playItem, true, true);
|
||||
DBWriter.addItemToPlaybackHistory(this, playItem.getMedia());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearErrorNotifications() {
|
||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
nm.cancel(R.id.notification_gpodnet_sync_error);
|
||||
|
@ -156,7 +236,7 @@ public class GpodnetSyncService extends Service {
|
|||
}
|
||||
|
||||
private void updateErrorNotification(GpodnetServiceException exception) {
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification");
|
||||
Log.d(TAG, "Posting error notification");
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
|
||||
final String title;
|
||||
|
@ -186,7 +266,7 @@ public class GpodnetSyncService extends Service {
|
|||
private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) {
|
||||
@Override
|
||||
public void onWaitCompleted() {
|
||||
syncChanges();
|
||||
sync();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -209,7 +289,7 @@ public class GpodnetSyncService extends Service {
|
|||
|
||||
private void reinit() {
|
||||
if (thread != null && thread.isAlive()) {
|
||||
Log.d(TAG, "Interrupting waiter thread");
|
||||
Log.d(TAG, "Interrupting waiter thread");
|
||||
thread.interrupt();
|
||||
}
|
||||
thread = new Thread() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -43,6 +43,9 @@ import de.danoeh.antennapod.core.feed.Chapter;
|
|||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.feed.MediaType;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action;
|
||||
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
|
||||
|
@ -167,6 +170,8 @@ public class PlaybackService extends Service {
|
|||
private PlaybackServiceMediaPlayer mediaPlayer;
|
||||
private PlaybackServiceTaskManager taskManager;
|
||||
|
||||
private int startPosition;
|
||||
|
||||
private static volatile MediaType currentMediaType = MediaType.UNKNOWN;
|
||||
|
||||
private final IBinder mBinder = new LocalBinder();
|
||||
|
@ -445,6 +450,37 @@ public class PlaybackService extends Service {
|
|||
}
|
||||
writePlayerStatusPlaybackPreferences();
|
||||
|
||||
final Playable playable = mediaPlayer.getPSMPInfo().playable;
|
||||
|
||||
// Gpodder: send play action
|
||||
if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) {
|
||||
FeedMedia media = (FeedMedia) playable;
|
||||
FeedItem item = media.getItem();
|
||||
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
|
||||
.currentDeviceId()
|
||||
.currentTimestamp()
|
||||
.started(startPosition / 1000)
|
||||
.position(getCurrentPosition() / 1000)
|
||||
.total(getDuration() / 1000)
|
||||
.build();
|
||||
GpodnetPreferences.enqueueEpisodeAction(action);
|
||||
}
|
||||
|
||||
// if episode is near end [outro playing]:
|
||||
// mark as read, remove from queue, add to playlist history
|
||||
// auto delete: see {@link de.danoeh.antennapod.activity.MediaPlayerActivity#onStop()}
|
||||
if (playable instanceof FeedMedia) {
|
||||
FeedMedia media = (FeedMedia) playable;
|
||||
if(media.hasAlmostEnded()) {
|
||||
FeedItem item = media.getItem();
|
||||
Log.d(TAG, "smart mark as read");
|
||||
DBWriter.markItemRead(PlaybackService.this, item, true, false);
|
||||
DBWriter.removeQueueItem(PlaybackService.this, item.getId(), false);
|
||||
DBWriter.addItemToPlaybackHistory(PlaybackService.this, media);
|
||||
// episode should already be flattered, no action required
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case STOPPED:
|
||||
|
@ -463,6 +499,7 @@ public class PlaybackService extends Service {
|
|||
writePlayerStatusPlaybackPreferences();
|
||||
setupNotification(newInfo);
|
||||
started = true;
|
||||
startPosition = mediaPlayer.getPosition();
|
||||
break;
|
||||
|
||||
case ERROR:
|
||||
|
@ -540,8 +577,8 @@ public class PlaybackService extends Service {
|
|||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Playback ended");
|
||||
|
||||
final Playable media = mediaPlayer.getPSMPInfo().playable;
|
||||
if (media == null) {
|
||||
final Playable playable = mediaPlayer.getPSMPInfo().playable;
|
||||
if (playable == null) {
|
||||
Log.e(TAG, "Cannot end playback: media was null");
|
||||
return;
|
||||
}
|
||||
|
@ -551,13 +588,14 @@ public class PlaybackService extends Service {
|
|||
boolean isInQueue = false;
|
||||
FeedItem nextItem = null;
|
||||
|
||||
if (media instanceof FeedMedia) {
|
||||
FeedItem item = ((FeedMedia) media).getItem();
|
||||
if (playable instanceof FeedMedia) {
|
||||
FeedMedia media = (FeedMedia) playable;
|
||||
FeedItem item = media.getItem();
|
||||
DBWriter.markItemRead(PlaybackService.this, item, true, true);
|
||||
|
||||
try {
|
||||
final List<FeedItem> queue = taskManager.getQueue();
|
||||
isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
|
||||
isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId());
|
||||
nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
|
@ -566,21 +604,30 @@ public class PlaybackService extends Service {
|
|||
if (isInQueue) {
|
||||
DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true);
|
||||
}
|
||||
DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
|
||||
DBWriter.addItemToPlaybackHistory(PlaybackService.this, media);
|
||||
|
||||
// auto-flattr if enabled
|
||||
if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) {
|
||||
DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item);
|
||||
}
|
||||
|
||||
//Delete episode if enabled
|
||||
// Delete episode if enabled
|
||||
if(UserPreferences.isAutoDelete()) {
|
||||
DBWriter.deleteFeedMediaOfItem(PlaybackService.this, item.getMedia().getId());
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Episode Deleted");
|
||||
DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId());
|
||||
Log.d(TAG, "Episode Deleted");
|
||||
}
|
||||
|
||||
// gpodder play action
|
||||
if(GpodnetPreferences.loggedIn()) {
|
||||
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
|
||||
.currentDeviceId()
|
||||
.currentTimestamp()
|
||||
.started(startPosition / 1000)
|
||||
.position(getDuration() / 1000)
|
||||
.total(getDuration() / 1000)
|
||||
.build();
|
||||
GpodnetPreferences.enqueueEpisodeAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Load next episode if previous episode was in the queue and if there
|
||||
|
@ -605,12 +652,10 @@ public class PlaybackService extends Service {
|
|||
final boolean stream;
|
||||
|
||||
if (playNextEpisode) {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Playback of next episode will start immediately.");
|
||||
Log.d(TAG, "Playback of next episode will start immediately.");
|
||||
prepareImmediately = startWhenPrepared = true;
|
||||
} else {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "No more episodes available to play");
|
||||
Log.d(TAG, "No more episodes available to play");
|
||||
|
||||
prepareImmediately = startWhenPrepared = false;
|
||||
stopForeground(true);
|
||||
|
@ -619,7 +664,7 @@ public class PlaybackService extends Service {
|
|||
|
||||
writePlaybackPreferencesNoMediaPlaying();
|
||||
if (nextMedia != null) {
|
||||
stream = !media.localFileAvailable();
|
||||
stream = !playable.localFileAvailable();
|
||||
mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
|
||||
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
|
||||
(nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
|
||||
|
@ -631,8 +676,7 @@ public class PlaybackService extends Service {
|
|||
}
|
||||
|
||||
public void setSleepTimer(long waitingTime) {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
|
||||
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
|
||||
+ " milliseconds");
|
||||
taskManager.setSleepTimer(waitingTime);
|
||||
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
|
||||
|
@ -675,8 +719,7 @@ public class PlaybackService extends Service {
|
|||
}
|
||||
|
||||
private void writePlaybackPreferences() {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Writing playback preferences");
|
||||
Log.d(TAG, "Writing playback preferences");
|
||||
|
||||
SharedPreferences.Editor editor = PreferenceManager
|
||||
.getDefaultSharedPreferences(getApplicationContext()).edit();
|
||||
|
@ -918,15 +961,15 @@ public class PlaybackService extends Service {
|
|||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Saving current position to " + position);
|
||||
if (updatePlayedDuration && playable instanceof FeedMedia) {
|
||||
FeedMedia m = (FeedMedia) playable;
|
||||
FeedItem item = m.getItem();
|
||||
m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed)));
|
||||
FeedMedia media = (FeedMedia) playable;
|
||||
FeedItem item = media.getItem();
|
||||
media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed)));
|
||||
// Auto flattr
|
||||
if (isAutoFlattrable(m) &&
|
||||
(m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) {
|
||||
if (isAutoFlattrable(media) &&
|
||||
(media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) {
|
||||
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration())
|
||||
Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration())
|
||||
+ " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration));
|
||||
DBTasks.flattrItemIfLoggedIn(this, item);
|
||||
}
|
||||
|
@ -1231,7 +1274,26 @@ public class PlaybackService extends Service {
|
|||
|
||||
|
||||
public void seekTo(final int t) {
|
||||
if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING
|
||||
&& GpodnetPreferences.loggedIn()) {
|
||||
final Playable playable = mediaPlayer.getPSMPInfo().playable;
|
||||
if (playable instanceof FeedMedia) {
|
||||
FeedMedia media = (FeedMedia) playable;
|
||||
FeedItem item = media.getItem();
|
||||
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
|
||||
.currentDeviceId()
|
||||
.currentTimestamp()
|
||||
.started(startPosition / 1000)
|
||||
.position(getCurrentPosition() / 1000)
|
||||
.total(getDuration() / 1000)
|
||||
.build();
|
||||
GpodnetPreferences.enqueueEpisodeAction(action);
|
||||
}
|
||||
}
|
||||
mediaPlayer.seekTo(t);
|
||||
if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) {
|
||||
startPosition = t;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1270,10 +1332,9 @@ public class PlaybackService extends Service {
|
|||
return mediaPlayer.getVideoSize();
|
||||
}
|
||||
|
||||
private boolean isAutoFlattrable(Playable p) {
|
||||
if (p != null && p instanceof FeedMedia) {
|
||||
FeedMedia media = (FeedMedia) p;
|
||||
FeedItem item = ((FeedMedia) p).getItem();
|
||||
private boolean isAutoFlattrable(FeedMedia media) {
|
||||
if (media != null) {
|
||||
FeedItem item = media.getItem();
|
||||
return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred();
|
||||
} else {
|
||||
return false;
|
||||
|
|
|
@ -2,7 +2,6 @@ package de.danoeh.antennapod.core.storage;
|
|||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.SQLException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -680,6 +679,42 @@ public final class DBReader {
|
|||
|
||||
}
|
||||
|
||||
static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) {
|
||||
Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
|
||||
FeedItem item = null;
|
||||
Cursor itemCursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl);
|
||||
if (itemCursor.moveToFirst()) {
|
||||
List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor);
|
||||
if (list.size() > 0) {
|
||||
item = list.get(0);
|
||||
loadFeedDataOfFeedItemlist(context, list);
|
||||
if (item.hasChapters()) {
|
||||
loadChaptersOfFeedItem(adapter, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific FeedItem from the database.
|
||||
*
|
||||
* @param context A context that is used for opening a database connection.
|
||||
* @param podcastUrl the corresponding feed's url
|
||||
* @param episodeUrl the feed item's url
|
||||
* @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes
|
||||
* as well as chapter marks of the FeedItem will also be loaded from the database.
|
||||
*/
|
||||
public static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl) {
|
||||
Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
|
||||
|
||||
PodDBAdapter adapter = new PodDBAdapter(context);
|
||||
adapter.open();
|
||||
FeedItem item = getFeedItem(context, podcastUrl, episodeUrl, adapter);
|
||||
adapter.close();
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads additional information about a FeedItem, e.g. shownotes
|
||||
*
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -1,194 +0,0 @@
|
|||
package de.danoeh.antennapod.core.syndication.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.danoeh.antennapod.core.BuildConfig;
|
||||
|
||||
/**
|
||||
* Parses several date formats.
|
||||
*/
|
||||
public class SyndDateUtils {
|
||||
private static final String TAG = "DateUtils";
|
||||
|
||||
private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z",
|
||||
"dd MMM yy HH:mm Z"};
|
||||
|
||||
/**
|
||||
* RFC 3339 date format for UTC dates.
|
||||
*/
|
||||
public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
||||
|
||||
/**
|
||||
* RFC 3339 date format for localtime dates with offset.
|
||||
*/
|
||||
public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ";
|
||||
|
||||
public static final String ISO8601_SHORT = "yyyy-MM-dd";
|
||||
|
||||
private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() {
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue() {
|
||||
return new SimpleDateFormat(RFC822DATES[0], Locale.US);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue() {
|
||||
return new SimpleDateFormat(RFC3339UTC, Locale.US);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static ThreadLocal<SimpleDateFormat> ISO8601ShortFormatter = new ThreadLocal<SimpleDateFormat>() {
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue() {
|
||||
return new SimpleDateFormat(ISO8601_SHORT, Locale.US);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public static Date parseRFC822Date(String date) {
|
||||
Date result = null;
|
||||
if (date.contains("PDT")) {
|
||||
date = date.replace("PDT", "PST8PDT");
|
||||
}
|
||||
if (date.contains(",")) {
|
||||
// Remove day of the week
|
||||
date = date.substring(date.indexOf(",") + 1).trim();
|
||||
}
|
||||
SimpleDateFormat format = RFC822Formatter.get();
|
||||
|
||||
for (String RFC822DATE : RFC822DATES) {
|
||||
try {
|
||||
format.applyPattern(RFC822DATE);
|
||||
result = format.parse(date);
|
||||
break;
|
||||
} catch (ParseException e) {
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, "ParserException", e);
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
Log.e(TAG, "Unable to parse feed date correctly:" + date);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Date parseRFC3339Date(String date) {
|
||||
Date result = null;
|
||||
SimpleDateFormat format = RFC3339Formatter.get();
|
||||
boolean isLocal = date.endsWith("Z");
|
||||
if (date.contains(".")) {
|
||||
// remove secfrac
|
||||
int fracIndex = date.indexOf(".");
|
||||
String first = date.substring(0, fracIndex);
|
||||
String second = null;
|
||||
if (isLocal) {
|
||||
second = date.substring(date.length() - 1);
|
||||
} else {
|
||||
if (date.contains("+")) {
|
||||
second = date.substring(date.indexOf("+"));
|
||||
} else {
|
||||
second = date.substring(date.indexOf("-"));
|
||||
}
|
||||
}
|
||||
|
||||
date = first + second;
|
||||
}
|
||||
if (isLocal) {
|
||||
try {
|
||||
result = format.parse(date);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
format.applyPattern(RFC3339LOCAL);
|
||||
// remove last colon
|
||||
StringBuffer buf = new StringBuffer(date.length() - 1);
|
||||
int colonIdx = date.lastIndexOf(':');
|
||||
for (int x = 0; x < date.length(); x++) {
|
||||
if (x != colonIdx)
|
||||
buf.append(date.charAt(x));
|
||||
}
|
||||
String bufStr = buf.toString();
|
||||
try {
|
||||
result = format.parse(bufStr);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to parse date");
|
||||
} finally {
|
||||
format.applyPattern(RFC3339UTC);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
public static Date parseISO8601Date(String date) {
|
||||
if(date.length() > ISO8601_SHORT.length()) {
|
||||
return parseRFC3339Date(date);
|
||||
}
|
||||
Date result = null;
|
||||
if(date.length() == "YYYYMMDD".length()) {
|
||||
date = date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6,8);
|
||||
}
|
||||
SimpleDateFormat format = ISO8601ShortFormatter.get();
|
||||
try {
|
||||
result = format.parse(date);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string of the form [HH:]MM:SS[.mmm] and converts it to
|
||||
* milliseconds.
|
||||
*
|
||||
* @throws java.lang.NumberFormatException if the number segments contain invalid numbers.
|
||||
*/
|
||||
public static long parseTimeString(final String time) {
|
||||
String[] parts = time.split(":");
|
||||
long result = 0;
|
||||
int idx = 0;
|
||||
if (parts.length == 3) {
|
||||
// string has hours
|
||||
result += Integer.valueOf(parts[idx]) * 3600000L;
|
||||
idx++;
|
||||
}
|
||||
if (parts.length >= 2) {
|
||||
result += Integer.valueOf(parts[idx]) * 60000L;
|
||||
idx++;
|
||||
result += (Float.valueOf(parts[idx])) * 1000L;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String formatRFC822Date(Date date) {
|
||||
SimpleDateFormat format = RFC822Formatter.get();
|
||||
return format.format(date);
|
||||
}
|
||||
|
||||
public static String formatRFC3339Local(Date date) {
|
||||
SimpleDateFormat format = RFC3339Formatter.get();
|
||||
format.applyPattern(RFC3339LOCAL);
|
||||
String result = format.format(date);
|
||||
format.applyPattern(RFC3339UTC);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String formatRFC3339UTC(Date date) {
|
||||
SimpleDateFormat format = RFC3339Formatter.get();
|
||||
format.applyPattern(RFC3339UTC);
|
||||
return format.format(date);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package de.danoeh.antennapod.core.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Parses several date formats.
|
||||
*/
|
||||
public class DateUtils {
|
||||
private static final String TAG = "DateUtils";
|
||||
|
||||
private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z",
|
||||
"dd MMM yy HH:mm Z"};
|
||||
|
||||
/**
|
||||
* RFC 3339 date format for UTC dates.
|
||||
*/
|
||||
public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
||||
|
||||
/**
|
||||
* RFC 3339 date format for localtime dates with offset.
|
||||
*/
|
||||
public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ";
|
||||
|
||||
public static final String ISO8601_SHORT = "yyyy-MM-dd";
|
||||
|
||||
private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() {
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue() {
|
||||
return new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue() {
|
||||
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public static Date parse(String date) {
|
||||
if(date == null) {
|
||||
throw new IllegalArgumentException("Date most not be null");
|
||||
}
|
||||
date = date.replace('/', ' ');
|
||||
date = date.replace('-', ' ');
|
||||
if(date.contains(".")) {
|
||||
int start = date.indexOf('.');
|
||||
int current = start+1;
|
||||
while(current < date.length() && Character.isDigit(date.charAt(current))) {
|
||||
current++;
|
||||
}
|
||||
if(current - start > 4) {
|
||||
if(current < date.length()-1) {
|
||||
date = date.substring(0, start + 4) + date.substring(current);
|
||||
} else {
|
||||
date = date.substring(0, start + 4);
|
||||
}
|
||||
} else if(current - start < 4) {
|
||||
if(current < date.length()-1) {
|
||||
date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)) + date.substring(current);
|
||||
} else {
|
||||
date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
String[] patterns = {
|
||||
"dd MMM yy HH:mm:ss Z",
|
||||
"dd MMM yy HH:mm Z",
|
||||
"EEE, dd MMM yyyy HH:mm:ss Z",
|
||||
"EEEE, dd MMM yy HH:mm:ss Z",
|
||||
"EEE MMM d HH:mm:ss yyyy",
|
||||
"yyyy MM dd'T'HH:mm:ss",
|
||||
"yyyy MM dd'T'HH:mm:ss.SSS",
|
||||
"yyyy MM dd'T'HH:mm:ss.SSS Z",
|
||||
"yyyy MM dd'T'HH:mm:ssZ",
|
||||
"yyyy MM dd'T'HH:mm:ss'Z'",
|
||||
"yyyy MM ddZ",
|
||||
"yyyy MM dd"
|
||||
};
|
||||
SimpleDateFormat parser = new SimpleDateFormat("", Locale.US);
|
||||
parser.setLenient(false);
|
||||
ParsePosition pos = new ParsePosition(0);
|
||||
for(String pattern : patterns) {
|
||||
parser.applyPattern(pattern);
|
||||
pos.setIndex(0);
|
||||
Date result = parser.parse(date, pos);
|
||||
if(result != null && pos.getIndex() == date.length()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes a string of the form [HH:]MM:SS[.mmm] and converts it to
|
||||
* milliseconds.
|
||||
*
|
||||
* @throws java.lang.NumberFormatException if the number segments contain invalid numbers.
|
||||
*/
|
||||
public static long parseTimeString(final String time) {
|
||||
String[] parts = time.split(":");
|
||||
long result = 0;
|
||||
int idx = 0;
|
||||
if (parts.length == 3) {
|
||||
// string has hours
|
||||
result += Integer.valueOf(parts[idx]) * 3600000L;
|
||||
idx++;
|
||||
}
|
||||
if (parts.length >= 2) {
|
||||
result += Integer.valueOf(parts[idx]) * 60000L;
|
||||
idx++;
|
||||
result += (Float.valueOf(parts[idx])) * 1000L;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String formatRFC822Date(Date date) {
|
||||
SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
|
||||
return format.format(date);
|
||||
}
|
||||
|
||||
public static String formatRFC3339Local(Date date) {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
|
||||
return format.format(date);
|
||||
}
|
||||
|
||||
public static String formatRFC3339UTC(Date date) {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
return format.format(date);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string-array name="smart_mark_as_played_values">
|
||||
<item>off</item>
|
||||
<item>15</item>
|
||||
<item>30</item>
|
||||
<item>45</item>
|
||||
<item>60</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="seek_delta_values">
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
|
|
|
@ -98,9 +98,9 @@
|
|||
<string name="stream_label">Stream</string>
|
||||
<string name="remove_label">Remove</string>
|
||||
<string name="remove_episode_lable">Remove episode</string>
|
||||
<string name="mark_read_label">Mark as read</string>
|
||||
<string name="mark_unread_label">Mark as unread</string>
|
||||
<string name="marked_as_read_label">Marked as read</string>
|
||||
<string name="mark_read_label">Mark as played</string>
|
||||
<string name="mark_unread_label">Mark as unplayed</string>
|
||||
<string name="marked_as_read_label">Marked as played</string>
|
||||
<string name="add_to_queue_label">Add to Queue</string>
|
||||
<string name="remove_from_queue_label">Remove from Queue</string>
|
||||
<string name="visit_website_label">Visit Website</string>
|
||||
|
@ -220,6 +220,8 @@
|
|||
<string name="pref_followQueue_sum">Jump to next queue item when playback completes</string>
|
||||
<string name="pref_auto_delete_sum">Delete episode when playback completes</string>
|
||||
<string name="pref_auto_delete_title">Auto Delete</string>
|
||||
<string name="pref_smart_mark_as_played_sum">Mark episodes as played even if less than a certain amount of seconds of playing time is still left</string>
|
||||
<string name="pref_smart_mark_as_played_title">Smart mark as played</string>
|
||||
<string name="playback_pref">Playback</string>
|
||||
<string name="network_pref">Network</string>
|
||||
<string name="pref_autoUpdateIntervall_title">Update interval</string>
|
||||
|
@ -277,6 +279,8 @@
|
|||
<string name="pref_expand_notify_unsupport_toast">Android versions before 4.1 do not support expanded notifications.</string>
|
||||
<string name="pref_queueAddToFront_sum">Add new episodes to the front of the queue.</string>
|
||||
<string name="pref_queueAddToFront_title">Enqueue at front.</string>
|
||||
<string name="pref_smart_mark_as_played_disabled">Disabled</string>
|
||||
|
||||
|
||||
<!-- Auto-Flattr dialog -->
|
||||
<string name="auto_flattr_enable">Enable automatic flattring</string>
|
||||
|
|
Loading…
Reference in New Issue