diff options
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 {
|