Upload changes instead of whole subscription list

This commit is contained in:
daniel oeh 2013-09-02 15:13:00 +02:00
parent 730ba3cc26
commit 1f594ad311
10 changed files with 308 additions and 30 deletions

View File

@ -375,7 +375,8 @@
<activity
android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"
android:configChanges="orientation"
android:label="@string/gpodnet_auth_label">
android:label="@string/gpodnet_auth_label"
android:screenOrientation="portrait">
<intent-filter>
<action android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"/>
<category android:name="android.intent.category.DEFAULT"/>

View File

@ -25,20 +25,20 @@
android:textColor="?android:attr/textColorSecondary"
android:layout_margin="16dp"/>
<EditText
android:id="@+id/etxtDeviceID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/gpodnetauth_device_deviceID"
android:layout_below="@id/txtvDescription"
android:layout_margin="8dp"/>
<EditText
android:id="@+id/etxtCaption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/gpodnetauth_device_caption"
android:layout_below="@id/etxtDeviceID"
android:layout_below="@id/txtvDescription"
android:layout_margin="8dp"/>
<EditText
android:id="@+id/etxtDeviceID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/gpodnetauth_device_deviceID"
android:layout_below="@id/etxtCaption"
android:layout_margin="8dp"/>
<Button
@ -47,7 +47,7 @@
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentRight="true"
android:layout_below="@id/etxtCaption"
android:layout_below="@id/etxtDeviceID"
android:text="@string/gpodnetauth_device_butCreateNewDevice"/>
<TextView
@ -89,10 +89,10 @@
android:id="@+id/butChooseExistingDevice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txtvChooseExistingDevice"
android:layout_alignParentRight="true"
android:layout_margin="16dp"
android:text="@string/gpodnetauth_device_butChoose"/>
android:text="@string/gpodnetauth_device_butChoose"
android:layout_alignTop="@+id/spinnerChooseDevice"
android:layout_alignRight="@+id/txtvChooseExistingDevice"/>
<Spinner
android:id="@+id/spinnerChooseDevice"

View File

@ -2,6 +2,7 @@ package de.danoeh.antennapod.activity.gpoddernet;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
@ -19,6 +20,7 @@ import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.service.GpodnetSyncService;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@ -32,6 +34,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class GpodnetAuthenticationActivity extends ActionBarActivity {
private static final String TAG = "GpodnetAuthenticationActivity";
private static final String CURRENT_STEP = "current_step";
private ViewFlipper viewFlipper;
private static final int STEP_DEFAULT = -1;
@ -78,6 +82,10 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity {
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
}
private void setupLoginView(View view) {
final EditText username = (EditText) view.findViewById(R.id.etxtUsername);
final EditText password = (EditText) view.findViewById(R.id.etxtPassword);
@ -167,7 +175,7 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity {
if (gpodnetDevices != null) {
List<String> deviceNames = new ArrayList<String>();
for (GpodnetDevice device : gpodnetDevices) {
deviceNames.add(device.getId());
deviceNames.add(device.getCaption());
}
spinnerDevices.setAdapter(new ArrayAdapter<String>(GpodnetAuthenticationActivity.this,
android.R.layout.simple_spinner_dropdown_item, deviceNames));
@ -244,6 +252,7 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity {
}
});
deviceID.setText(generateDeviceID());
chooseDevice.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -254,6 +263,18 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity {
});
}
private String generateDeviceID() {
final int DEVICE_ID_LENGTH = 10;
StringBuilder buffer = new StringBuilder(DEVICE_ID_LENGTH);
SecureRandom random = new SecureRandom();
for (int i = 0; i < DEVICE_ID_LENGTH; i++) {
buffer.append(random.nextInt(10));
}
return buffer.toString();
}
private boolean checkDeviceIDText(EditText deviceID, TextView txtvError, List<GpodnetDevice> devices) {
String text = deviceID.getText().toString();
if (text.length() == 0) {

View File

@ -1,10 +1,7 @@
package de.danoeh.antennapod.gpoddernet;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange;
import de.danoeh.antennapod.gpoddernet.model.GpodnetTag;
import de.danoeh.antennapod.gpoddernet.model.*;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
@ -30,6 +27,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@ -387,6 +385,55 @@ public class GpodnetService {
}
}
/**
* Updates the subscription list of a specific device.
* <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.gpoddernet.model.GpodnetUploadChangesResponse}
* for details.
* @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null.
* @throws de.danoeh.antennapod.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there
* is an authentication error.
*/
public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection<String> added,
Collection<String> removed) throws GpodnetServiceException {
if (username == null || deviceId == null || added == null || removed == null) {
throw new IllegalArgumentException(
"Username, device ID, added and removed must not be null");
}
try {
URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/subscriptions/%s/%s.json", username, deviceId), null);
final JSONObject requestObject = new JSONObject();
requestObject.put("add", new JSONArray(added));
requestObject.put("remove", new JSONArray(removed));
HttpPost request = new HttpPost(uri);
StringEntity entity = new StringEntity(requestObject.toString(), "UTF-8");
request.setEntity(entity);
final String response = executeRequest(request);
return GpodnetUploadChangesResponse.fromJSONObject(response);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalStateException(e);
} catch (JSONException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new IllegalStateException(e);
}
}
/**
* Returns all subscription changes of a specific device.
* <p/>

View File

@ -0,0 +1,56 @@
package de.danoeh.antennapod.gpoddernet.model;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* Object returned by {@link de.danoeh.antennapod.gpoddernet.GpodnetService} in uploadChanges method.
*/
public class GpodnetUploadChangesResponse {
/**
* timestamp/ID that can be used for requesting changes since this upload.
*/
public final long timestamp;
/**
* URLs that should be updated. The key of the map is the original URL, the value of the map
* is the sanitized URL.
*/
public final Map<String, String> updatedUrls;
public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) {
this.timestamp = timestamp;
this.updatedUrls = updatedUrls;
}
/**
* Creates a new GpodnetUploadChangesResponse-object from a JSON object that was
* returned by an uploadChanges call.
*
* @throws org.json.JSONException If the method could not parse the JSONObject.
*/
public static GpodnetUploadChangesResponse fromJSONObject(String objectString) throws JSONException {
final JSONObject object = new JSONObject(objectString);
final long timestamp = object.getLong("timestamp");
Map<String, String> updatedUrls = new HashMap<String, String>();
JSONArray urls = object.getJSONArray("update_urls");
for (int i = 0; i < urls.length(); i++) {
JSONArray urlPair = urls.getJSONArray(i);
updatedUrls.put(urlPair.getString(0), urlPair.getString(1));
}
return new GpodnetUploadChangesResponse(timestamp, updatedUrls);
}
@Override
public String toString() {
return "GpodnetUploadChangesResponse{" +
"timestamp=" + timestamp +
", updatedUrls=" + updatedUrls +
'}';
}
}

