aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml44
-rw-r--r--res/menu/select_playlist_context.xml11
-rw-r--r--res/menu/select_podcasts_context.xml10
-rw-r--r--res/values/arrays.xml23
-rw-r--r--res/values/strings.xml10
-rw-r--r--res/xml/authenticator.xml7
-rw-r--r--res/xml/playlists_syncadapter.xml8
-rw-r--r--res/xml/podcasts_syncadapter.xml8
-rw-r--r--res/xml/settings.xml23
-rw-r--r--src/github/daneren2005/dsub/activity/SettingsActivity.java30
-rw-r--r--src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java21
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectArtistFragment.java5
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java28
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java47
-rw-r--r--src/github/daneren2005/dsub/service/CachedMusicService.java5
-rw-r--r--src/github/daneren2005/dsub/service/DownloadFile.java14
-rw-r--r--src/github/daneren2005/dsub/service/MusicService.java4
-rw-r--r--src/github/daneren2005/dsub/service/OfflineMusicService.java5
-rw-r--r--src/github/daneren2005/dsub/service/RESTMusicService.java28
-rw-r--r--src/github/daneren2005/dsub/service/sync/AuthenticatorService.java90
-rw-r--r--src/github/daneren2005/dsub/service/sync/PlaylistStubProvider.java61
-rw-r--r--src/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java68
-rw-r--r--src/github/daneren2005/dsub/service/sync/PlaylistSyncService.java48
-rw-r--r--src/github/daneren2005/dsub/service/sync/PodcastStubProvider.java61
-rw-r--r--src/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java99
-rw-r--r--src/github/daneren2005/dsub/service/sync/PodcastSyncService.java48
-rw-r--r--src/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java116
-rw-r--r--src/github/daneren2005/dsub/util/Constants.java9
-rw-r--r--src/github/daneren2005/dsub/util/SyncUtil.java134
-rw-r--r--src/github/daneren2005/dsub/util/Util.java16
-rw-r--r--src/github/daneren2005/dsub/view/PlaylistView.java9
-rw-r--r--src/github/daneren2005/dsub/view/PodcastChannelView.java15
-rw-r--r--src/github/daneren2005/dsub/view/UpdateView.java5
33 files changed, 1074 insertions, 36 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 62869d96..58e18fef 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -15,7 +15,11 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
-
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+ <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
+ <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+
+
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
@@ -63,6 +67,34 @@
<service android:name="github.daneren2005.dsub.service.DownloadServiceImpl"
android:label="Subsonic Download Service"/>
+ <service android:name="github.daneren2005.dsub.service.sync.AuthenticatorService">
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator"/>
+ </intent-filter>
+
+ <meta-data android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator" />
+ </service>
+ <service android:name=".service.sync.PlaylistSyncService"
+ android:exported="true"
+ android:process=":sync">
+
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter"/>
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/playlists_syncadapter" />
+ </service>
+ <service android:name=".service.sync.PodcastSyncService"
+ android:exported="true"
+ android:process=":sync">
+
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter"/>
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/podcasts_syncadapter" />
+ </service>
<receiver android:name="github.daneren2005.dsub.receiver.MediaButtonIntentReceiver">
<intent-filter android:priority="999">
@@ -120,6 +152,16 @@
<provider android:name="github.daneren2005.dsub.provider.DSubSearchProvider"
android:authorities="github.daneren2005.dsub.provider.DSubSearchProvider"/>
+ <provider android:name=".service.sync.PlaylistStubProvider"
+ android:authorities="github.daneren2005.dsub.playlists.provider"
+ android:label="Playlists"
+ android:exported="false"
+ android:syncable="true"/>
+ <provider android:name=".service.sync.PodcastStubProvider"
+ android:authorities="github.daneren2005.dsub.podcasts.provider"
+ android:label="Podcasts"
+ android:exported="false"
+ android:syncable="true"/>
<meta-data android:name="android.app.default_searchable"
android:value="github.daneren2005.dsub.activity.QueryReceiverActivity"/>
diff --git a/res/menu/select_playlist_context.xml b/res/menu/select_playlist_context.xml
index 0ec94e02..7cdc0c79 100644
--- a/res/menu/select_playlist_context.xml
+++ b/res/menu/select_playlist_context.xml
@@ -22,10 +22,13 @@
android:title="@string/common.download"
/>
- <item
- android:id="@+id/playlist_menu_pin"
- android:title="@string/common.pin"
- />
+ <item
+ android:id="@+id/playlist_menu_sync"
+ android:title="@string/menu.keep_synced"/>
+
+ <item
+ android:id="@+id/playlist_menu_stop_sync"
+ android:title="@string/menu.stop_sync"/>
<item
android:id="@+id/playlist_update_info"
diff --git a/res/menu/select_podcasts_context.xml b/res/menu/select_podcasts_context.xml
index c9785b92..3a2a1c60 100644
--- a/res/menu/select_podcasts_context.xml
+++ b/res/menu/select_podcasts_context.xml
@@ -6,5 +6,13 @@
android:title="@string/common.info"/>
<item
android:id="@+id/podcast_channel_delete"
- android:title="@string/common.delete"/>
+ android:title="@string/common.delete"/>
+
+ <item
+ android:id="@+id/podcast_menu_sync"
+ android:title="@string/menu.keep_synced"/>
+
+ <item
+ android:id="@+id/podcast_menu_stop_sync"
+ android:title="@string/menu.stop_sync"/>
</menu> \ No newline at end of file
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index 16288149..4b8caa96 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -194,4 +194,25 @@
<item>@string/settings.video_flash</item>
</string-array>
-</resources>
+ <string-array name="syncIntervalValues">
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ <item>120</item>
+ <item>240</item>
+ <item>360</item>
+ <item>720</item>
+ <item>1440</item>
+ </string-array>
+ <string-array name="syncIntervalNames">
+ <item>15 Minutes</item>
+ <item>30 Minutes</item>
+ <item>1 Hour</item>
+ <item>2 Hours</item>
+ <item>3 Hours</item>
+ <item>6 Hours</item>
+ <item>12 Hours</item>
+ <item>Daily</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index b19c7b7d..a6de6759 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -75,6 +75,8 @@
<string name="menu.set_timer">Set Timer</string>
<string name="menu.check_podcasts">Check For New Episodes</string>
<string name="menu.add_podcast">Add Channel</string>
+ <string name="menu.keep_synced">Keep Synced</string>
+ <string name="menu.stop_sync">Stop syncing</string>
<string name="playlist.label">Playlists</string>
<string name="playlist.update_info">Update Information</string>
@@ -309,7 +311,7 @@
<string name="settings.media_button_summary">Respond to phone, headset and Bluetooth media buttons</string>
<string name="settings.screen_lit_title">Keep screen on</string>
<string name="settings.screen_lit_summary">Keeping the screen on while downloading improves download speed.</string>
- <string name="settings.playlist_title">Playlists</string>
+ <string name="settings.playlist_title">Play</string>
<string name="settings.playlist_random_size_title">Random Size</string>
<string name="settings.buffer_length">Buffer Length (0 = when fully cached)</string>
<string name="settings.sleep_timer_title">Sleep Timer</string>
@@ -348,6 +350,12 @@
<string name="settings.podcasts_enabled_summary">Whether or not to display the podcast listing in the pull out drawer</string>
<string name="settings.bookmarks_enabled">Bookmarks Enabled</string>
<string name="settings.bookmarks_enabled_summary">Whether or not to display the bookmarks listing in the pull out drawer</string>
+ <string name="settings.sync_title">Sync</string>
+ <string name="settings.sync_enabled">Sync Enabled</string>
+ <string name="settings.sync_enabled_summary">Whether or not playlists or podcasts are periodically checked for changes</string>
+ <string name="settings.sync_interval">Sync Interval</string>
+ <string name="settings.sync_wifi">Sync on Wifi only</string>
+ <string name="settings.sync_wifi_summary">Only sync while on wifi</string>
<string name="shuffle.title">Shuffle By</string>
<string name="shuffle.startYear">Start Year:</string>
diff --git a/res/xml/authenticator.xml b/res/xml/authenticator.xml
new file mode 100644
index 00000000..ce62b117
--- /dev/null
+++ b/res/xml/authenticator.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<account-authenticator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="subsonic.org"
+ android:icon="@drawable/launch"
+ android:smallIcon="@drawable/launch"
+ android:label="@string/common.appname"/> \ No newline at end of file
diff --git a/res/xml/playlists_syncadapter.xml b/res/xml/playlists_syncadapter.xml
new file mode 100644
index 00000000..418f3f49
--- /dev/null
+++ b/res/xml/playlists_syncadapter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="github.daneren2005.dsub.playlists.provider"
+ android:accountType="subsonic.org"
+ android:userVisible="true"
+ android:supportsUploading="false"
+ android:allowParallelSyncs="false"
+ android:isAlwaysSyncable="true"/> \ No newline at end of file
diff --git a/res/xml/podcasts_syncadapter.xml b/res/xml/podcasts_syncadapter.xml
new file mode 100644
index 00000000..21f421f6
--- /dev/null
+++ b/res/xml/podcasts_syncadapter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="github.daneren2005.dsub.podcasts.provider"
+ android:accountType="subsonic.org"
+ android:userVisible="true"
+ android:supportsUploading="false"
+ android:allowParallelSyncs="false"
+ android:isAlwaysSyncable="true"/> \ No newline at end of file
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
index 99233785..6756a5d8 100644
--- a/res/xml/settings.xml
+++ b/res/xml/settings.xml
@@ -172,6 +172,29 @@
</PreferenceCategory>
<PreferenceCategory
+ android:title="@string/settings.sync_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.sync_enabled"
+ android:summary="@string/settings.sync_enabled_summary"
+ android:key="syncEnabled"
+ android:defaultValue="true"/>
+
+ <ListPreference
+ android:title="@string/settings.sync_interval"
+ android:key="syncInterval"
+ android:defaultValue="60"
+ android:entryValues="@array/syncIntervalValues"
+ android:entries="@array/syncIntervalNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.sync_wifi"
+ android:summary="@string/settings.sync_wifi_summary"
+ android:key="syncWifi"
+ android:defaultValue="true"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
android:title="@string/settings.other_title">
<CheckBoxPreference
diff --git a/src/github/daneren2005/dsub/activity/SettingsActivity.java b/src/github/daneren2005/dsub/activity/SettingsActivity.java
index c723cca5..f8fed602 100644
--- a/src/github/daneren2005/dsub/activity/SettingsActivity.java
+++ b/src/github/daneren2005/dsub/activity/SettingsActivity.java
@@ -19,6 +19,8 @@
package github.daneren2005.dsub.activity;
import android.annotation.TargetApi;
+import android.accounts.Account;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -78,6 +80,7 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
private PreferenceCategory serversCategory;
private EditTextPreference chatRefreshRate;
private ListPreference videoPlayer;
+ private ListPreference syncInterval;
private int serverCount = 3;
private SharedPreferences settings;
@@ -107,6 +110,7 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
addServerPreference = (Preference) findPreference(Constants.PREFERENCES_KEY_SERVER_ADD);
chatRefreshRate = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH);
videoPlayer = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
+ syncInterval = (ListPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_INTERVAL);
settings = Util.getPreferences(this);
serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 3);
@@ -154,6 +158,31 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
editor.commit();
serverSettings.put(instance, new ServerSettings(instance));
+
+ return true;
+ }
+ });
+
+ findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Boolean syncEnabled = (Boolean) newValue;
+
+ Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE);
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled);
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, syncEnabled);
+
+ return true;
+ }
+ });
+ syncInterval.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Integer syncInterval = Integer.parseInt(((String) newValue));
+
+ Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, new Bundle(), 60L * syncInterval);
return true;
}
@@ -254,6 +283,7 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
bufferLength.setSummary(bufferLength.getText() + " seconds");
chatRefreshRate.setSummary(chatRefreshRate.getText());
videoPlayer.setSummary(videoPlayer.getEntry());
+ syncInterval.setSummary(syncInterval.getEntry());
for (ServerSettings ss : serverSettings.values()) {
ss.update();
}
diff --git a/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
index 08acf591..66d4d4ee 100644
--- a/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
+++ b/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
@@ -18,6 +18,10 @@
*/
package github.daneren2005.dsub.activity;
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
@@ -96,6 +100,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity {
if("".equals(fragmentType) || fragmentType == null) {
// Initial startup stuff
loadSettings();
+ createAccount();
}
currentFragment.setPrimaryFragment(true);
@@ -436,6 +441,22 @@ public class SubsonicFragmentActivity extends SubsonicActivity {
}
}
+ private void createAccount() {
+ AccountManager accountManager = (AccountManager) this.getSystemService(ACCOUNT_SERVICE);
+ Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE);
+ accountManager.addAccountExplicitly(account, null, null);
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ boolean syncEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_ENABLED, true);
+ int syncInterval = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_SYNC_INTERVAL, "60"));
+
+ // Make sync run every hour
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval);
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, syncEnabled);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, new Bundle(), 60L * syncInterval);
+ }
+
private void showInfoDialog() {
if (!infoDialogDisplayed) {
infoDialogDisplayed = true;
diff --git a/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java b/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java
index ed629a15..81fc362b 100644
--- a/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java
+++ b/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java
@@ -1,7 +1,9 @@
package github.daneren2005.dsub.fragments;
import android.content.Intent;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.ContextMenu;
@@ -67,6 +69,9 @@ public class SelectArtistFragment extends SubsonicFragment implements AdapterVie
artistList = (ListView) rootView.findViewById(R.id.fragment_list);
artistList.setOnItemClickListener(this);
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ artistList.setFastScrollAlwaysVisible(true);
+ }
folderButtonParent = inflater.inflate(R.layout.select_artist_header, artistList, false);
folderName = (TextView) folderButtonParent.findViewById(R.id.select_artist_folder_2);
diff --git a/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
index 01c9d938..e6a49db1 100644
--- a/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
+++ b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
@@ -2,10 +2,8 @@ package github.daneren2005.dsub.fragments;
import android.app.AlertDialog;
import android.content.DialogInterface;
-import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
-import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -23,6 +21,7 @@ import github.daneren2005.dsub.service.MusicService;
import github.daneren2005.dsub.service.MusicServiceFactory;
import github.daneren2005.dsub.service.OfflineException;
import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.SyncUtil;
import github.daneren2005.dsub.util.BackgroundTask;
import github.daneren2005.dsub.util.CacheCleaner;
import github.daneren2005.dsub.util.Constants;
@@ -106,6 +105,14 @@ public class SelectPlaylistFragment extends SubsonicFragment implements AdapterV
}
else {
inflater.inflate(R.menu.select_playlist_context, menu);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Playlist playlist = (Playlist) list.getItemAtPosition(info.position);
+ if(SyncUtil.isSyncedPlaylist(context, playlist.getId())) {
+ menu.removeItem(R.id.playlist_menu_sync);
+ } else {
+ menu.removeItem(R.id.playlist_menu_stop_sync);
+ }
}
}
@@ -125,8 +132,11 @@ public class SelectPlaylistFragment extends SubsonicFragment implements AdapterV
case R.id.playlist_menu_download:
downloadPlaylist(playlist.getId(), playlist.getName(), false, true, false, false, true);
break;
- case R.id.playlist_menu_pin:
- downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
+ case R.id.playlist_menu_sync:
+ syncPlaylist(playlist);
+ break;
+ case R.id.playlist_menu_stop_sync:
+ stopSyncPlaylist(playlist);
break;
case R.id.playlist_menu_play_now:
fragment = new SelectDirectoryFragment();
@@ -217,6 +227,7 @@ public class SelectPlaylistFragment extends SubsonicFragment implements AdapterV
protected Void doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(context);
musicService.deletePlaylist(playlist.getId(), context, null);
+ SyncUtil.removeSyncedPlaylist(context, playlist.getId());
return null;
}
@@ -306,4 +317,13 @@ public class SelectPlaylistFragment extends SubsonicFragment implements AdapterV
.setNegativeButton(R.string.common_cancel, null)
.show();
}
+
+ private void syncPlaylist(Playlist playlist) {
+ SyncUtil.addSyncedPlaylist(context, playlist.getId());
+ downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
+ }
+
+ private void stopSyncPlaylist(Playlist playlist) {
+ SyncUtil.removeSyncedPlaylist(context, playlist.getId());
+ }
}
diff --git a/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
index 143ff229..8b83a0c5 100644
--- a/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
+++ b/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
@@ -21,9 +21,6 @@ package github.daneren2005.dsub.fragments;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.text.SpannableString;
-import android.text.method.LinkMovementMethod;
-import android.text.util.Linkify;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
@@ -36,11 +33,13 @@ import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PodcastChannel;
import github.daneren2005.dsub.service.MusicService;
import github.daneren2005.dsub.service.MusicServiceFactory;
import github.daneren2005.dsub.service.OfflineException;
import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.SyncUtil;
import github.daneren2005.dsub.util.BackgroundTask;
import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.LoadingTask;
@@ -134,6 +133,14 @@ public class SelectPodcastsFragment extends SubsonicFragment implements AdapterV
if(!Util.isOffline(context)) {
android.view.MenuInflater inflater = context.getMenuInflater();
inflater.inflate(R.menu.select_podcasts_context, menu);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ PodcastChannel podcast = (PodcastChannel) podcastListView.getItemAtPosition(info.position);
+ if(SyncUtil.isSyncedPodcast(context, podcast.getId())) {
+ menu.removeItem(R.id.podcast_menu_sync);
+ } else {
+ menu.removeItem(R.id.podcast_menu_stop_sync);
+ }
}
}
@@ -147,6 +154,12 @@ public class SelectPodcastsFragment extends SubsonicFragment implements AdapterV
PodcastChannel channel = (PodcastChannel) podcastListView.getItemAtPosition(info.position);
switch (menuItem.getItemId()) {
+ case R.id.podcast_menu_sync:
+ syncPodcast(channel);
+ break;
+ case R.id.podcast_menu_stop_sync:
+ stopSyncPodcast(channel);
+ break;
case R.id.podcast_channel_info:
displayPodcastInfo(channel);
break;
@@ -305,6 +318,7 @@ public class SelectPodcastsFragment extends SubsonicFragment implements AdapterV
protected Void doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(context);
musicService.deletePodcastChannel(channel.getId(), context, null);
+ stopSyncPodcast(channel);
return null;
}
@@ -330,4 +344,31 @@ public class SelectPodcastsFragment extends SubsonicFragment implements AdapterV
}
});
}
+
+ private void syncPodcast(final PodcastChannel podcast) {
+ new LoadingTask<MusicDirectory>(context, false) {
+ @Override
+ protected MusicDirectory doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getPodcastEpisodes(true, podcast.getId(), context, this);
+ }
+
+ @Override
+ protected void done(MusicDirectory result) {
+ List<String> existingEpisodes = new ArrayList<String>();
+ for(MusicDirectory.Entry entry: result.getChildren()) {
+ String id = entry.getId();
+ if(id != null) {
+ existingEpisodes.add(entry.getId());
+ }
+ }
+
+ SyncUtil.addSyncedPodcast(context, podcast.getId(), existingEpisodes);
+ }
+ }.execute();
+ }
+
+ private void stopSyncPodcast(PodcastChannel podcast) {
+ SyncUtil.removeSyncedPodcast(context, podcast.getId());
+ }
}
diff --git a/src/github/daneren2005/dsub/service/CachedMusicService.java b/src/github/daneren2005/dsub/service/CachedMusicService.java
index 5a7737de..4d7ddb7e 100644
--- a/src/github/daneren2005/dsub/service/CachedMusicService.java
+++ b/src/github/daneren2005/dsub/service/CachedMusicService.java
@@ -429,6 +429,11 @@ public class CachedMusicService implements MusicService {
public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
return musicService.processOfflineSyncs(context, progressListener);
}
+
+ @Override
+ public void setInstance(Integer instance) throws Exception {
+ musicService.setInstance(instance);
+ }
private String getCacheName(Context context, String name, String id) {
String s = Util.getRestUrl(context, null) + id;
diff --git a/src/github/daneren2005/dsub/service/DownloadFile.java b/src/github/daneren2005/dsub/service/DownloadFile.java
index c2cffdef..51649b62 100644
--- a/src/github/daneren2005/dsub/service/DownloadFile.java
+++ b/src/github/daneren2005/dsub/service/DownloadFile.java
@@ -99,13 +99,21 @@ public class DownloadFile {
}
public synchronized void download() {
- FileUtil.createDirectoryForParent(saveFile);
+ preDownload();
+ downloadTask.start();
+ }
+ public synchronized void downloadNow() {
+ preDownload();
+ downloadTask.execute();
+
+ }
+ private void preDownload() {
+ FileUtil.createDirectoryForParent(saveFile);
failedDownload = false;
if(!partialFile.exists()) {
bitRate = Util.getMaxBitrate(context);
}
- downloadTask = new DownloadTask();
- downloadTask.start();
+ downloadTask = new DownloadTask();
}
public synchronized void cancelDownload() {
diff --git a/src/github/daneren2005/dsub/service/MusicService.java b/src/github/daneren2005/dsub/service/MusicService.java
index 8d204530..b5b37354 100644
--- a/src/github/daneren2005/dsub/service/MusicService.java
+++ b/src/github/daneren2005/dsub/service/MusicService.java
@@ -146,4 +146,6 @@ public interface MusicService {
void deleteBookmark(String id, Context context, ProgressListener progressListener) throws Exception;
int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception;
-} \ No newline at end of file
+
+ void setInstance(Integer instance) throws Exception;
+}
diff --git a/src/github/daneren2005/dsub/service/OfflineMusicService.java b/src/github/daneren2005/dsub/service/OfflineMusicService.java
index ba798c09..3386af70 100644
--- a/src/github/daneren2005/dsub/service/OfflineMusicService.java
+++ b/src/github/daneren2005/dsub/service/OfflineMusicService.java
@@ -660,6 +660,11 @@ public class OfflineMusicService extends RESTMusicService {
public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
throw new OfflineException("Offline scrobble cached can not be processes while in offline mode");
}
+
+ @Override
+ public void setInstance(Integer instance) throws Exception{
+ throw new OfflineException("Offline servers only have one instance");
+ }
private void listFilesRecursively(File parent, List<File> children) {
for (File file : FileUtil.listMediaFiles(parent)) {
diff --git a/src/github/daneren2005/dsub/service/RESTMusicService.java b/src/github/daneren2005/dsub/service/RESTMusicService.java
index 22abacb1..c96f3569 100644
--- a/src/github/daneren2005/dsub/service/RESTMusicService.java
+++ b/src/github/daneren2005/dsub/service/RESTMusicService.java
@@ -129,6 +129,7 @@ public class RESTMusicService implements MusicService {
private String redirectFrom;
private String redirectTo;
private final ThreadSafeClientConnManager connManager;
+ private Integer instance;
public RESTMusicService() {
@@ -245,7 +246,7 @@ public class RESTMusicService implements MusicService {
}
private String getCachedIndexesFilename(Context context, String musicFolderId) {
- String s = Util.getRestUrl(context, null) + musicFolderId;
+ String s = getRestUrl(context, null) + musicFolderId;
return "indexes-" + Math.abs(s.hashCode()) + ".ser";
}
@@ -596,7 +597,7 @@ public class RESTMusicService implements MusicService {
return bitmap;
}
- String url = Util.getRestUrl(context, "getCoverArt");
+ String url = getRestUrl(context, "getCoverArt");
InputStream in = null;
try {
@@ -640,7 +641,7 @@ public class RESTMusicService implements MusicService {
@Override
public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception {
- String url = Util.getRestUrl(context, "stream");
+ String url = getRestUrl(context, "stream");
// Set socket read timeout. Note: The timeout increases as the offset gets larger. This is
// to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server.
@@ -674,7 +675,7 @@ public class RESTMusicService implements MusicService {
@Override
public String getVideoUrl(int maxBitrate, Context context, String id) {
- StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "videoPlayer"));
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "videoPlayer"));
builder.append("&id=").append(id);
builder.append("&maxBitRate=").append(maxBitrate);
builder.append("&autoplay=true");
@@ -686,7 +687,7 @@ public class RESTMusicService implements MusicService {
@Override
public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
- StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "stream"));
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "stream"));
builder.append("&id=").append(id);
if(!"raw".equals(format)) {
checkServerVersion(context, "1.9", "Video streaming not supported.");
@@ -703,7 +704,7 @@ public class RESTMusicService implements MusicService {
public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
checkServerVersion(context, "1.9", "HLS video streaming not supported.");
- StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "hls"));
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "hls"));
builder.append("&id=").append(id);
if(bitRate > 0) {
builder.append("&bitRate=").append(bitRate);
@@ -1113,6 +1114,11 @@ public class RESTMusicService implements MusicService {
return id;
}
+
+ @Override
+ public void setInstance(Integer instance) throws Exception {
+ this.instance = instance;
+ }
private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception {
return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList());
@@ -1130,7 +1136,7 @@ public class RESTMusicService implements MusicService {
progressListener.updateProgress(R.string.service_connecting);
}
- String url = Util.getRestUrl(context, method);
+ String url = getRestUrl(context, method);
return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener);
}
@@ -1316,4 +1322,12 @@ public class RESTMusicService implements MusicService {
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
return networkInfo == null ? -1 : networkInfo.getType();
}
+
+ private String getRestUrl(Context context, String method) {
+ if(instance == null) {
+ return Util.getRestUrl(context, method);
+ } else {
+ return Util.getRestUrl(context, method, instance);
+ }
+ }
}
diff --git a/src/github/daneren2005/dsub/service/sync/AuthenticatorService.java b/src/github/daneren2005/dsub/service/sync/AuthenticatorService.java
new file mode 100644
index 00000000..ae12c88e
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/AuthenticatorService.java
@@ -0,0 +1,90 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.service.sync;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.NetworkErrorException;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class AuthenticatorService extends Service {
+ private SubsonicAuthenticator authenticator;
+
+ @Override
+ public void onCreate() {
+ authenticator = new SubsonicAuthenticator(this);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return authenticator.getIBinder();
+
+ }
+
+ private class SubsonicAuthenticator extends AbstractAccountAuthenticator {
+ public SubsonicAuthenticator(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+ return null;
+ }
+
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ return null;
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
+ return null;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/sync/PlaylistStubProvider.java b/src/github/daneren2005/dsub/service/sync/PlaylistStubProvider.java
new file mode 100644
index 00000000..e7053ceb
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/PlaylistStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.service.sync;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PlaylistStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return new String();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java b/src/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java
new file mode 100644
index 00000000..a709384b
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java
@@ -0,0 +1,68 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+*/
+
+package github.daneren2005.dsub.service.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.Log;
+
+import java.util.List;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 8/28/13.
+*/
+
+public class PlaylistSyncAdapter extends SubsonicSyncAdapter {
+ private static String TAG = PlaylistSyncAdapter.class.getSimpleName();
+
+ public PlaylistSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+ @TargetApi(14)
+ public PlaylistSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ }
+
+ @Override
+ public void onExecuteSync(Context context, int instance) {
+ String serverName = Util.getServerName(context, instance);
+ List<String> playlistList = SyncUtil.getSyncedPlaylists(context, instance);
+ for(int i = 0; i < playlistList.size(); i++) {
+ String id = playlistList.get(i);
+ try {
+ MusicDirectory playlist = musicService.getPlaylist(true, id, serverName, context, null);
+
+ for(MusicDirectory.Entry entry: playlist.getChildren()) {
+ DownloadFile file = new DownloadFile(context, entry, true);
+ while(!file.isSaved() && !file.isFailedMax()) {
+ file.downloadNow();
+ }
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to get playlist for " + serverName);
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/sync/PlaylistSyncService.java b/src/github/daneren2005/dsub/service/sync/PlaylistSyncService.java
new file mode 100644
index 00000000..80ec5564
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/PlaylistSyncService.java
@@ -0,0 +1,48 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.service.sync;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PlaylistSyncService extends Service {
+ private static PlaylistSyncAdapter playlistSyncAdapter;
+ private static final Object syncLock = new Object();
+
+ @Override
+ public void onCreate() {
+ synchronized (syncLock) {
+ if(playlistSyncAdapter == null) {
+ playlistSyncAdapter = new PlaylistSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return playlistSyncAdapter.getSyncAdapterBinder();
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/sync/PodcastStubProvider.java b/src/github/daneren2005/dsub/service/sync/PodcastStubProvider.java
new file mode 100644
index 00000000..cff319aa
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/PodcastStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.service.sync;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PodcastStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return new String();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java b/src/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java
new file mode 100644
index 00000000..3b67125e
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java
@@ -0,0 +1,99 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.service.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.SyncUtil.SyncSet;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PodcastSyncAdapter extends SubsonicSyncAdapter {
+ private static String TAG = PodcastSyncAdapter.class.getSimpleName();
+
+ public PodcastSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+ @TargetApi(14)
+ public PodcastSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ }
+
+ @Override
+ public void onExecuteSync(Context context, int instance) {
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ ArrayList<SyncSet> podcastList = SyncUtil.getSyncedPodcasts(context, instance);
+
+ try {
+ // Only refresh if syncs exist (implies a server where supported)
+ if(podcastList.size() > 0) {
+ // Refresh podcast listings before syncing
+ musicService.refreshPodcasts(context, null);
+ }
+
+ boolean updated = false;
+ for(int i = 0; i < podcastList.size(); i++) {
+ SyncSet set = podcastList.get(i);
+ String id = set.id;
+ List<String> existingEpisodes = set.synced;
+ try {
+ MusicDirectory podcasts = musicService.getPodcastEpisodes(true, id, context, null);
+
+ for(MusicDirectory.Entry entry: podcasts.getChildren()) {
+ // Make sure podcast is valid and not already synced
+ if(entry.getId() != null && "completed".equals(((PodcastEpisode)entry).getStatus()) && !existingEpisodes.contains(entry.getId())) {
+ DownloadFile file = new DownloadFile(context, entry, true);
+ while(!file.isSaved() && !file.isFailedMax()) {
+ file.downloadNow();
+ }
+ // Only add if actualy downloaded correctly
+ if(file.isSaved()) {
+ existingEpisodes.add(entry.getId());
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to get podcasts for " + id + " on " + Util.getServerName(context, instance));
+ }
+ }
+
+ // Make sure there are is at least one change before re-syncing
+ if(updated) {
+ FileUtil.serialize(context, podcastList, SyncUtil.getPodcastSyncFile(context, instance));
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to get podcasts for " + Util.getServerName(context, instance));
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/sync/PodcastSyncService.java b/src/github/daneren2005/dsub/service/sync/PodcastSyncService.java
new file mode 100644
index 00000000..ff9ef5f1
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/PodcastSyncService.java
@@ -0,0 +1,48 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.service.sync;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PodcastSyncService extends Service {
+ private static PodcastSyncAdapter podcastSyncAdapter;
+ private static final Object syncLock = new Object();
+
+ @Override
+ public void onCreate() {
+ synchronized (syncLock) {
+ if(podcastSyncAdapter == null) {
+ podcastSyncAdapter = new PodcastSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return podcastSyncAdapter.getSyncAdapterBinder();
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java b/src/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java
new file mode 100644
index 00000000..1de9712d
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java
@@ -0,0 +1,116 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.service.sync;
+
+import android.accounts.Account;
+import android.annotation.TargetApi;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.SyncResult;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+
+import github.daneren2005.dsub.service.RESTMusicService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 9/6/13.
+ */
+
+public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter {
+ private static final String TAG = SubsonicSyncAdapter.class.getSimpleName();
+ protected RESTMusicService musicService = new RESTMusicService();
+ private Context context;
+
+ public SubsonicSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ this.context = context;
+ }
+ @TargetApi(14)
+ public SubsonicSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ this.context = context;
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+
+ // Don't try to sync if no network!
+ if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) {
+ Log.w(TAG, "Not running sync, not connected to network");
+ return;
+ }
+
+ // Make sure battery > x% or is charging
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ Intent batteryStatus = context.registerReceiver(null, intentFilter);
+ if(batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1) != BatteryManager.BATTERY_STATUS_CHARGING) {
+ int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+
+ if((level / (float)scale) > 0.15) {
+ Log.w(TAG, "Not running sync, battery too low");
+ return;
+ }
+ }
+
+ // Check if user wants to only sync on wifi
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) {
+ if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ executeSync(context);
+ } else {
+ Log.w(TAG, "Not running sync, not connected to wifi");
+ }
+ } else {
+ executeSync(context);
+ }
+ }
+
+ private void executeSync(Context context) {
+ String className = this.getClass().getSimpleName();
+ Log.i(TAG, "Running sync for " + className);
+ long start = System.currentTimeMillis();
+ int servers = Util.getServerCount(context);
+ for(int i = 1; i <= servers; i++) {
+ try {
+ musicService.setInstance(i);
+ onExecuteSync(context, i);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e);
+ }
+ }
+
+ Log.i(TAG, className + " executed in " + (System.currentTimeMillis() - start) + " ms");
+ }
+ public void onExecuteSync(Context context, int instance) {
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/Constants.java b/src/github/daneren2005/dsub/util/Constants.java
index d24e2341..00b6508b 100644
--- a/src/github/daneren2005/dsub/util/Constants.java
+++ b/src/github/daneren2005/dsub/util/Constants.java
@@ -119,6 +119,9 @@ public final class Constants {
public static final String PREFERENCES_KEY_CHAT_ENABLED = "chatEnabled";
public static final String PREFERENCES_KEY_VIDEO_PLAYER = "videoPlayer";
public static final String PREFERENCES_KEY_CONTROL_MODE = "remoteControlMode";
+ public static final String PREFERENCES_KEY_SYNC_ENABLED = "syncEnabled";
+ public static final String PREFERENCES_KEY_SYNC_INTERVAL = "syncInterval";
+ public static final String PREFERENCES_KEY_SYNC_WIFI = "syncWifi";
public static final String PREFERENCES_KEY_PAUSE_DISCONNECT = "pauseOnDisconnect";
public static final String PREFERENCES_KEY_HIDE_WIDGET = "hideWidget";
public static final String PREFERENCES_KEY_PODCASTS_ENABLED = "podcastsEnabled";
@@ -152,6 +155,12 @@ public final class Constants {
public static final String OFFLINE_SYNC_NAME = "github.daneren2005.dsub.offline";
public static final String OFFLINE_SYNC_DEFAULT = "syncDefaults";
+ // Account prefs
+ public static final String SYNC_ACCOUNT_NAME = "Subsonic Account";
+ public static final String SYNC_ACCOUNT_TYPE = "subsonic.org";
+ public static final String SYNC_ACCOUNT_PLAYLIST_AUTHORITY = "github.daneren2005.dsub.playlists.provider";
+ public static final String SYNC_ACCOUNT_PODCAST_AUTHORITY = "github.daneren2005.dsub.podcasts.provider";
+
// Number of free trial days for non-licensed servers.
public static final int FREE_TRIAL_DAYS = 30;
diff --git a/src/github/daneren2005/dsub/util/SyncUtil.java b/src/github/daneren2005/dsub/util/SyncUtil.java
new file mode 100644
index 00000000..463baf9e
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/SyncUtil.java
@@ -0,0 +1,134 @@
+package github.daneren2005.dsub.util;
+
+import android.content.Context;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Scott on 11/24/13.
+ */
+public final class SyncUtil {
+ private static String TAG = SyncUtil.class.getSimpleName();
+ private static ArrayList<String> syncedPlaylists;
+ private static ArrayList<SyncSet> syncedPodcasts;
+
+ // Playlist sync
+ public static boolean isSyncedPlaylist(Context context, String playlistId) {
+ if(syncedPlaylists == null) {
+ syncedPlaylists = getSyncedPlaylists(context);
+ }
+ return syncedPlaylists.contains(playlistId);
+ }
+ public static ArrayList<String> getSyncedPlaylists(Context context) {
+ return getSyncedPlaylists(context, Util.getActiveServer(context));
+ }
+ public static ArrayList<String> getSyncedPlaylists(Context context, int instance) {
+ ArrayList<String> playlists = FileUtil.deserialize(context, getPlaylistSyncFile(context, instance), ArrayList.class);
+ if(playlists == null) {
+ playlists = new ArrayList<String>();
+ }
+ return playlists;
+ }
+ public static void addSyncedPlaylist(Context context, String playlistId) {
+ String playlistFile = getPlaylistSyncFile(context);
+ ArrayList<String> playlists = getSyncedPlaylists(context);
+ if(!playlists.contains(playlistId)) {
+ playlists.add(playlistId);
+ }
+ FileUtil.serialize(context, playlists, playlistFile);
+ syncedPlaylists = playlists;
+ }
+ public static void removeSyncedPlaylist(Context context, String playlistId) {
+ String playlistFile = getPlaylistSyncFile(context);
+ ArrayList<String> playlists = getSyncedPlaylists(context);
+ if(playlists.contains(playlistId)) {
+ playlists.remove(playlistId);
+ FileUtil.serialize(context, playlists, playlistFile);
+ syncedPlaylists = playlists;
+ }
+ }
+ public static String getPlaylistSyncFile(Context context) {
+ int instance = Util.getActiveServer(context);
+ return getPlaylistSyncFile(context, instance);
+ }
+ public static String getPlaylistSyncFile(Context context, int instance) {
+ return "sync-playlist-" + (Util.getRestUrl(context, null, instance)).hashCode() + ".ser";
+ }
+
+ // Podcast sync
+ public static boolean isSyncedPodcast(Context context, String podcastId) {
+ if(syncedPodcasts == null) {
+ syncedPodcasts = getSyncedPodcasts(context);
+ }
+ return syncedPodcasts.contains(new SyncSet(podcastId));
+ }
+ public static ArrayList<SyncSet> getSyncedPodcasts(Context context) {
+ return getSyncedPodcasts(context, Util.getActiveServer(context));
+ }
+ public static ArrayList<SyncSet> getSyncedPodcasts(Context context, int instance) {
+ ArrayList<SyncSet> podcasts = FileUtil.deserialize(context, getPodcastSyncFile(context, instance), ArrayList.class);
+ if(podcasts == null) {
+ podcasts = new ArrayList<SyncSet>();
+ }
+ return podcasts;
+ }
+ public static void addSyncedPodcast(Context context, String podcastId, List<String> synced) {
+ String podcastFile = getPodcastSyncFile(context);
+ ArrayList<SyncSet> podcasts = getSyncedPodcasts(context);
+ SyncSet set = new SyncSet(podcastId, synced);
+ if(!podcasts.contains(set)) {
+ podcasts.add(set);
+ }
+ FileUtil.serialize(context, podcasts, podcastFile);
+ syncedPodcasts = podcasts;
+ }
+ public static void removeSyncedPodcast(Context context, String podcastId) {
+ String podcastFile = getPodcastSyncFile(context);
+ ArrayList<SyncSet> podcasts = getSyncedPodcasts(context);
+ SyncSet set = new SyncSet(podcastId);
+ if(podcasts.contains(set)) {
+ podcasts.remove(set);
+ FileUtil.serialize(context, podcasts, podcastFile);
+ syncedPodcasts = podcasts;
+ }
+ }
+ public static String getPodcastSyncFile(Context context) {
+ int instance = Util.getActiveServer(context);
+ return getPodcastSyncFile(context, instance);
+ }
+ public static String getPodcastSyncFile(Context context, int instance) {
+ return "sync-podcast-" + (Util.getRestUrl(context, null, instance)).hashCode() + ".ser";
+ }
+
+ public static class SyncSet implements Serializable {
+ public String id;
+ public List<String> synced;
+
+ protected SyncSet() {
+
+ }
+ public SyncSet(String id) {
+ this.id = id;
+ }
+ public SyncSet(String id, List<String> synced) {
+ this.id = id;
+ this.synced = synced;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if(obj instanceof SyncSet) {
+ return this.id.equals(((SyncSet)obj).id);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/Util.java b/src/github/daneren2005/dsub/util/Util.java
index 83e8deee..101aa8b5 100644
--- a/src/github/daneren2005/dsub/util/Util.java
+++ b/src/github/daneren2005/dsub/util/Util.java
@@ -80,6 +80,8 @@ import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -104,7 +106,7 @@ public final class Util {
public static final String AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
public static final String AVRCP_METADATA_CHANGED = "com.android.music.metachanged";
-
+
private static boolean hasFocus = false;
private static boolean pauseFocus = false;
private static boolean lowerFocus = false;
@@ -334,11 +336,17 @@ public final class Util {
}
public static String getRestUrl(Context context, String method) {
- StringBuilder builder = new StringBuilder();
-
SharedPreferences prefs = getPreferences(context);
-
int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ return getRestUrl(context, method, prefs, instance);
+ }
+ public static String getRestUrl(Context context, String method, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return getRestUrl(context, method, prefs, instance);
+ }
+ public static String getRestUrl(Context context, String method, SharedPreferences prefs, int instance) {
+ StringBuilder builder = new StringBuilder();
+
String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
diff --git a/src/github/daneren2005/dsub/view/PlaylistView.java b/src/github/daneren2005/dsub/view/PlaylistView.java
index c75f8ad7..8b5e8ca9 100644
--- a/src/github/daneren2005/dsub/view/PlaylistView.java
+++ b/src/github/daneren2005/dsub/view/PlaylistView.java
@@ -26,9 +26,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.Playlist;
-import github.daneren2005.dsub.util.FileUtil;
-import github.daneren2005.dsub.util.Util;
-import java.io.File;
+import github.daneren2005.dsub.util.SyncUtil;
/**
* Used to display albums in a {@code ListView}.
@@ -63,4 +61,9 @@ public class PlaylistView extends UpdateView {
this.playlist = (Playlist) obj;
titleView.setText(playlist.getName());
}
+
+ @Override
+ protected void updateBackground() {
+ pinned = SyncUtil.isSyncedPlaylist(context, playlist.getId());
+ }
}
diff --git a/src/github/daneren2005/dsub/view/PodcastChannelView.java b/src/github/daneren2005/dsub/view/PodcastChannelView.java
index 9ad78d1c..aff97524 100644
--- a/src/github/daneren2005/dsub/view/PodcastChannelView.java
+++ b/src/github/daneren2005/dsub/view/PodcastChannelView.java
@@ -26,6 +26,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.util.SyncUtil;
import github.daneren2005.dsub.util.FileUtil;
import java.io.File;
@@ -66,6 +67,18 @@ public class PodcastChannelView extends UpdateView {
@Override
protected void updateBackground() {
- exists = file.exists();
+ if(SyncUtil.isSyncedPodcast(context, channel.getId())) {
+ if(exists) {
+ shaded = false;
+ exists = false;
+ }
+ pinned = true;
+ } else if(file.exists()) {
+ if(pinned) {
+ shaded = false;
+ pinned = false;
+ }
+ exists = true;
+ }
}
}
diff --git a/src/github/daneren2005/dsub/view/UpdateView.java b/src/github/daneren2005/dsub/view/UpdateView.java
index 05b17417..f247137d 100644
--- a/src/github/daneren2005/dsub/view/UpdateView.java
+++ b/src/github/daneren2005/dsub/view/UpdateView.java
@@ -48,6 +48,7 @@ public class UpdateView extends LinearLayout {
protected ImageView moreButton;
protected boolean exists = false;
+ protected boolean pinned = false;
protected boolean shaded = false;
protected boolean starred = false;
protected boolean isStarred = false;
@@ -169,9 +170,9 @@ public class UpdateView extends LinearLayout {
}
protected void update() {
if(moreButton != null) {
- if(exists) {
+ if(exists || pinned) {
if(!shaded) {
- moreButton.setImageResource(R.drawable.download_cached);
+ moreButton.setImageResource(exists ? R.drawable.download_cached : R.drawable.download_pinned);
shaded = true;
}
} else {