Uncoupled AntennaPod from specific GpodnetSyncService

This commit is contained in:
ByteHamster 2020-03-27 18:35:25 +01:00
parent 3c8fb2e296
commit 2b8c3ff04e
47 changed files with 789 additions and 1219 deletions

View File

@ -5,10 +5,11 @@ import java.util.Arrays;
import java.util.List;
import androidx.test.runner.AndroidJUnit4;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@ -30,7 +31,7 @@ public class GPodnetServiceTest {
@Before
public void setUp() {
service = new GpodnetService();
service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetService.DEFAULT_BASE_HOST);
}
private void authenticate() throws GpodnetServiceException {
@ -42,7 +43,7 @@ public class GPodnetServiceTest {
authenticate();
ArrayList<String> l = new ArrayList<>();
l.add("http://bitsundso.de/feed");
service.uploadSubscriptions(USER, "radio", l);
service.uploadSubscriptions("radio", l);
}
@Test
@ -51,7 +52,7 @@ public class GPodnetServiceTest {
ArrayList<String> l = new ArrayList<>();
l.add("http://bitsundso.de/feed");
l.add("http://gamesundso.de/feed");
service.uploadSubscriptions(USER, "radio", l);
service.uploadSubscriptions( "radio", l);
}
@Test
@ -61,41 +62,40 @@ public class GPodnetServiceTest {
List<String> subscriptions = Arrays.asList(URLS[0], URLS[1]);
List<String> removed = singletonList(URLS[0]);
List<String> added = Arrays.asList(URLS[2], URLS[3]);
service.uploadSubscriptions(USER, "radio", subscriptions);
service.uploadChanges(USER, "radio", added, removed);
service.uploadSubscriptions("radio", subscriptions);
service.uploadChanges("radio", added, removed);
}
@Test
public void testGetSubscriptionChanges() throws GpodnetServiceException {
authenticate();
service.getSubscriptionChanges(USER, "radio", 1362322610L);
service.getSubscriptionChanges("radio", 1362322610L);
}
@Test
public void testGetSubscriptionsOfUser()
throws GpodnetServiceException {
authenticate();
service.getSubscriptionsOfUser(USER);
service.getSubscriptionsOfUser();
}
@Test
public void testGetSubscriptionsOfDevice()
throws GpodnetServiceException {
authenticate();
service.getSubscriptionsOfDevice(USER, "radio");
service.getSubscriptionsOfDevice("radio");
}
@Test
public void testConfigureDevices() throws GpodnetServiceException {
authenticate();
service.configureDevice(USER, "foo", "This is an updated caption",
GpodnetDevice.DeviceType.LAPTOP);
service.configureDevice("foo", "This is an updated caption", GpodnetDevice.DeviceType.LAPTOP);
}
@Test
public void testGetDevices() throws GpodnetServiceException {
authenticate();
service.getDevices(USER);
service.getDevices();
}
@Test

View File

@ -30,12 +30,13 @@ import java.util.regex.Pattern;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice;
/**
* Guides the user through the authentication process
@ -69,7 +70,7 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.gpodnetauth_activity);
service = new GpodnetService();
service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname());
viewFlipper = findViewById(R.id.viewflipper);
LayoutInflater inflater = (LayoutInflater)
@ -85,14 +86,6 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity {
advance();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (service != null) {
service.shutdown();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -221,7 +214,7 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity {
@Override
protected List<GpodnetDevice> doInBackground(GpodnetService... params) {
try {
return params[0].getDevices(username);
return params[0].getDevices();
} catch (GpodnetServiceException e) {
e.printStackTrace();
return null;
@ -268,7 +261,7 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity {
@Override
protected GpodnetDevice doInBackground(GpodnetService... params) {
try {
params[0].configureDevice(username, deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE);
params[0].configureDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE);
return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0);
} catch (GpodnetServiceException e) {
e.printStackTrace();
@ -349,7 +342,7 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity {
final Button back = view.findViewById(R.id.butGoMainscreen);
sync.setOnClickListener(v -> {
GpodnetSyncService.sendSyncIntent(GpodnetAuthenticationActivity.this);
SyncService.sync(GpodnetAuthenticationActivity.this);
finish();
});
back.setOnClickListener(v -> {

View File

@ -13,13 +13,14 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import java.util.List;
/**
* Adapter for displaying a list of GPodnetPodcast-Objects.

View File

@ -7,10 +7,10 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag;
import java.util.List;
/**
* Adapter for displaying a list of GPodnetPodcast-Objects.

View File

@ -14,7 +14,6 @@ class ClientConfigurator {
ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME;
ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl();
ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl();
ClientConfig.gpodnetCallbacks = new GpodnetCallbacksImpl();
ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl();
ClientConfig.dbTasksCallbacks = new DBTasksCallbacksImpl();
ClientConfig.castCallbacks = new CastCallbackImpl();

View File

@ -1,22 +0,0 @@
package de.danoeh.antennapod.config;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.GpodnetCallbacks;
public class GpodnetCallbacksImpl implements GpodnetCallbacks {
@Override
public boolean gpodnetEnabled() {
return true;
}
@Override
public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context) {
return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT);
}
}

View File

@ -10,8 +10,8 @@ import android.widget.EditText;
import android.widget.LinearLayout;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
/**
* Creates a dialog that lets the user change the hostname for the gpodder.net service.

View File

@ -1,8 +1,10 @@
package de.danoeh.antennapod.discovery;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import io.reactivex.Single;
import io.reactivex.SingleOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -14,9 +16,9 @@ import java.util.List;
public class GpodnetPodcastSearcher implements PodcastSearcher {
public Single<List<PodcastSearchResult>> search(String query) {
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
GpodnetService service = null;
try {
service = new GpodnetService();
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
GpodnetPreferences.getHostname());
List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0);
List<PodcastSearchResult> results = new ArrayList<>();
for (GpodnetPodcast podcast : gpodnetPodcasts) {
@ -26,10 +28,6 @@ public class GpodnetPodcastSearcher implements PodcastSearcher {
} catch (GpodnetServiceException e) {
e.printStackTrace();
subscriber.onError(e);
} finally {
if (service != null) {
service.shutdown();
}
}
})
.subscribeOn(Schedulers.io())

View File

@ -1,7 +1,7 @@
package de.danoeh.antennapod.discovery;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import de.mfietz.fyydlin.SearchHit;
import org.json.JSONArray;
import org.json.JSONException;

View File

@ -27,10 +27,11 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
/**
* Displays a list of GPodnetPodcast-Objects in a GridView
@ -117,16 +118,12 @@ public abstract class PodcastListFragment extends Fragment {
protected List<GpodnetPodcast> doInBackground(Void... params) {
GpodnetService service = null;
try {
service = new GpodnetService();
service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname());
return loadPodcastData(service);
} catch (GpodnetServiceException e) {
exception = e;
e.printStackTrace();
return null;
} finally {
if (service != null) {
service.shutdown();
}
}
}

View File

@ -1,16 +1,12 @@
package de.danoeh.antennapod.fragment.gpodnet;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import java.util.List;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
/**
*
*/
public class PodcastTopListFragment extends PodcastListFragment {
private static final String TAG = "PodcastTopListFragment";
private static final int PODCAST_COUNT = 50;
@Override

View File

@ -7,15 +7,14 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import org.apache.commons.lang3.Validate;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import java.util.List;
/**
* Performs a search on the gpodder.net directory and displays the results.

View File

@ -3,10 +3,10 @@ package de.danoeh.antennapod.fragment.gpodnet;
import java.util.Collections;
import java.util.List;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
/**
* Displays suggestions from gpodder.net

View File

@ -3,15 +3,15 @@ package de.danoeh.antennapod.fragment.gpodnet;
import android.os.Bundle;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag;
import org.apache.commons.lang3.Validate;
import java.util.List;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
import java.util.List;
/**
* Shows all podcasts from gpodder.net that belong to a specific tag.

View File

@ -13,15 +13,16 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter;
import de.danoeh.antennapod.core.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag;
import java.util.List;
public class TagListFragment extends ListFragment {
private static final int COUNT = 50;
@ -91,15 +92,13 @@ public class TagListFragment extends ListFragment {
@Override
protected List<GpodnetTag> doInBackground(Void... params) {
GpodnetService service = new GpodnetService();
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname());
try {
return service.getTopTags(COUNT);
} catch (GpodnetServiceException e) {
e.printStackTrace();
exception = e;
return null;
} finally {
service.shutdown();
}
}

View File

@ -11,7 +11,7 @@ import android.widget.Toast;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog;
@ -51,10 +51,10 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
private final SharedPreferences.OnSharedPreferenceChangeListener gpoddernetListener =
(sharedPreferences, key) -> {
if (GpodnetPreferences.PREF_LAST_SYNC_ATTEMPT_TIMESTAMP.equals(key)) {
updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(),
GpodnetPreferences.getLastSyncAttemptTimestamp());
}
//if (GpodnetPreferences.PREF_LAST_SYNC_ATTEMPT_TIMESTAMP.equals(key)) {
// updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(),
// GpodnetPreferences.getLastSyncAttemptTimestamp());
//}
};
private void setupGpodderScreen() {
@ -75,21 +75,13 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
return true;
});
findPreference(PREF_GPODNET_SYNC).setOnPreferenceClickListener(preference -> {
GpodnetSyncService.sendSyncIntent(getActivity().getApplicationContext());
Toast toast = Toast.makeText(getActivity(), R.string.pref_gpodnet_sync_started,
Toast.LENGTH_SHORT);
toast.show();
SyncService.sync(getActivity().getApplicationContext());
Toast.makeText(getActivity(), R.string.pref_gpodnet_sync_started, Toast.LENGTH_SHORT).show();
return true;
});
findPreference(PREF_GPODNET_FORCE_FULL_SYNC).setOnPreferenceClickListener(preference -> {
GpodnetPreferences.setLastSubscriptionSyncTimestamp(0L);
GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(0L);
GpodnetPreferences.setLastSyncAttempt(false, 0);
updateLastGpodnetSyncReport(false, 0);
GpodnetSyncService.sendSyncIntent(getActivity().getApplicationContext());
Toast toast = Toast.makeText(getActivity(), R.string.pref_gpodnet_sync_started,
Toast.LENGTH_SHORT);
toast.show();
SyncService.fullSync(getContext());
Toast.makeText(getActivity(), R.string.pref_gpodnet_sync_started, Toast.LENGTH_SHORT).show();
return true;
});
findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(preference -> {
@ -119,8 +111,8 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
String summary = String.format(format, GpodnetPreferences.getUsername(),
GpodnetPreferences.getDeviceID());
findPreference(PREF_GPODNET_LOGOUT).setSummary(Html.fromHtml(summary));
updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(),
GpodnetPreferences.getLastSyncAttemptTimestamp());
//updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(),
// GpodnetPreferences.getLastSyncAttemptTimestamp());
} else {
findPreference(PREF_GPODNET_LOGOUT).setSummary(null);
updateLastGpodnetSyncReport(false, 0);

View File

@ -12,13 +12,13 @@ import android.view.MenuItem;
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.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.ShareUtils;
@ -191,14 +191,13 @@ public class FeedItemMenuHandler {
FeedMedia media = selectedItem.getMedia();
// not all items have media, Gpodder only cares about those that do
if (media != null) {
GpodnetEpisodeAction actionPlay = new GpodnetEpisodeAction.Builder(selectedItem, Action.PLAY)
.currentDeviceId()
EpisodeAction actionPlay = new EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY)
.currentTimestamp()
.started(media.getDuration() / 1000)
.position(media.getDuration() / 1000)
.total(media.getDuration() / 1000)
.build();
GpodnetPreferences.enqueueEpisodeAction(actionPlay);
SyncService.enqueueEpisodeAction(context, actionPlay);
}
}
break;
@ -206,11 +205,10 @@ public class FeedItemMenuHandler {
selectedItem.setPlayed(false);
DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false);
if (GpodnetPreferences.loggedIn() && selectedItem.getMedia() != null) {
GpodnetEpisodeAction actionNew = new GpodnetEpisodeAction.Builder(selectedItem, Action.NEW)
.currentDeviceId()
EpisodeAction actionNew = new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp()
.build();
GpodnetPreferences.enqueueEpisodeAction(actionNew);
SyncService.enqueueEpisodeAction(context, actionNew);
}
break;
case R.id.add_to_queue_item:

View File

@ -30,8 +30,6 @@ public class ClientConfig {
public static PlaybackServiceCallbacks playbackServiceCallbacks;
public static GpodnetCallbacks gpodnetCallbacks;
public static DBTasksCallbacks dbTasksCallbacks;
public static CastCallbacks castCallbacks;

View File

@ -26,9 +26,10 @@
</intent-filter>
</service>
<service
android:name=".service.GpodnetSyncService"
android:name=".sync.SyncService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:enabled="true" />
android:enabled="true"
android:exported="false" />
<receiver
android:name=".receiver.MediaButtonReceiver"

View File

@ -1,27 +0,0 @@
package de.danoeh.antennapod.core;
import android.app.PendingIntent;
import android.content.Context;
/**
* Callbacks related to the gpodder.net integration of the core module
*/
public interface GpodnetCallbacks {
/**
* Returns if true if the gpodder.net integration should be activated,
* false otherwise.
*/
boolean gpodnetEnabled();
/**
* Returns a PendingIntent for the error notification of the GpodnetSyncService.
* <p/>
* What the PendingIntent does may be implementation-specific.
*
* @return A PendingIntent for the notification or null if gpodder.net integration
* has been disabled (i.e. gpodnetEnabled() == false).
*/
PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context);
}

View File

@ -15,7 +15,6 @@ import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@ -25,6 +24,8 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
public class FeedMedia extends FeedFile implements Playable {
private static final String TAG = "FeedMedia";
@ -502,17 +503,14 @@ public class FeedMedia extends FeedFile implements Playable {
private void postPlaybackTasks(Context context, boolean completed) {
if (item != null) {
// gpodder play action
if (startPosition >= 0 && (completed || startPosition < position) &&
GpodnetPreferences.loggedIn()) {
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.PLAY)
.currentDeviceId()
if (startPosition >= 0 && (completed || startPosition < position) && GpodnetPreferences.loggedIn()) {
EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.PLAY)
.currentTimestamp()
.started(startPosition / 1000)
.position((completed ? duration : position) / 1000)
.total(duration / 1000)
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
SyncService.enqueueEpisodeAction(context, action);
}
}
}

View File

@ -1,21 +0,0 @@
package de.danoeh.antennapod.core.gpoddernet;
public class GpodnetServiceAuthenticationException extends GpodnetServiceException {
private static final long serialVersionUID = 1L;
public GpodnetServiceAuthenticationException() {
super();
}
public GpodnetServiceAuthenticationException(String message, Throwable cause) {
super(message, cause);
}
public GpodnetServiceAuthenticationException(String message) {
super(message);
}
public GpodnetServiceAuthenticationException(Throwable cause) {
super(cause);
}
}

View File

@ -1,20 +0,0 @@
package de.danoeh.antennapod.core.gpoddernet;
public class GpodnetServiceException extends Exception {
private static final long serialVersionUID = 1L;
GpodnetServiceException() {
}
GpodnetServiceException(String message) {
super(message);
}
public GpodnetServiceException(Throwable cause) {
super(cause);
}
GpodnetServiceException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -2,22 +2,11 @@ package de.danoeh.antennapod.core.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
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;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
/**
* Manages preferences for accessing gpodder.net service
@ -34,37 +23,11 @@ public class GpodnetPreferences {
private static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
private static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname";
private static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
private static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_episode_actions_sync_timestamp";
private static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added";
private static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed";
private static final String PREF_SYNC_EPISODE_ACTIONS = "de.danoeh.antennapod.preferences.gpoddernet.sync_queued_episode_actions";
public static final String PREF_LAST_SYNC_ATTEMPT_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_attempt_timestamp";
private static final String PREF_LAST_SYNC_ATTEMPT_RESULT = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_attempt_result";
private static String username;
private static String password;
private static String deviceID;
private static String hostname;
private static final ReentrantLock feedListLock = new ReentrantLock();
private static Set<String> addedFeeds;
private static Set<String> removedFeeds;
private static List<GpodnetEpisodeAction> queuedEpisodeActions;
/**
* Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
*/
private static long lastSubscriptionSyncTimestamp;
private static long lastEpisodeActionsSyncTimeStamp;
private static long lastSyncAttemptTimestamp;
private static boolean lastSyncAttemptResult;
private static boolean preferencesLoaded = false;
private static SharedPreferences getPreferences() {
@ -87,13 +50,6 @@ public class GpodnetPreferences {
username = prefs.getString(PREF_GPODNET_USERNAME, null);
password = prefs.getString(PREF_GPODNET_PASSWORD, null);
deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
lastSubscriptionSyncTimestamp = prefs.getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0);
lastEpisodeActionsSyncTimeStamp = prefs.getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0);
lastSyncAttemptTimestamp = prefs.getLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0);
lastSyncAttemptResult = prefs.getBoolean(PREF_LAST_SYNC_ATTEMPT_RESULT, false);
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;
@ -106,24 +62,6 @@ public class GpodnetPreferences {
editor.apply();
}
private static void writePreference(String key, long value) {
SharedPreferences.Editor editor = getPreferences().edit();
editor.putLong(key, value);
editor.apply();
}
private static void writePreference(String key, Collection<String> value) {
SharedPreferences.Editor editor = getPreferences().edit();
editor.putString(key, writeListToString(value));
editor.apply();
}
private static void writePreference(String key, boolean value) {
SharedPreferences.Editor editor = getPreferences().edit();
editor.putBoolean(key, value);
editor.apply();
}
public static String getUsername() {
ensurePreferencesLoaded();
return username;
@ -154,43 +92,6 @@ public class GpodnetPreferences {
writePreference(PREF_GPODNET_DEVICEID, deviceID);
}
public static long getLastSubscriptionSyncTimestamp() {
ensurePreferencesLoaded();
return lastSubscriptionSyncTimestamp;
}
public static void setLastSubscriptionSyncTimestamp(long timestamp) {
GpodnetPreferences.lastSubscriptionSyncTimestamp = timestamp;
writePreference(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, timestamp);
}
public static long getLastEpisodeActionsSyncTimestamp() {
ensurePreferencesLoaded();
return lastEpisodeActionsSyncTimeStamp;
}
public static void setLastEpisodeActionsSyncTimestamp(long timestamp) {
GpodnetPreferences.lastEpisodeActionsSyncTimeStamp = timestamp;
writePreference(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp);
}
public static long getLastSyncAttemptTimestamp() {
ensurePreferencesLoaded();
return lastSyncAttemptTimestamp;
}
public static boolean getLastSyncAttemptResult() {
ensurePreferencesLoaded();
return lastSyncAttemptResult;
}
public static void setLastSyncAttempt(boolean result, long timestamp) {
GpodnetPreferences.lastSyncAttemptResult = result;
GpodnetPreferences.lastSyncAttemptTimestamp = timestamp;
writePreference(PREF_LAST_SYNC_ATTEMPT_RESULT, result);
writePreference(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, timestamp);
}
public static String getHostname() {
ensurePreferencesLoaded();
return hostname;
@ -205,92 +106,6 @@ public class GpodnetPreferences {
}
}
public static void addAddedFeed(String feed) {
ensurePreferencesLoaded();
feedListLock.lock();
if (addedFeeds.add(feed)) {
writePreference(PREF_SYNC_ADDED, addedFeeds);
}
if (removedFeeds.remove(feed)) {
writePreference(PREF_SYNC_REMOVED, removedFeeds);
}
feedListLock.unlock();
GpodnetSyncService.sendSyncSubscriptionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance());
}
public static void addRemovedFeed(String feed) {
ensurePreferencesLoaded();
feedListLock.lock();
if (removedFeeds.add(feed)) {
writePreference(PREF_SYNC_REMOVED, removedFeeds);
}
if (addedFeeds.remove(feed)) {
writePreference(PREF_SYNC_ADDED, addedFeeds);
}
feedListLock.unlock();
GpodnetSyncService.sendSyncSubscriptionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance());
}
public static Set<String> getAddedFeedsCopy() {
ensurePreferencesLoaded();
Set<String> copy = new HashSet<>();
feedListLock.lock();
copy.addAll(addedFeeds);
feedListLock.unlock();
return copy;
}
public static void removeAddedFeeds(Collection<String> removed) {
ensurePreferencesLoaded();
feedListLock.lock();
addedFeeds.removeAll(removed);
writePreference(PREF_SYNC_ADDED, addedFeeds);
feedListLock.unlock();
}
public static Set<String> getRemovedFeedsCopy() {
ensurePreferencesLoaded();
Set<String> copy = new HashSet<>();
feedListLock.lock();
copy.addAll(removedFeeds);
feedListLock.unlock();
return copy;
}
public static void removeRemovedFeeds(Collection<String> removed) {
ensurePreferencesLoaded();
feedListLock.lock();
removedFeeds.removeAll(removed);
writePreference(PREF_SYNC_REMOVED, removedFeeds);
feedListLock.unlock();
}
public static void enqueueEpisodeAction(GpodnetEpisodeAction action) {
ensurePreferencesLoaded();
feedListLock.lock();
queuedEpisodeActions.add(action);
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
feedListLock.unlock();
GpodnetSyncService.sendSyncActionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance());
}
public static List<GpodnetEpisodeAction> getQueuedEpisodeActions() {
ensurePreferencesLoaded();
List<GpodnetEpisodeAction> copy = new ArrayList<>();
feedListLock.lock();
copy.addAll(queuedEpisodeActions);
feedListLock.unlock();
return copy;
}
public static void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) {
ensurePreferencesLoaded();
feedListLock.lock();
queuedEpisodeActions.removeAll(queued);
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
feedListLock.unlock();
}
/**
* Returns true if device ID, username and password have a non-null value
*/
@ -304,57 +119,10 @@ public class GpodnetPreferences {
setUsername(null);
setPassword(null);
setDeviceID(null);
feedListLock.lock();
addedFeeds.clear();
writePreference(PREF_SYNC_ADDED, addedFeeds);
removedFeeds.clear();
writePreference(PREF_SYNC_REMOVED, removedFeeds);
queuedEpisodeActions.clear();
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
feedListLock.unlock();
setLastSubscriptionSyncTimestamp(0);
setLastSyncAttempt(false, 0);
SyncService.clearQueue(ClientConfig.applicationCallbacks.getApplicationInstance());
UserPreferences.setGpodnetNotificationsEnabled();
}
private static Set<String> readListFromString(String s) {
Set<String> result = new HashSet<>();
Collections.addAll(result, s.split(" "));
return result;
}
private static String writeListToString(Collection<String> c) {
StringBuilder result = new StringBuilder();
for (String item : c) {
result.append(item);
result.append(" ");
}
return result.toString().trim();
}
private static List<GpodnetEpisodeAction> readEpisodeActionsFromString(String s) {
String[] lines = s.split("\n");
List<GpodnetEpisodeAction> result = new ArrayList<>(lines.length);
for(String line : lines) {
if(TextUtils.isEmpty(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://")) {

View File

@ -1,363 +0,0 @@
package de.danoeh.antennapod.core.service;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.SafeJobIntentService;
import androidx.collection.ArrayMap;
import android.util.Log;
import android.util.Pair;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
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.feed.FeedMedia;
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.preferences.UserPreferences;
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;
import de.danoeh.antennapod.core.util.URLChecker;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
/**
* Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument.
* This class also provides static methods for starting the GpodnetSyncService.
*/
public class GpodnetSyncService extends SafeJobIntentService {
private static final String TAG = "GpodnetSyncService";
private static final long WAIT_INTERVAL = 5000L;
private static final String ARG_ACTION = "action";
private static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync";
private static final String ACTION_SYNC_SUBSCRIPTIONS = "de.danoeh.antennapod.intent.action.sync_subscriptions";
private static final String ACTION_SYNC_ACTIONS = "de.danoeh.antennapod.intent.action.sync_ACTIONS";
private GpodnetService service;
private static final AtomicInteger syncActionCount = new AtomicInteger(0);
private static boolean syncSubscriptions = false;
private static boolean syncActions = false;
private static final int JOB_ID = -17000;
private static void enqueueWork(Context context, Intent intent) {
enqueueWork(context, GpodnetSyncService.class, JOB_ID, intent);
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
final String action = intent.getStringExtra(ARG_ACTION);
if (action != null) {
switch(action) {
case ACTION_SYNC:
syncSubscriptions = true;
syncActions = true;
break;
case ACTION_SYNC_SUBSCRIPTIONS:
syncSubscriptions = true;
break;
case ACTION_SYNC_ACTIONS:
syncActions = true;
break;
default:
Log.e(TAG, "Received invalid intent: action argument is invalid");
}
if(syncSubscriptions || syncActions) {
Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
int syncActionId = syncActionCount.incrementAndGet();
try {
Thread.sleep(WAIT_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (syncActionId == syncActionCount.get()) {
// onHandleWork was not called again in the meantime
sync();
}
}
} else {
Log.e(TAG, "Received invalid intent: action argument is null");
}
}
private synchronized GpodnetService tryLogin() throws GpodnetServiceException {
if (service == null) {
service = new GpodnetService();
service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
}
return service;
}
private synchronized void sync() {
if (!GpodnetPreferences.loggedIn() || !NetworkUtils.networkAvailable()) {
stopForeground(true);
stopSelf();
return;
}
boolean initialSync = GpodnetPreferences.getLastSubscriptionSyncTimestamp() == 0 &&
GpodnetPreferences.getLastEpisodeActionsSyncTimestamp() == 0;
if(syncSubscriptions) {
syncSubscriptionChanges();
syncSubscriptions = false;
}
if(syncActions) {
// we only sync episode actions after the subscriptions have been added to the database
if(!initialSync) {
syncEpisodeActions();
}
syncActions = false;
}
}
private synchronized void syncSubscriptionChanges() {
final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp();
try {
final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls();
Collection<String> localAdded = GpodnetPreferences.getAddedFeedsCopy();
Collection<String> localRemoved = GpodnetPreferences.getRemovedFeedsCopy();
GpodnetService service = tryLogin();
// first sync: download all subscriptions...
GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(),
GpodnetPreferences.getDeviceID(), timestamp);
long newTimeStamp = subscriptionChanges.getTimestamp();
Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
processSubscriptionChanges(localSubscriptions, localAdded, localRemoved, subscriptionChanges);
if(timestamp == 0) {
// this is this apps first sync with gpodder:
// only submit changes gpodder has not just sent us
localAdded = localSubscriptions;
localAdded.removeAll(subscriptionChanges.getAdded());
localRemoved.removeAll(subscriptionChanges.getRemoved());
}
if(localAdded.size() > 0 || localRemoved.size() > 0) {
Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
localAdded, localRemoved));
GpodnetUploadChangesResponse uploadResponse = service.uploadChanges(GpodnetPreferences.getUsername(),
GpodnetPreferences.getDeviceID(), localAdded, localRemoved);
newTimeStamp = uploadResponse.timestamp;
Log.d(TAG, "Upload changes response: " + uploadResponse);
GpodnetPreferences.removeAddedFeeds(localAdded);
GpodnetPreferences.removeRemovedFeeds(localRemoved);
}
GpodnetPreferences.setLastSubscriptionSyncTimestamp(newTimeStamp);
GpodnetPreferences.setLastSyncAttempt(true, System.currentTimeMillis());
clearErrorNotifications();
} catch (GpodnetServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
} catch (DownloadRequestException e) {
e.printStackTrace();
}
}
private synchronized void processSubscriptionChanges(List<String> localSubscriptions,
Collection<String> localAdded,
Collection<String> localRemoved,
GpodnetSubscriptionChange changes) throws DownloadRequestException {
// local changes are always superior to remote changes!
// add subscription if (1) not already subscribed and (2) not just unsubscribed
for (String downloadUrl : changes.getAdded()) {
if (!URLChecker.containsUrl(localSubscriptions, downloadUrl) && !localRemoved.contains(downloadUrl)) {
Feed feed = new Feed(downloadUrl, null);
DownloadRequester.getInstance().downloadFeed(this, feed);
}
}
// remove subscription if not just subscribed (again)
for (String downloadUrl : changes.getRemoved()) {
if (!localAdded.contains(downloadUrl)) {
DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl);
}
}
}
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);
List<GpodnetEpisodeAction> remoteActions = getResponse.getEpisodeActions();
List<GpodnetEpisodeAction> localActions = GpodnetPreferences.getQueuedEpisodeActions();
processEpisodeActions(localActions, remoteActions);
// upload local actions
if(localActions.size() > 0) {
Log.d(TAG, "Uploading episode actions: " + localActions);
GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(localActions);
lastUpdate = postResponse.timestamp;
Log.d(TAG, "Upload episode response: " + postResponse);
GpodnetPreferences.removeQueuedEpisodeActions(localActions);
}
GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(lastUpdate);
GpodnetPreferences.setLastSyncAttempt(true, System.currentTimeMillis());
clearErrorNotifications();
} catch (GpodnetServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
}
}
private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> localActions,
List<GpodnetEpisodeAction> remoteActions) {
if(remoteActions.size() == 0) {
return;
}
Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new ArrayMap<>();
for(GpodnetEpisodeAction action : localActions) {
Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode());
GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key);
if (mostRecent == null || mostRecent.getTimestamp() == null) {
localMostRecentPlayAction.put(key, action);
} else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
localMostRecentPlayAction.put(key, action);
}
}
// make sure more recent local actions are not overwritten by older remote actions
Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new ArrayMap<>();
for (GpodnetEpisodeAction action : remoteActions) {
switch (action.getAction()) {
case NEW:
FeedItem newItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode());
if(newItem != null) {
DBWriter.markItemPlayed(newItem, FeedItem.UNPLAYED, true);
} else {
Log.i(TAG, "Unknown feed item: " + action);
}
break;
case DOWNLOAD:
break;
case PLAY:
Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode());
GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key);
if(localMostRecent == null ||
localMostRecent.getTimestamp() == null ||
localMostRecent.getTimestamp().before(action.getTimestamp())) {
GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key);
if (mostRecent == null || mostRecent.getTimestamp() == null) {
mostRecentPlayAction.put(key, action);
} else if (action.getTimestamp() != null && mostRecent.getTimestamp().before(action.getTimestamp())) {
mostRecentPlayAction.put(key, action);
} else {
Log.d(TAG, "No date information in action, skipping it");
}
}
break;
case DELETE:
// NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop
break;
}
}
for (GpodnetEpisodeAction action : mostRecentPlayAction.values()) {
FeedItem playItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode());
if (playItem != null) {
FeedMedia media = playItem.getMedia();
media.setPosition(action.getPosition() * 1000);
DBWriter.setFeedMedia(media);
if(playItem.getMedia().hasAlmostEnded()) {
DBWriter.markItemPlayed(playItem, FeedItem.PLAYED, true);
DBWriter.addItemToPlaybackHistory(playItem.getMedia());
}
}
}
}
private void clearErrorNotifications() {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(R.id.notification_gpodnet_sync_error);
nm.cancel(R.id.notification_gpodnet_sync_autherror);
}
private void updateErrorNotification(GpodnetServiceException exception) {
Log.d(TAG, "Posting error notification");
GpodnetPreferences.setLastSyncAttempt(false, System.currentTimeMillis());
final String title;
final String description;
final int id;
if (exception instanceof GpodnetServiceAuthenticationException) {
title = getString(R.string.gpodnetsync_auth_error_title);
description = getString(R.string.gpodnetsync_auth_error_descr);
id = R.id.notification_gpodnet_sync_autherror;
} else {
if (UserPreferences.gpodnetNotificationsEnabled()) {
title = getString(R.string.gpodnetsync_error_title);
description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage();
id = R.id.notification_gpodnet_sync_error;
} else {
return;
}
}
PendingIntent activityIntent = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this);
Notification notification = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR)
.setContentTitle(title)
.setContentText(description)
.setContentIntent(activityIntent)
.setSmallIcon(R.drawable.ic_notification_sync_error)
.setAutoCancel(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.build();
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(id, notification);
}
public static void sendSyncIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC);
enqueueWork(context, intent);
}
}
public static void sendSyncSubscriptionsIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC_SUBSCRIPTIONS);
enqueueWork(context, intent);
}
}
public static void sendSyncActionsIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC_ACTIONS);
enqueueWork(context, intent);
}
}
}