View File

@ -3,6 +3,12 @@ package de.danoeh.antennapod.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.service.GpodnetSyncService;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
/**
* Manages preferences for accessing gpodder.net service
@ -17,11 +23,17 @@ public class GpodnetPreferences {
public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added";
public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed";
private static String username;
private static String password;
private static String deviceID;
private static ReentrantLock feedListLock = new ReentrantLock();
private static Set<String> addedFeeds;
private static Set<String> removedFeeds;
/**
* Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
*/
@ -33,13 +45,16 @@ public class GpodnetPreferences {
return PodcastApp.getInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
private static void ensurePreferencesLoaded() {
private static synchronized void ensurePreferencesLoaded() {
if (!preferencesLoaded) {
SharedPreferences prefs = getPreferences();
username = prefs.getString(PREF_GPODNET_USERNAME, null);
password = prefs.getString(PREF_GPODNET_PASSWORD, null);
deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0);
addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, ""));
removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, ""));
preferencesLoaded = true;
}
}
@ -56,6 +71,12 @@ public class GpodnetPreferences {
editor.commit();
}
private static void writePreference(String key, Collection<String> value) {
SharedPreferences.Editor editor = getPreferences().edit();
editor.putString(key, writeListToString(value));
editor.commit();
}
public static String getUsername() {
ensurePreferencesLoaded();
return username;
@ -96,6 +117,65 @@ public class GpodnetPreferences {
writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp);
}
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.sendActionUploadIntent(PodcastApp.getInstance());
}
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.sendActionUploadIntent(PodcastApp.getInstance());
}
public static Set<String> getAddedFeedsCopy() {
ensurePreferencesLoaded();
Set<String> copy = new HashSet<String>();
feedListLock.lock();
copy.addAll(addedFeeds);
feedListLock.unlock();
return copy;
}
public static void removeAddedFeeds(Set<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<String>();
feedListLock.lock();
copy.addAll(removedFeeds);
feedListLock.unlock();
return copy;
}
public static void removeRemovedFeeds(Set<String> removed) {
ensurePreferencesLoaded();
removedFeeds.removeAll(removed);
writePreference(PREF_SYNC_REMOVED, removedFeeds);
}
/**
* Returns true if device ID, username and password have a non-null value
*/
@ -110,4 +190,21 @@ public class GpodnetPreferences {
setDeviceID(null);
setLastSyncTimestamp(0);
}
private static Set<String> readListFromString(String s) {
Set<String> result = new HashSet<String>();
for (String item : s.split(" ")) {
result.add(item);
}
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();
}
}

View File