View File

@ -17,6 +17,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.danoeh.antennapod.core.sync.SyncService;
import org.apache.commons.io.FileUtils;
import org.greenrobot.eventbus.EventBus;
@ -42,9 +43,7 @@ import de.danoeh.antennapod.core.event.FeedItemEvent;
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.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler;
import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask;
import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler;
@ -234,11 +233,7 @@ public class DownloadService extends Service {
// if this was the initial gpodder sync, i.e. we just synced the feeds successfully,
// it is now time to sync the episode actions
if (GpodnetPreferences.loggedIn() &&
GpodnetPreferences.getLastSubscriptionSyncTimestamp() > 0 &&
GpodnetPreferences.getLastEpisodeActionsSyncTimestamp() == 0) {
GpodnetSyncService.sendSyncActionsIntent(this);
}
SyncService.sync(this);
// start auto download in case anything new has shown up
DBTasks.autodownloadUndownloadedItems(getApplicationContext());

View File

@ -12,14 +12,14 @@ import java.util.concurrent.ExecutionException;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
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.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
import org.greenrobot.eventbus.EventBus;
/**
@ -99,12 +99,11 @@ public class MediaDownloadedHandler implements Runnable {
DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage(), request.isInitiatedByUser());
}
if (GpodnetPreferences.loggedIn() && item != null) {
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DOWNLOAD)
.currentDeviceId()
if (item != null) {
EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD)
.currentTimestamp()
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
SyncService.enqueueEpisodeAction(context, action);
}
}

View File

@ -17,8 +17,8 @@ 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.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
@ -123,9 +123,7 @@ public final class DBTasks {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
GpodnetSyncService.sendSyncIntent(context);
}
SyncService.sync(context);
// Note: automatic download of episodes will be done but not here.
// Instead it is done after all feeds have been refreshed (asynchronously),
// in DownloadService.onDestroy()

View File

@ -7,6 +7,8 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
@ -34,7 +36,6 @@ import de.danoeh.antennapod.core.feed.FeedEvent;
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.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@ -118,11 +119,10 @@ public class DBWriter {
// Gpodder: queue delete action for synchronization
if (GpodnetPreferences.loggedIn()) {
FeedItem item = media.getItem();
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE)
.currentDeviceId()
EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE)
.currentTimestamp()
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
SyncService.enqueueEpisodeAction(context, action);
}
}
EventBus.getDefault().post(FeedItemEvent.deletedMedia(Collections.singletonList(media.getItem())));
@ -169,9 +169,7 @@ public class DBWriter {
adapter.removeFeed(feed);
adapter.close();
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
}
SyncService.enqueueFeedRemoved(context, feed.getDownload_url());
EventBus.getDefault().post(new FeedListUpdateEvent(feed));
// we assume we also removed download log entries for the feed or its media files.
@ -727,10 +725,8 @@ public class DBWriter {
adapter.setCompleteFeed(feeds);
adapter.close();
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
for (Feed feed : feeds) {
GpodnetPreferences.addAddedFeed(feed.getDownload_url());
}
for (Feed feed : feeds) {
SyncService.enqueueFeedAdded(context, feed.getDownload_url());
}
BackupManager backupManager = new BackupManager(context);

View File

@ -0,0 +1,381 @@
package de.danoeh.antennapod.core.sync;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.collection.ArrayMap;
import androidx.core.app.NotificationCompat;
import androidx.core.app.SafeJobIntentService;
import androidx.core.util.Pair;
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.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
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.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
import de.danoeh.antennapod.core.sync.model.EpisodeActionChanges;
import de.danoeh.antennapod.core.sync.model.ISyncService;
import de.danoeh.antennapod.core.sync.model.SubscriptionChanges;
import de.danoeh.antennapod.core.sync.model.SyncServiceException;
import de.danoeh.antennapod.core.sync.model.UploadChangesResponse;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.URLChecker;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class SyncService extends SafeJobIntentService {
private static final String PREF_NAME = "SyncService";
private static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "last_sync_timestamp";
private static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp";
private static final String PREF_QUEUED_FEEDS_ADDED = "sync_added";
private static final String PREF_QUEUED_FEEDS_REMOVED = "sync_removed";
private static final String PREF_QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions";
private static final String PREF_LAST_SYNC_ATTEMPT_TIMESTAMP = "last_sync_attempt_timestamp";
private static final String TAG = "SyncService";
private static final int JOB_ID = -17000;
private static final Object lock = new Object();
private static boolean syncPending = false;
private ISyncService syncServiceImpl;
@Override
protected void onHandleWork(@NonNull Intent intent) {
syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetService.DEFAULT_BASE_HOST);
if (!NetworkUtils.networkAvailable()) {
stopForeground(true);
stopSelf();
return;
}
try {
// Leave some time, so other actions can be queued
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
syncPending = false;
try {
syncServiceImpl.login();
syncSubscriptions();
syncEpisodeActions();
syncServiceImpl.logout();
clearErrorNotifications();
} catch (SyncServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
}
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply();
}
public static void clearQueue(Context context) {
synchronized (lock) {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
.putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0)
.putString(PREF_QUEUED_EPISODE_ACTIONS, "[]")
.putString(PREF_QUEUED_FEEDS_ADDED, "[]")
.putString(PREF_QUEUED_FEEDS_REMOVED, "[]")
.apply();
}
}
public static void enqueueFeedAdded(Context context, String downloadUrl) {
synchronized (lock) {
try {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]");
JSONArray queue = new JSONArray(json);
queue.put(downloadUrl);
prefs.edit().putString(PREF_QUEUED_FEEDS_ADDED, queue.toString()).apply();
} catch (JSONException e) {
e.printStackTrace();
}
}
sync(context);
}
public static void enqueueFeedRemoved(Context context, String downloadUrl) {
synchronized (lock) {
try {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]");
JSONArray queue = new JSONArray(json);
queue.put(downloadUrl);
prefs.edit().putString(PREF_QUEUED_FEEDS_REMOVED, queue.toString()).apply();
} catch (JSONException e) {
e.printStackTrace();
}
}
sync(context);
}
public static void enqueueEpisodeAction(Context context, EpisodeAction action) {
synchronized (lock) {
try {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]");
JSONArray queue = new JSONArray(json);
queue.put(action.writeToJSONObject());
prefs.edit().putString(PREF_QUEUED_EPISODE_ACTIONS, queue.toString()).apply();
} catch (JSONException e) {
e.printStackTrace();
}
}
sync(context);
}
public static void sync(Context context) {
if (!syncPending) {
syncPending = true;
enqueueWork(context, SyncService.class, JOB_ID, new Intent());
} else {
Log.d(TAG, "Ignored sync: Job already enqueued");
}
}
public static void fullSync(Context context) {
synchronized (lock) {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
.putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0)
.apply();
}
sync(context);
}
private List<EpisodeAction> getQueuedEpisodeActions() {
ArrayList<EpisodeAction> actions = new ArrayList<>();
try {
SharedPreferences prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]");
JSONArray queue = new JSONArray(json);
for (int i = 0; i < queue.length(); i++) {
actions.add(EpisodeAction.readFromJSONObject(queue.getJSONObject(i)));
}
} catch (JSONException e) {
e.printStackTrace();
}
return actions;
}
private List<String> getQueuedRemovedFeeds() {
ArrayList<String> actions = new ArrayList<>();
try {
SharedPreferences prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]");
JSONArray queue = new JSONArray(json);
for (int i = 0; i < queue.length(); i++) {
actions.add(queue.getString(i));
}
} catch (JSONException e) {
e.printStackTrace();
}
return actions;
}
private List<String> getQueuedAddedFeeds() {
ArrayList<String> actions = new ArrayList<>();
try {
SharedPreferences prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]");
JSONArray queue = new JSONArray(json);
for (int i = 0; i < queue.length(); i++) {
actions.add(queue.getString(i));
}
} catch (JSONException e) {
e.printStackTrace();
}
return actions;
}
private void syncSubscriptions() throws SyncServiceException {
final long lastSync = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0);
final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls();
SubscriptionChanges subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync);
long newTimeStamp = subscriptionChanges.getTimestamp();
List<String> queuedRemovedFeeds = getQueuedRemovedFeeds();
List<String> queuedAddedFeeds = getQueuedAddedFeeds();
Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
for (String downloadUrl : subscriptionChanges.getAdded()) {
if (!URLChecker.containsUrl(localSubscriptions, downloadUrl) && !queuedRemovedFeeds.contains(downloadUrl)) {
Feed feed = new Feed(downloadUrl, null);
try {
DownloadRequester.getInstance().downloadFeed(this, feed);
} catch (DownloadRequestException e) {
e.printStackTrace();
}
}
}
// remove subscription if not just subscribed (again)
for (String downloadUrl : subscriptionChanges.getRemoved()) {
if (!queuedAddedFeeds.contains(downloadUrl)) {
DBTasks.removeFeedWithDownloadUrl(this, downloadUrl);
}
}
if (lastSync == 0) {
Log.d(TAG, "First sync. Adding all local subscriptions.");
queuedAddedFeeds = localSubscriptions;
queuedAddedFeeds.removeAll(subscriptionChanges.getAdded());
queuedRemovedFeeds.removeAll(subscriptionChanges.getRemoved());
}
if (queuedAddedFeeds.size() > 0 || queuedRemovedFeeds.size() > 0) {
Log.d(TAG, "Added: " + StringUtils.join(queuedAddedFeeds, ", "));
Log.d(TAG, "Removed: " + StringUtils.join(queuedRemovedFeeds, ", "));
synchronized (lock) {
UploadChangesResponse uploadResponse = syncServiceImpl
.uploadSubscriptionChanges(queuedAddedFeeds, queuedRemovedFeeds);
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putString(PREF_QUEUED_FEEDS_ADDED, "[]").apply();
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putString(PREF_QUEUED_FEEDS_REMOVED, "[]").apply();
newTimeStamp = uploadResponse.timestamp;
}
}
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply();
}
private void syncEpisodeActions() throws SyncServiceException {
final long lastSync = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0);
EpisodeActionChanges getResponse = syncServiceImpl.getEpisodeActionChanges(lastSync);
long newTimeStamp = getResponse.getTimestamp();
List<EpisodeAction> remoteActions = getResponse.getEpisodeActions();
processEpisodeActions(remoteActions);
// upload local actions
List<EpisodeAction> queuedEpisodeActions = getQueuedEpisodeActions();
if (queuedEpisodeActions.size() > 0) {
synchronized (lock) {
Log.d(TAG, "Uploading actions: " + StringUtils.join(queuedEpisodeActions, ", "));
UploadChangesResponse postResponse = syncServiceImpl.uploadEpisodeActions(queuedEpisodeActions);
newTimeStamp = postResponse.timestamp;
Log.d(TAG, "Upload episode response: " + postResponse);
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putString(PREF_QUEUED_EPISODE_ACTIONS, "[]").apply();
}
}
getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
.putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, newTimeStamp).apply();
}
private synchronized void processEpisodeActions(List<EpisodeAction> remoteActions) {
if (remoteActions.size() == 0) {
return;
}
Map<Pair<String, String>, EpisodeAction> localMostRecentPlayAction = new ArrayMap<>();
for (EpisodeAction action : getQueuedEpisodeActions()) {
Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode());
EpisodeAction mostRecent = localMostRecentPlayAction.get(key);
if (mostRecent == null || mostRecent.getTimestamp() == null) {
localMostRecentPlayAction.put(key, action);
} else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
localMostRecentPlayAction.put(key, action);
}
}
// make sure more recent local actions are not overwritten by older remote actions
Map<Pair<String, String>, EpisodeAction> mostRecentPlayAction = new ArrayMap<>();
for (EpisodeAction action : remoteActions) {
switch (action.getAction()) {
case NEW:
FeedItem newItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode());
if (newItem != null) {
DBWriter.markItemPlayed(newItem, FeedItem.UNPLAYED, true);
} else {
Log.i(TAG, "Unknown feed item: " + action);
}
break;
case DOWNLOAD:
break;
case PLAY:
Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode());
EpisodeAction localMostRecent = localMostRecentPlayAction.get(key);
if (localMostRecent == null || localMostRecent.getTimestamp() == null
|| localMostRecent.getTimestamp().before(action.getTimestamp())) {
EpisodeAction mostRecent = mostRecentPlayAction.get(key);
if (mostRecent == null || mostRecent.getTimestamp() == null) {
mostRecentPlayAction.put(key, action);
} else if (action.getTimestamp() != null
&& mostRecent.getTimestamp().before(action.getTimestamp())) {
mostRecentPlayAction.put(key, action);
} else {
Log.d(TAG, "No date information in action, skipping it");
}
}
break;
case DELETE:
// NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop
break;
}
}
for (EpisodeAction action : mostRecentPlayAction.values()) {
FeedItem playItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode());
if (playItem != null) {
FeedMedia media = playItem.getMedia();
media.setPosition(action.getPosition() * 1000);
DBWriter.setFeedMedia(media);
if (playItem.getMedia().hasAlmostEnded()) {
DBWriter.markItemPlayed(playItem, FeedItem.PLAYED, true);
DBWriter.addItemToPlaybackHistory(playItem.getMedia());
}
}
}
}
private void clearErrorNotifications() {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(R.id.notification_gpodnet_sync_error);
nm.cancel(R.id.notification_gpodnet_sync_autherror);
}
private void updateErrorNotification(SyncServiceException exception) {
Log.d(TAG, "Posting error notification");
final String description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage();
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR)
.setContentTitle(getString(R.string.gpodnetsync_error_title))
.setContentText(description)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_notification_sync_error)
.setAutoCancel(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.build();
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(R.id.notification_gpodnet_sync_error, notification);
}
}

View File

@ -1,7 +1,26 @@
package de.danoeh.antennapod.core.gpoddernet;
package de.danoeh.antennapod.core.sync.gpoddernet;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
import de.danoeh.antennapod.core.sync.model.EpisodeActionChanges;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.core.sync.model.ISyncService;
import de.danoeh.antennapod.core.sync.model.SubscriptionChanges;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetUploadChangesResponse;
import de.danoeh.antennapod.core.sync.model.SyncServiceException;
import de.danoeh.antennapod.core.sync.model.UploadChangesResponse;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.io.Charsets;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -14,63 +33,41 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
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;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* Communicates with the gpodder.net service.
*/
public class GpodnetService {
private static final String TAG = "GpodnetService";
private static final String BASE_SCHEME = "https";
public class GpodnetService implements ISyncService {
public static final String DEFAULT_BASE_HOST = "gpodder.net";
private final String BASE_HOST;
private static final String BASE_SCHEME = "https";
private static final MediaType TEXT = MediaType.parse("plain/text; charset=utf-8");
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private final String baseHost;
private final OkHttpClient httpClient;
private String username = null;
public GpodnetService(OkHttpClient httpClient, String baseHost) {
this.httpClient = httpClient;
this.baseHost = baseHost;
}
public GpodnetService() {
httpClient = AntennapodHttpClient.getHttpClient();
BASE_HOST = GpodnetPreferences.getHostname();
private void requireLoggedIn() {
if (username == null) {
throw new IllegalStateException("Not logged in");
}
}
/**
* Returns the [count] most used tags.
*/
public List<GpodnetTag> getTopTags(int count)
throws GpodnetServiceException {
public List<GpodnetTag> getTopTags(int count) throws GpodnetServiceException {
URL url;
try {
url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/tags/%d.json", count), null).toURL();
url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/tags/%d.json", count), null).toURL();
} catch (MalformedURLException | URISyntaxException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
@ -80,13 +77,12 @@ public class GpodnetService {
String response = executeRequest(request);
try {
JSONArray jsonTagList = new JSONArray(response);
List<GpodnetTag> tagList = new ArrayList<>(
jsonTagList.length());
List<GpodnetTag> tagList = new ArrayList<>(jsonTagList.length());
for (int i = 0; i < jsonTagList.length(); i++) {
JSONObject jObj = jsonTagList.getJSONObject(i);
String title = jObj.getString("title");
String tag = jObj.getString("tag");
int usage = jObj.getInt("usage");
JSONObject jsonObject = jsonTagList.getJSONObject(i);
String title = jsonObject.getString("title");
String tag = jsonObject.getString("tag");
int usage = jsonObject.getInt("usage");
tagList.add(new GpodnetTag(title, tag, usage));
}
return tagList;
@ -101,11 +97,10 @@ public class GpodnetService {
*
* @throws IllegalArgumentException if tag is null
*/
public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag,
int count)
public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag, int count)
throws GpodnetServiceException {
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
URL url = new URI(BASE_SCHEME, baseHost, String.format(
"/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@ -125,15 +120,13 @@ public class GpodnetService {
* @param count of elements that should be returned. Must be in range 1..100.
* @throws IllegalArgumentException if count is out of range.
*/
public List<GpodnetPodcast> getPodcastToplist(int count)
throws GpodnetServiceException {
if(count < 1 || count > 100) {
public List<GpodnetPodcast> getPodcastToplist(int count) throws GpodnetServiceException {
if (count < 1 || count > 100) {
throw new IllegalArgumentException("Count must be in range 1..100");
}
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/toplist/%d.json", count), null).toURL();
URL url = new URI(BASE_SCHEME, baseHost, String.format("/toplist/%d.json", count), null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@ -159,13 +152,12 @@ public class GpodnetService {
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
public List<GpodnetPodcast> getSuggestions(int count) throws GpodnetServiceException {
if(count < 1 || count > 100) {
if (count < 1 || count > 100) {
throw new IllegalArgumentException("Count must be in range 1..100");
}
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/suggestions/%d.json", count), null).toURL();
URL url = new URI(BASE_SCHEME, baseHost, String.format("/suggestions/%d.json", count), null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@ -185,13 +177,12 @@ public class GpodnetService {
* Must be in range 1..256. If the value is out of range, the
* default value defined by the gpodder.net API will be used.
*/
public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize)
throws GpodnetServiceException {
public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize) throws GpodnetServiceException {
String parameters = (scaledLogoSize > 0 && scaledLogoSize <= 256) ? String
.format("q=%s&scale_logo=%d", query, scaledLogoSize) : String
.format("q=%s", query);
try {
URL url = new URI(BASE_SCHEME, null, BASE_HOST, -1, "/search.json",
URL url = new URI(BASE_SCHEME, null, baseHost, -1, "/search.json",
parameters, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@ -213,16 +204,12 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param username The username. Must be the same user as the one which is
* currently logged in.
* @throws IllegalArgumentException If username is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
public List<GpodnetDevice> getDevices(@NonNull String username)
throws GpodnetServiceException {
public List<GpodnetDevice> getDevices() throws GpodnetServiceException {
requireLoggedIn();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/devices/%s.json", username), null).toURL();
URL url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/devices/%s.json", username), null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
JSONArray devicesArray = new JSONArray(response);
@ -238,19 +225,14 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param username The username. Must be the same user as the one which is
* currently logged in.
* @param deviceId The ID of the device that should be configured.
* @throws IllegalArgumentException If username or deviceId is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
public void configureDevice(@NonNull String username,
@NonNull String deviceId,
String caption,
GpodnetDevice.DeviceType type)
public void configureDevice(@NonNull String deviceId, String caption, GpodnetDevice.DeviceType type)
throws GpodnetServiceException {
requireLoggedIn();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
URL url = new URI(BASE_SCHEME, baseHost, String.format(
"/api/2/devices/%s/%s.json", username, deviceId), null).toURL();
String content;
if (caption != null || type != null) {
@ -279,18 +261,14 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param username The username. Must be the same user as the one which is
* currently logged in.
* @param deviceId The ID of the device whose subscriptions should be returned.
* @return A list of subscriptions in OPML format.
* @throws IllegalArgumentException If username or deviceId is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
public String getSubscriptionsOfDevice(@NonNull String username,
@NonNull String deviceId)
throws GpodnetServiceException {
public String getSubscriptionsOfDevice(@NonNull String deviceId) throws GpodnetServiceException {
requireLoggedIn();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
URL url = new URI(BASE_SCHEME, baseHost, String.format(
"/subscriptions/%s/%s.opml", username, deviceId), null).toURL();
Request.Builder request = new Request.Builder().url(url);
return executeRequest(request);
@ -305,18 +283,14 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param username The username. Must be the same user as the one which is
* currently logged in.
* @return A list of subscriptions in OPML format.
* @throws IllegalArgumentException If username is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
public String getSubscriptionsOfUser(@NonNull String username)
throws GpodnetServiceException {
public String getSubscriptionsOfUser() throws GpodnetServiceException {
requireLoggedIn();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/subscriptions/%s.opml", username), null).toURL();
URL url = new URI(BASE_SCHEME, baseHost, String.format("/subscriptions/%s.opml", username), null).toURL();
Request.Builder request = new Request.Builder().url(url);
return executeRequest(request);
} catch (MalformedURLException | URISyntaxException e) {
@ -330,21 +304,17 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param username The username. Must be the same user as the one which is
* currently logged in.
* @param deviceId The ID of the device whose subscriptions should be updated.
* @param subscriptions A list of feed URLs containing all subscriptions of the
* device.
* @throws IllegalArgumentException If username, deviceId or subscriptions is null.
* @throws GpodnetServiceAuthenticationException If there is an authentication error.
*/
public void uploadSubscriptions(@NonNull String username,
@NonNull String deviceId,
@NonNull List<String> subscriptions)
public void uploadSubscriptions(@NonNull String deviceId, @NonNull List<String> subscriptions)
throws GpodnetServiceException {
requireLoggedIn();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
URL url = new URI(BASE_SCHEME, baseHost, String.format(
"/subscriptions/%s/%s.txt", username, deviceId), null).toURL();
StringBuilder builder = new StringBuilder();
for (String s : subscriptions) {
@ -366,25 +336,19 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param username The username. Must be the same user as the one which is
* currently logged in.
* @param deviceId The ID of the device whose subscriptions should be updated.
* @param added Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates
* @param removed Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates
* @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse}
* @return a GpodnetUploadChangesResponse. See {@link 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.
* @throws GpodnetServiceException if added or removed contain duplicates or if there
* is an authentication error.
*/
public GpodnetUploadChangesResponse uploadChanges(@NonNull String username,
@NonNull String deviceId,
@NonNull Collection<String> added,
@NonNull Collection<String> removed)
throws GpodnetServiceException {
public GpodnetUploadChangesResponse uploadChanges(@NonNull String deviceId, @NonNull Collection<String> added,
@NonNull Collection<String> removed) throws GpodnetServiceException {
requireLoggedIn();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
URL url = new URI(BASE_SCHEME, baseHost, String.format(
"/api/2/subscriptions/%s/%s.json", username, deviceId), null).toURL();
final JSONObject requestObject = new JSONObject();
@ -408,24 +372,19 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param username The username. Must be the same user as the one which is
* currently logged in.
* @param deviceId The ID of the device whose subscription changes should be
* downloaded.
* @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 GpodnetSubscriptionChange getSubscriptionChanges(@NonNull String username,
@NonNull String deviceId,
long timestamp) throws GpodnetServiceException {
public SubscriptionChanges getSubscriptionChanges(@NonNull String deviceId, long timestamp)
throws GpodnetServiceException {
requireLoggedIn();
String params = String.format("since=%d", timestamp);
String path = String.format("/api/2/subscriptions/%s/%s.json",
username, deviceId);
String path = String.format("/api/2/subscriptions/%s/%s.json", username, deviceId);
try {
URL url = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params,
URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params,
null).toURL();
Request.Builder request = new Request.Builder().url(url);
@ -447,26 +406,24 @@ public class GpodnetService {
* <p/>
* This method requires authentication.
*
* @param episodeActions Collection of episode actions.
* @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse}
* @param episodeActions Collection of episode actions.
* @return a GpodnetUploadChangesResponse. See {@link 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.
* @throws GpodnetServiceException if added or removed contain duplicates or if there
* is an authentication error.
*/
public GpodnetEpisodeActionPostResponse uploadEpisodeActions(@NonNull Collection<GpodnetEpisodeAction> episodeActions)
throws GpodnetServiceException {
String username = GpodnetPreferences.getUsername();
@Override
public UploadChangesResponse uploadEpisodeActions(List<EpisodeAction> episodeActions) throws SyncServiceException {
requireLoggedIn();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
URL url = new URI(BASE_SCHEME, baseHost, String.format(
"/api/2/episodes/%s.json", username), null).toURL();
final JSONArray list = new JSONArray();
for(GpodnetEpisodeAction episodeAction : episodeActions) {
for (EpisodeAction episodeAction : episodeActions) {
JSONObject obj = episodeAction.writeToJSONObject();
if(obj != null) {
if (obj != null) {
obj.put("device", GpodnetPreferences.getDeviceID());
list.put(obj);
}
}
@ -478,7 +435,7 @@ public class GpodnetService {
return GpodnetEpisodeActionPostResponse.fromJSONObject(response);
} catch (JSONException | MalformedURLException | URISyntaxException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
throw new SyncServiceException(e);
}
}
@ -490,19 +447,15 @@ public class GpodnetService {
*
* @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.
* @throws SyncServiceException If there is an authentication error.
*/
public GpodnetEpisodeActionGetResponse getEpisodeChanges(long timestamp) throws GpodnetServiceException {
String username = GpodnetPreferences.getUsername();
@Override
public EpisodeActionChanges getEpisodeActionChanges(long timestamp) throws SyncServiceException {
requireLoggedIn();
String params = String.format("since=%d", timestamp);
String path = String.format("/api/2/episodes/%s.json",
username);
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();
URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@ -513,7 +466,7 @@ public class GpodnetService {
throw new IllegalStateException(e);
} catch (JSONException | MalformedURLException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
throw new SyncServiceException(e);
}
}
@ -525,33 +478,27 @@ public class GpodnetService {
*
* @throws IllegalArgumentException If username or password is null.
*/
public void authenticate(@NonNull String username,
@NonNull String password)
throws GpodnetServiceException {
public void authenticate(@NonNull String username, @NonNull String password) throws GpodnetServiceException {
URL url;
try {
url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/auth/%s/login.json", username), null).toURL();
url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/auth/%s/login.json", username), null).toURL();
} catch (MalformedURLException | URISyntaxException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
}
RequestBody body = RequestBody.create(TEXT, "");
Request.Builder request = new Request.Builder().url(url).post(body);
executeRequestWithAuthentication(request, username, password);
}
/**
* Shuts down the GpodnetService's HTTP client. The service will be shut down in a separate thread to avoid
* NetworkOnMainThreadExceptions.
*/
public void shutdown() {
new Thread() {
@Override
public void run() {
AntennapodHttpClient.cleanup();
}
}.start();
RequestBody requestBody = RequestBody.create(TEXT, "");
Request request = new Request.Builder().url(url).post(requestBody).build();
try {
String credential = Credentials.basic(username, password, Charsets.UTF_8);
Request authRequest = request.newBuilder().header("Authorization", credential).build();
Response response = httpClient.newCall(authRequest).execute();
checkStatusCode(response);
response.body().close();
this.username = username;
} catch (Exception e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
}
}
private String executeRequest(@NonNull Request.Builder requestB) throws GpodnetServiceException {
@ -576,36 +523,7 @@ public class GpodnetService {
return responseString;
}
private String executeRequestWithAuthentication(Request.Builder requestB,
String username, String password) throws GpodnetServiceException {
if (requestB == null || username == null || password == null) {
throw new IllegalArgumentException(
"request and credentials must not be null");
}
Request request = requestB.build();
String result = null;
ResponseBody body = null;
try {
String credential = Credentials.basic(username, password, Charset.forName("UTF-8"));
Request authRequest = request.newBuilder().header("Authorization", credential).build();
Response response = httpClient.newCall(authRequest).execute();
checkStatusCode(response);
body = response.body();
result = getStringFromResponseBody(body);
} catch (Exception e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
} finally {
if (body != null) {
body.close();
}
}
return result;
}
private String getStringFromResponseBody(@NonNull ResponseBody body)
throws GpodnetServiceException {
private String getStringFromResponseBody(@NonNull ResponseBody body) throws GpodnetServiceException {
ByteArrayOutputStream outputStream;
int contentLength = (int) body.contentLength();
if (contentLength > 0) {
@ -627,36 +545,31 @@ public class GpodnetService {
return outputStream.toString();
}
private void checkStatusCode(@NonNull Response response)
throws GpodnetServiceException {
private void checkStatusCode(@NonNull Response response) throws GpodnetServiceException {
int responseCode = response.code();
if (responseCode != HttpURLConnection.HTTP_OK) {
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new GpodnetServiceAuthenticationException("Wrong username or password");
} else {
throw new GpodnetServiceBadStatusCodeException("Bad response code: "
+ responseCode, responseCode);
throw new GpodnetServiceBadStatusCodeException("Bad response code: " + responseCode, responseCode);
}
}
}
private List<GpodnetPodcast> readPodcastListFromJSONArray(@NonNull JSONArray array)
throws JSONException {
List<GpodnetPodcast> result = new ArrayList<>(
array.length());
private List<GpodnetPodcast> readPodcastListFromJSONArray(@NonNull JSONArray array) throws JSONException {
List<GpodnetPodcast> result = new ArrayList<>(array.length());
for (int i = 0; i < array.length(); i++) {
result.add(readPodcastFromJSONObject(array.getJSONObject(i)));
}
return result;
}
private GpodnetPodcast readPodcastFromJSONObject(JSONObject object)
throws JSONException {
private GpodnetPodcast readPodcastFromJSONObject(JSONObject object) throws JSONException {
String url = object.getString("url");
String title;
Object titleObj = object.opt("title");
if (titleObj != null && titleObj instanceof String) {
if (titleObj instanceof String) {
title = (String) titleObj;
} else {
title = url;
@ -664,7 +577,7 @@ public class GpodnetService {
String description;
Object descriptionObj = object.opt("description");
if (descriptionObj != null && descriptionObj instanceof String) {
if (descriptionObj instanceof String) {
description = (String) descriptionObj;
} else {
description = "";
@ -673,49 +586,38 @@ public class GpodnetService {
int subscribers = object.getInt("subscribers");
Object logoUrlObj = object.opt("logo_url");
String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj
: null;
String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj : null;
if (logoUrl == null) {
Object scaledLogoUrl = object.opt("scaled_logo_url");
if (scaledLogoUrl != null && scaledLogoUrl instanceof String) {
if (scaledLogoUrl instanceof String) {
logoUrl = (String) scaledLogoUrl;
}
}
String website = null;
Object websiteObj = object.opt("website");
if (websiteObj != null && websiteObj instanceof String) {
if (websiteObj instanceof String) {
website = (String) websiteObj;
}
String mygpoLink = object.getString("mygpo_link");
String author = null;
Object authorObj = object.opt("author");
if (authorObj != null && authorObj instanceof String) {
if (authorObj instanceof String) {
author = (String) authorObj;
}
return new GpodnetPodcast(url,
title,
description,
subscribers,
logoUrl,
website,
mygpoLink,
author);
return new GpodnetPodcast(url, title, description, subscribers, logoUrl, website, mygpoLink, author);
}
private List<GpodnetDevice> readDeviceListFromJSONArray(@NonNull JSONArray array)
throws JSONException {
List<GpodnetDevice> result = new ArrayList<>(
array.length());
private List<GpodnetDevice> readDeviceListFromJSONArray(@NonNull JSONArray array) throws JSONException {
List<GpodnetDevice> result = new ArrayList<>(array.length());
for (int i = 0; i < array.length(); i++) {
result.add(readDeviceFromJSONObject(array.getJSONObject(i)));
}
return result;
}
private GpodnetDevice readDeviceFromJSONObject(JSONObject object)
throws JSONException {
private GpodnetDevice readDeviceFromJSONObject(JSONObject object) throws JSONException {
String id = object.getString("id");
String caption = object.getString("caption");
String type = object.getString("type");
@ -723,8 +625,8 @@ public class GpodnetService {
return new GpodnetDevice(id, caption, type, subscriptions);
}
private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject(
@NonNull JSONObject object) throws JSONException {
private SubscriptionChanges readSubscriptionChangesFromJSONObject(@NonNull JSONObject object)
throws JSONException {
List<String> added = new LinkedList<>();
JSONArray jsonAdded = object.getJSONArray("add");
@ -745,24 +647,44 @@ public class GpodnetService {
}
long timestamp = object.getLong("timestamp");
return new GpodnetSubscriptionChange(added, removed, timestamp);
return new SubscriptionChanges(added, removed, timestamp);
}
private GpodnetEpisodeActionGetResponse readEpisodeActionsFromJSONObject(
@NonNull JSONObject object) throws JSONException {
private EpisodeActionChanges readEpisodeActionsFromJSONObject(@NonNull JSONObject object)
throws JSONException {
List<GpodnetEpisodeAction> episodeActions = new ArrayList<>();
List<EpisodeAction> episodeActions = new ArrayList<>();
long timestamp = object.getLong("timestamp");
JSONArray jsonActions = object.getJSONArray("actions");
for(int i=0; i < jsonActions.length(); i++) {
for (int i = 0; i < jsonActions.length(); i++) {
JSONObject jsonAction = jsonActions.getJSONObject(i);
GpodnetEpisodeAction episodeAction = GpodnetEpisodeAction.readFromJSONObject(jsonAction);
if(episodeAction != null) {
EpisodeAction episodeAction = EpisodeAction.readFromJSONObject(jsonAction);
if (episodeAction != null) {
episodeActions.add(episodeAction);
}
}
return new GpodnetEpisodeActionGetResponse(episodeActions, timestamp);
return new EpisodeActionChanges(episodeActions, timestamp);
}
@Override
public void login() throws GpodnetServiceException {
authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
}
@Override
public SubscriptionChanges getSubscriptionChanges(long lastSync) throws GpodnetServiceException {
return getSubscriptionChanges(GpodnetPreferences.getDeviceID(), lastSync);
}
@Override
public UploadChangesResponse uploadSubscriptionChanges(List<String> addedFeeds, List<String> removedFeeds)
throws GpodnetServiceException {
return uploadChanges(GpodnetPreferences.getDeviceID(), addedFeeds, removedFeeds);
}
@Override
public void logout() {
}
}

View File

@ -0,0 +1,9 @@
package de.danoeh.antennapod.core.sync.gpoddernet;
public class GpodnetServiceAuthenticationException extends GpodnetServiceException {
private static final long serialVersionUID = 1L;
public GpodnetServiceAuthenticationException(String message) {
super(message);
}
}

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.gpoddernet;
package de.danoeh.antennapod.core.sync.gpoddernet;
class GpodnetServiceBadStatusCodeException extends GpodnetServiceException {
private static final long serialVersionUID = 1L;

View File

@ -0,0 +1,15 @@
package de.danoeh.antennapod.core.sync.gpoddernet;
import de.danoeh.antennapod.core.sync.model.SyncServiceException;
public class GpodnetServiceException extends SyncServiceException {
private static final long serialVersionUID = 1L;
public GpodnetServiceException(String message) {
super(message);
}
public GpodnetServiceException(Throwable e) {
super(e);
}
}

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.gpoddernet.model;
import androidx.annotation.NonNull;

View File

@ -1,7 +1,8 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.gpoddernet.model;
import androidx.collection.ArrayMap;
import de.danoeh.antennapod.core.sync.model.UploadChangesResponse;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.json.JSONArray;
@ -10,13 +11,7 @@ import org.json.JSONObject;
import java.util.Map;
public class GpodnetEpisodeActionPostResponse {
/**
* timestamp/ID that can be used for requesting changes since this upload.
*/
public final long timestamp;
public class GpodnetEpisodeActionPostResponse extends UploadChangesResponse {
/**
* URLs that should be updated. The key of the map is the original URL, the value of the map
* is the sanitized URL.
@ -24,7 +19,7 @@ public class GpodnetEpisodeActionPostResponse {
private final Map<String, String> updatedUrls;
private GpodnetEpisodeActionPostResponse(long timestamp, Map<String, String> updatedUrls) {
this.timestamp = timestamp;
super(timestamp);
this.updatedUrls = updatedUrls;
}

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.gpoddernet.model;
import androidx.annotation.NonNull;

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.gpoddernet.model;
import android.os.Parcel;
import android.os.Parcelable;

View File

@ -1,7 +1,9 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.gpoddernet.model;
import androidx.collection.ArrayMap;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.model.UploadChangesResponse;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -9,23 +11,17 @@ import org.json.JSONObject;
import java.util.Map;
/**
* Object returned by {@link de.danoeh.antennapod.core.gpoddernet.GpodnetService} in uploadChanges method.
* Object returned by {@link GpodnetService} in uploadChanges method.
*/
public class GpodnetUploadChangesResponse {
/**
* timestamp/ID that can be used for requesting changes since this upload.
*/
public final long timestamp;
public class GpodnetUploadChangesResponse extends UploadChangesResponse {
/**
* URLs that should be updated. The key of the map is the original URL, the value of the map
* is the sanitized URL.
*/
private final Map<String, String> updatedUrls;
public final Map<String, String> updatedUrls;
private GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) {
this.timestamp = timestamp;
public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) {
super(timestamp);
this.updatedUrls = updatedUrls;
}

View File

@ -1,9 +1,10 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.model;
import android.text.TextUtils;
import android.util.Log;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.util.DateUtils;
import org.json.JSONException;
import org.json.JSONObject;
@ -12,104 +13,65 @@ 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
}
public class EpisodeAction {
private static final String TAG = "EpisodeAction";
public static final Action NEW = Action.NEW;
public static final Action DOWNLOAD = Action.DOWNLOAD;
public static final Action PLAY = Action.PLAY;
public static final Action DELETE = Action.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) {
private EpisodeAction(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]);
return new Builder(podcast, episode, action)
.deviceId(deviceId)
.timestamp(new Date(Long.parseLong(fields[4])))
.started(Integer.parseInt(fields[5]))
.position(Integer.parseInt(fields[6]))
.total(Integer.parseInt(fields[7]))
.build();
} 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
* @param object JSON representation
* @return episode action object, or null if mandatory values are missing
*/
public static GpodnetEpisodeAction readFromJSONObject(JSONObject object) {
public static EpisodeAction readFromJSONObject(JSONObject object) {
String podcast = object.optString("podcast", null);
String episode = object.optString("episode", null);
String actionString = object.optString("action", null);
if(TextUtils.isEmpty(podcast) || TextUtils.isEmpty(episode) || TextUtils.isEmpty(actionString)) {
if (TextUtils.isEmpty(podcast) || TextUtils.isEmpty(episode) || TextUtils.isEmpty(actionString)) {
return null;
}
GpodnetEpisodeAction.Action action;
EpisodeAction.Action action;
try {
action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase());
action = EpisodeAction.Action.valueOf(actionString.toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
String deviceId = object.optString("device", "");
GpodnetEpisodeAction.Builder builder = new GpodnetEpisodeAction.Builder(podcast, episode, action)
.deviceId(deviceId);
EpisodeAction.Builder builder = new EpisodeAction.Builder(podcast, episode, action);
String utcTimestamp = object.optString("timestamp", null);
if(!TextUtils.isEmpty(utcTimestamp)) {
if (!TextUtils.isEmpty(utcTimestamp)) {
builder.timestamp(DateUtils.parse(utcTimestamp));
}
if(action == GpodnetEpisodeAction.Action.PLAY) {
if (action == EpisodeAction.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) {
if (started >= 0 && position > 0 && total > 0) {
builder
.started(started)
.position(position)
.total(total);
.started(started)
.position(position)
.total(total);
}
}
return builder.build();
@ -123,10 +85,6 @@ public class GpodnetEpisodeAction {
return this.episode;
}
public String getDeviceId() {
return this.deviceId;
}
public Action getAction() {
return this.action;
}
@ -169,17 +127,17 @@ public class GpodnetEpisodeAction {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof GpodnetEpisodeAction)) return false;
if (o == null || !(o instanceof EpisodeAction)) return false;
GpodnetEpisodeAction that = (GpodnetEpisodeAction) o;
EpisodeAction that = (EpisodeAction) o;
if (started != that.started) return false;
if (position != that.position) return false;
if (total != that.total) return false;
if (podcast != null ? !podcast.equals(that.podcast) : that.podcast != null) return false;
if (episode != null ? !episode.equals(that.episode) : that.episode != null) return false;
if (deviceId != null ? !deviceId.equals(that.deviceId) : that.deviceId != null)
return false;
//if (deviceId != null ? !deviceId.equals(that.deviceId) : that.deviceId != null)
// return false;
if (action != that.action) return false;
return !(timestamp != null ? !timestamp.equals(that.timestamp) : that.timestamp != null);
@ -189,7 +147,6 @@ public class GpodnetEpisodeAction {
public int hashCode() {
int result = podcast != null ? podcast.hashCode() : 0;
result = 31 * result + (episode != null ? episode.hashCode() : 0);
result = 31 * result + (deviceId != null ? deviceId.hashCode() : 0);
result = 31 * result + (action != null ? action.hashCode() : 0);
result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0);
result = 31 * result + started;
@ -198,17 +155,6 @@ public class GpodnetEpisodeAction {
return result;
}
public String writeToString() {
return this.podcast + "\t"
+ this.episode + "\t"
+ this.deviceId + "\t"
+ this.action + "\t"
+ this.timestamp.getTime() + "\t"
+ this.started + "\t"
+ this.position + "\t"
+ this.total;
}
/**
* Returns a JSON object representation of this object
*
@ -219,17 +165,16 @@ public class GpodnetEpisodeAction {
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));
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) {
} catch (JSONException e) {
Log.e(TAG, "writeToJSONObject(): " + e.getMessage());
return null;
}
@ -241,7 +186,6 @@ public class GpodnetEpisodeAction {
return "GpodnetEpisodeAction{" +
"podcast='" + podcast + '\'' +
", episode='" + episode + '\'' +
", deviceId='" + deviceId + '\'' +
", action=" + action +
", timestamp=" + timestamp +
", started=" + started +
@ -250,6 +194,10 @@ public class GpodnetEpisodeAction {
'}';
}
public enum Action {
NEW, DOWNLOAD, PLAY, DELETE
}
public static class Builder {
// mandatory
@ -258,7 +206,6 @@ public class GpodnetEpisodeAction {
private final Action action;
// optional
private String deviceId = "";
private Date timestamp;
private int started = -1;
private int position = -1;
@ -274,15 +221,6 @@ public class GpodnetEpisodeAction {
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;
@ -293,28 +231,28 @@ public class GpodnetEpisodeAction {
}
public Builder started(int seconds) {
if(action == Action.PLAY) {
if (action == Action.PLAY) {
this.started = seconds;
}
return this;
}
public Builder position(int seconds) {
if(action == Action.PLAY) {
if (action == Action.PLAY) {
this.position = seconds;
}
return this;
}
public Builder total(int seconds) {
if(action == Action.PLAY) {
if (action == Action.PLAY) {
this.total = seconds;
}
return this;
}
public GpodnetEpisodeAction build() {
return new GpodnetEpisodeAction(this);
public EpisodeAction build() {
return new EpisodeAction(this);
}
}

View File

@ -1,22 +1,21 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.model;
import androidx.annotation.NonNull;
import java.util.List;
public class GpodnetEpisodeActionGetResponse {
public class EpisodeActionChanges {
private final List<GpodnetEpisodeAction> episodeActions;
private final List<EpisodeAction> episodeActions;
private final long timestamp;
public GpodnetEpisodeActionGetResponse(@NonNull List<GpodnetEpisodeAction> episodeActions,
long timestamp) {
public EpisodeActionChanges(@NonNull List<EpisodeAction> episodeActions, long timestamp) {
this.episodeActions = episodeActions;
this.timestamp = timestamp;
}
public List<GpodnetEpisodeAction> getEpisodeActions() {
public List<EpisodeAction> getEpisodeActions() {
return this.episodeActions;
}
@ -26,7 +25,7 @@ public class GpodnetEpisodeActionGetResponse {
@Override
public String toString() {
return "GpodnetEpisodeActionGetResponse{" +
return "EpisodeActionGetResponse{" +
"episodeActions=" + episodeActions +
", timestamp=" + timestamp +
'}';

View File

@ -0,0 +1,20 @@
package de.danoeh.antennapod.core.sync.model;
import java.util.List;
public interface ISyncService {
void login() throws SyncServiceException;
SubscriptionChanges getSubscriptionChanges(long lastSync) throws SyncServiceException;
UploadChangesResponse uploadSubscriptionChanges(
List<String> addedFeeds, List<String> removedFeeds) throws SyncServiceException;
EpisodeActionChanges getEpisodeActionChanges(long lastSync) throws SyncServiceException;
UploadChangesResponse uploadEpisodeActions(List<EpisodeAction> queuedEpisodeActions)
throws SyncServiceException;
void logout() throws SyncServiceException;
}

View File

@ -1,17 +1,17 @@
package de.danoeh.antennapod.core.gpoddernet.model;
package de.danoeh.antennapod.core.sync.model;
import androidx.annotation.NonNull;
import java.util.List;
public class GpodnetSubscriptionChange {
public class SubscriptionChanges {
private final List<String> added;
private final List<String> removed;
private final long timestamp;
public GpodnetSubscriptionChange(@NonNull List<String> added,
@NonNull List<String> removed,
long timestamp) {
public SubscriptionChanges(@NonNull List<String> added,
@NonNull List<String> removed,
long timestamp) {
this.added = added;
this.removed = removed;
this.timestamp = timestamp;
@ -19,7 +19,7 @@ public class GpodnetSubscriptionChange {
@Override
public String toString() {
return "GpodnetSubscriptionChange [added=" + added.toString()
return "SubscriptionChange [added=" + added.toString()
+ ", removed=" + removed.toString() + ", timestamp="
+ timestamp + "]";
}

View File

@ -0,0 +1,13 @@
package de.danoeh.antennapod.core.sync.model;
public class SyncServiceException extends Exception {
private static final long serialVersionUID = 1L;
public SyncServiceException(String message) {
super(message);
}
public SyncServiceException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,13 @@
package de.danoeh.antennapod.core.sync.model;
public abstract class UploadChangesResponse {
/**
* timestamp/ID that can be used for requesting changes since this upload.
*/
public final long timestamp;
public UploadChangesResponse(long timestamp) {
this.timestamp = timestamp;
}
}

View File

@ -39,8 +39,6 @@ public class ClientConfig {
public static PlaybackServiceCallbacks playbackServiceCallbacks;
public static GpodnetCallbacks gpodnetCallbacks;
public static DBTasksCallbacks dbTasksCallbacks;
public static CastCallbacks castCallbacks;

View File

@ -1,2 +1 @@
include ':app'
include ':core'
include ':app', ':sync', ':core'