@ -15,15 +15,15 @@ import de.danoeh.antennapod.gpoddernet.GpodnetService;
import de.danoeh.antennapod.gpoddernet.GpodnetServiceAuthenticationException;
import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange;
import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.storage.DBReader;
import de.danoeh.antennapod.storage.DBTasks;
import de.danoeh.antennapod.storage.DownloadRequestException;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.storage.*;
import de.danoeh.antennapod.util.NetworkUtils;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
/**
* Synchronizes local subscriptions with gpodder.net service. The service should be started with an ACTION_UPLOAD_CHANGES,
@ -71,8 +71,8 @@ public class GpodnetSyncService extends Service {
new Thread() {
@Override
public void run() {
uploadChanges();
downloadChanges();
uploadChanges();
}
}.start();
}
@ -135,14 +135,27 @@ public class GpodnetSyncService extends Service {
try {
if (AppConfig.DEBUG) Log.d(TAG, "Uploading subscription list");
GpodnetService service = tryLogin();
List<String> subscriptions = DBReader.getFeedListDownloadUrls(GpodnetSyncService.this);
if (AppConfig.DEBUG) Log.d(TAG, "Uploading subscriptions: " + subscriptions.toString());
Set<String> added = GpodnetPreferences.getAddedFeedsCopy();
Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy();
service.uploadSubscriptions(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), subscriptions);
if (AppConfig.DEBUG) Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
added.toString(), removed.toString()));
GpodnetUploadChangesResponse response = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed);
if (AppConfig.DEBUG) Log.d(TAG, "Upload subscriptions response: " + response.toString());
GpodnetPreferences.removeAddedFeeds(added);
GpodnetPreferences.removeRemovedFeeds(removed);
DBWriter.updateFeedDownloadURLs(GpodnetSyncService.this, response.updatedUrls).get();
} catch (GpodnetServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
stopSelf();

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@ -17,6 +18,7 @@ import android.preference.PreferenceManager;
import android.util.Log;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
import de.danoeh.antennapod.service.GpodnetSyncService;
import de.danoeh.antennapod.service.PlaybackService;
@ -173,7 +175,7 @@ public class DBWriter {
adapter.removeFeed(feed);
adapter.close();
GpodnetSyncService.sendActionUploadIntent(context);
GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
}
@ -617,7 +619,7 @@ public class DBWriter {
adapter.setCompleteFeed(feed);
adapter.close();
GpodnetSyncService.sendActionUploadIntent(context);
GpodnetPreferences.addAddedFeed(feed.getDownload_url());
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
});
@ -720,6 +722,26 @@ public class DBWriter {
});
}
/**
* Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed
* and the value is the updated URL
* */
public static Future<?> updateFeedDownloadURLs(final Context context, final Map<String, String> urls) {
return dbExec.submit(new Runnable() {
@Override
public void run() {
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
for (String key : urls.keySet()) {
if (AppConfig.DEBUG) Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key));
adapter.setFeedDownloadUrl(key, urls.get(key));
}
adapter.close();
}
});
}
private static boolean itemListContains(List<FeedItem> items, long itemId) {
for (FeedItem item : items) {
if (item.getId() == itemId) {

View File

@ -425,6 +425,15 @@ public class PodDBAdapter {
db.endTransaction();
}
/**
* Updates the download URL of a Feed.
*/
public void setFeedDownloadUrl(String original, String updated) {
ContentValues values = new ContentValues();
values.put(KEY_DOWNLOAD_URL, updated);
db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original});
}
public void setFeedItemlist(List<FeedItem> items) {
db.beginTransaction();
for (FeedItem item : items) {

View File

@ -1,12 +1,14 @@
package instrumentationTest.de.test.antennapod.gpodnet;
import android.test.AndroidTestCase;
import android.util.Log;
import de.danoeh.antennapod.gpoddernet.GpodnetService;
import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.gpoddernet.model.GpodnetTag;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@ -49,6 +51,16 @@ public class GPodnetServiceTest extends AndroidTestCase {
service.uploadSubscriptions(USER, "radio", l);
}
public void testUploadChanges() throws GpodnetServiceException {
authenticate();
String[] URLS = {"http://bitsundso.de/feed", "http://gamesundso.de/feed", "http://cre.fm/feed/mp3/", "http://freakshow.fm/feed/m4a/"};
List<String> subscriptions = Arrays.asList(URLS[0], URLS[1]);
List<String> removed = Arrays.asList(URLS[0]);
List<String> added = Arrays.asList(URLS[2], URLS[3]);
service.uploadSubscriptions(USER, "radio", subscriptions);
service.uploadChanges(USER, "radio", added, removed);
}
public void testGetSubscriptionChanges() throws GpodnetServiceException {
authenticate();
service.getSubscriptionChanges(USER, "radio", 1362322610L);