Skip to content

Commit 5669235

Browse files
symonbaikovparoj
authored andcommitted
Add entire-library caching support
Adds a new opt-in "Cache Entire Library" sync that pins every track in the server's library for offline playback and keeps the local cache in sync as the library grows. Implemented as a new SyncAdapter alongside the existing Starred / MostRecent / Playlist / Podcast adapters, so it reuses the existing sync schedule, wifi/battery gating, and master "Sync Enabled" switch. LibrarySyncAdapter walks the top-level index (honoring tag vs folder browsing through the inherited helper) and pins all songs via the existing SubsonicSyncAdapter.downloadRecursively() machinery. Songs removed server-side are unpinned via SyncUtil-tracked path lists, the same approach used by StarredSyncAdapter. Wires up: - LibrarySyncAdapter / LibrarySyncService / LibraryStubProvider - library_syncadapter.xml + AndroidManifest service & provider entries - provider.library resValue + SYNC_ACCOUNT_LIBRARY_AUTHORITY - PREFERENCES_KEY_SYNC_LIBRARY constant + "Cache Entire Library" CheckBoxPreference in Settings → Sync (default off) - ContentResolver.setSyncAutomatically + addPeriodicSync for the new authority in SubsonicFragmentActivity (matching the existing starred/mostrecent registration) - SyncUtil.getSyncedLibrary / setSyncedLibrary helpers and R.string.sync_new_library notification text Closes #73
1 parent 9a64f3e commit 5669235

12 files changed

Lines changed: 240 additions & 0 deletions

File tree

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ android {
1919
resValue 'string', 'provider.podcast', applicationId + ".podcasts.provider"
2020
resValue 'string', 'provider.starred', applicationId + ".starred.provider"
2121
resValue 'string', 'provider.recently_added', applicationId + ".mostrecent.provider"
22+
resValue 'string', 'provider.library', applicationId + ".library.provider"
2223
signingConfig signingConfigs.debug
2324
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2425
}

app/src/main/AndroidManifest.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,17 @@
172172
<meta-data android:name="android.content.SyncAdapter"
173173
android:resource="@xml/mostrecent_syncadapter" />
174174
</service>
175+
<service android:name="github.paroj.dsub2000.service.sync.LibrarySyncService"
176+
android:foregroundServiceType="dataSync|mediaPlayback"
177+
android:exported="true"
178+
android:process=":sync">
179+
180+
<intent-filter>
181+
<action android:name="android.content.SyncAdapter"/>
182+
</intent-filter>
183+
<meta-data android:name="android.content.SyncAdapter"
184+
android:resource="@xml/library_syncadapter" />
185+
</service>
175186

176187
<service android:name="github.paroj.dsub2000.service.HeadphoneListenerService"
177188
android:foregroundServiceType="mediaPlayback"
@@ -277,6 +288,11 @@
277288
android:label="@string/main.albums_newest"
278289
android:exported="false"
279290
android:syncable="true"/>
291+
<provider android:name="github.paroj.dsub2000.provider.LibraryStubProvider"
292+
android:authorities="@string/provider.library"
293+
android:label="@string/settings.sync_library"
294+
android:exported="false"
295+
android:syncable="true"/>
280296

281297
<meta-data android:name="android.app.default_searchable"
282298
android:value="github.paroj.dsub2000.activity.QueryReceiverActivity"/>

app/src/main/java/github/paroj/dsub2000/activity/SubsonicFragmentActivity.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,8 @@ protected Void doInBackground() throws Throwable {
928928
ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_STARRED_AUTHORITY, new Bundle(), 60L * syncInterval);
929929
ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_MOST_RECENT_AUTHORITY, (syncEnabled && prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT, false)));
930930
ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_MOST_RECENT_AUTHORITY, new Bundle(), 60L * syncInterval);
931+
ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_LIBRARY_AUTHORITY, (syncEnabled && prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_LIBRARY, false)));
932+
ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_LIBRARY_AUTHORITY, new Bundle(), 60L * syncInterval);
931933
return null;
932934
}
933935

app/src/main/java/github/paroj/dsub2000/fragments/SettingsFragment.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
9999
private CheckBoxPreference syncNotification;
100100
private CheckBoxPreference syncStarred;
101101
private CheckBoxPreference syncMostRecent;
102+
private CheckBoxPreference syncLibrary;
102103
private CheckBoxPreference replayGain;
103104
private ListPreference replayGainType;
104105
private Preference replayGainBump;
@@ -282,6 +283,7 @@ protected void onInitPreferences(PreferenceScreen preferenceScreen) {
282283
syncNotification = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION);
283284
syncStarred = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_STARRED);
284285
syncMostRecent = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT);
286+
syncLibrary = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_LIBRARY);
285287
replayGain = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN);
286288
replayGainType = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE);
287289
replayGainBump = this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP);
@@ -488,6 +490,7 @@ private void update() {
488490
syncNotification.setEnabled(true);
489491
syncStarred.setEnabled(true);
490492
syncMostRecent.setEnabled(true);
493+
syncLibrary.setEnabled(true);
491494
}
492495
} else {
493496
if(syncInterval.isEnabled()) {
@@ -496,6 +499,7 @@ private void update() {
496499
syncNotification.setEnabled(false);
497500
syncStarred.setEnabled(false);
498501
syncMostRecent.setEnabled(false);
502+
syncLibrary.setEnabled(false);
499503
}
500504
}
501505
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
This file is part of Subsonic.
3+
4+
Subsonic is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
Subsonic is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package github.paroj.dsub2000.provider;
19+
20+
import android.content.ContentProvider;
21+
import android.content.ContentValues;
22+
import android.database.Cursor;
23+
import android.net.Uri;
24+
25+
public class LibraryStubProvider extends ContentProvider {
26+
@Override
27+
public boolean onCreate() {
28+
return true;
29+
}
30+
31+
@Override
32+
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
33+
return null;
34+
}
35+
36+
@Override
37+
public String getType(Uri uri) {
38+
return "";
39+
}
40+
41+
@Override
42+
public Uri insert(Uri uri, ContentValues values) {
43+
return null;
44+
}
45+
46+
@Override
47+
public int delete(Uri uri, String selection, String[] selectionArgs) {
48+
return 0;
49+
}
50+
51+
@Override
52+
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
53+
return 0;
54+
}
55+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
This file is part of Subsonic.
3+
4+
Subsonic is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
Subsonic is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package github.paroj.dsub2000.service.sync;
19+
20+
import android.annotation.TargetApi;
21+
import android.content.Context;
22+
import android.util.Log;
23+
24+
import java.io.File;
25+
import java.util.ArrayList;
26+
27+
import github.paroj.dsub2000.R;
28+
import github.paroj.dsub2000.domain.Artist;
29+
import github.paroj.dsub2000.domain.Indexes;
30+
import github.paroj.dsub2000.domain.MusicDirectory;
31+
import github.paroj.dsub2000.util.FileUtil;
32+
import github.paroj.dsub2000.util.Notifications;
33+
import github.paroj.dsub2000.util.SyncUtil;
34+
import github.paroj.dsub2000.util.Util;
35+
36+
public class LibrarySyncAdapter extends SubsonicSyncAdapter {
37+
private static String TAG = LibrarySyncAdapter.class.getSimpleName();
38+
39+
public LibrarySyncAdapter(Context context, boolean autoInitialize) {
40+
super(context, autoInitialize);
41+
}
42+
@TargetApi(14)
43+
public LibrarySyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
44+
super(context, autoInitialize, allowParallelSyncs);
45+
}
46+
47+
@Override
48+
public void onExecuteSync(Context context, int instance) throws NetworkNotValidException {
49+
try {
50+
ArrayList<String> syncedList = new ArrayList<String>();
51+
52+
String musicFolderId = Util.getSelectedMusicFolderId(context, instance);
53+
Indexes indexes = musicService.getIndexes(musicFolderId, true, context, null);
54+
55+
// Build a virtual root directory whose children are all top-level artists
56+
// (and any loose top-level songs). downloadRecursively will then walk each
57+
// artist via getMusicDirectory(Entry), which already handles tag vs folder
58+
// browsing in the base class.
59+
MusicDirectory root = new MusicDirectory();
60+
for (Artist artist : indexes.getShortcuts()) {
61+
root.addChild(new MusicDirectory.Entry(artist));
62+
}
63+
for (Artist artist : indexes.getArtists()) {
64+
root.addChild(new MusicDirectory.Entry(artist));
65+
}
66+
if (indexes.getEntries() != null) {
67+
root.addChildren(indexes.getEntries());
68+
}
69+
70+
boolean updated = downloadRecursively(syncedList, root, context, true);
71+
72+
ArrayList<String> oldSyncedList = SyncUtil.getSyncedLibrary(context, instance);
73+
oldSyncedList.removeAll(syncedList);
74+
for (String path : oldSyncedList) {
75+
File saveFile = new File(path);
76+
FileUtil.unpinSong(context, saveFile);
77+
}
78+
79+
SyncUtil.setSyncedLibrary(syncedList, context, instance);
80+
if (updated) {
81+
Notifications.showSyncNotification(context, R.string.sync_new_library, null);
82+
}
83+
} catch (Exception e) {
84+
Log.e(TAG, "Failed to sync library for " + Util.getServerName(context, instance), e);
85+
}
86+
}
87+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
This file is part of Subsonic.
3+
4+
Subsonic is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
Subsonic is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package github.paroj.dsub2000.service.sync;
19+
20+
import android.app.Service;
21+
import android.content.Intent;
22+
import android.os.IBinder;
23+
24+
public class LibrarySyncService extends Service {
25+
private static LibrarySyncAdapter librarySyncAdapter;
26+
private static final Object syncLock = new Object();
27+
28+
@Override
29+
public void onCreate() {
30+
synchronized (syncLock) {
31+
if (librarySyncAdapter == null) {
32+
librarySyncAdapter = new LibrarySyncAdapter(getApplicationContext(), true);
33+
}
34+
}
35+
}
36+
37+
@Override
38+
public IBinder onBind(Intent intent) {
39+
return librarySyncAdapter.getSyncAdapterBinder();
40+
}
41+
}

app/src/main/java/github/paroj/dsub2000/util/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public final class Constants {
140140
public static final String PREFERENCES_KEY_SYNC_NOTIFICATION = "syncNotification";
141141
public static final String PREFERENCES_KEY_SYNC_STARRED = "syncStarred";
142142
public static final String PREFERENCES_KEY_SYNC_MOST_RECENT = "syncMostRecent";
143+
public static final String PREFERENCES_KEY_SYNC_LIBRARY = "syncLibrary";
143144
public static final String PREFERENCES_KEY_PAUSE_DISCONNECT = "pauseOnDisconnect";
144145
public static final String PREFERENCES_KEY_HIDE_WIDGET = "hideWidget";
145146
public static final String PREFERENCES_KEY_PODCASTS_ENABLED = "podcastsEnabled";
@@ -234,6 +235,7 @@ public final class Constants {
234235
public static final String SYNC_ACCOUNT_PODCAST_AUTHORITY = BuildConfig.APPLICATION_ID + ".podcasts.provider";
235236
public static final String SYNC_ACCOUNT_STARRED_AUTHORITY = BuildConfig.APPLICATION_ID + ".starred.provider";
236237
public static final String SYNC_ACCOUNT_MOST_RECENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".mostrecent.provider";
238+
public static final String SYNC_ACCOUNT_LIBRARY_AUTHORITY = BuildConfig.APPLICATION_ID + ".library.provider";
237239

238240
public static final String TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE";
239241

app/src/main/java/github/paroj/dsub2000/util/SyncUtil.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,21 @@ public static String getMostRecentSyncFile(Context context, int instance) {
174174
return "sync-most_recent-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
175175
}
176176

177+
// Entire library
178+
public static ArrayList<String> getSyncedLibrary(Context context, int instance) {
179+
ArrayList<String> list = FileUtil.deserializeCompressed(context, getLibrarySyncFile(context, instance), ArrayList.class);
180+
if(list == null) {
181+
list = new ArrayList<String>();
182+
}
183+
return list;
184+
}
185+
public static void setSyncedLibrary(ArrayList<String> syncedList, Context context, int instance) {
186+
FileUtil.serializeCompressed(context, syncedList, SyncUtil.getLibrarySyncFile(context, instance));
187+
}
188+
public static String getLibrarySyncFile(Context context, int instance) {
189+
return "sync-library-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
190+
}
191+
177192
public static String joinNames(List<String> names) {
178193
StringBuilder builder = new StringBuilder();
179194
for (String val : names) {

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
<string name="sync.new_playlists">New songs in playlists</string>
237237
<string name="sync.new_albums">New albums available</string>
238238
<string name="sync.new_starred">New starred songs available</string>
239+
<string name="sync.new_library">New library tracks cached</string>
239240

240241
<string name="starring_content_starred">Starred \"%s\"</string>
241242
<string name="starring_content_unstarred">Unstarred \"%s\"</string>
@@ -447,6 +448,8 @@
447448
<string name="settings.sync_most_recent_summary">Automatically cache newly added albums</string>
448449
<string name="settings.sync_starred">Sync Starred</string>
449450
<string name="settings.sync_starred_summary">Automatically cache songs, albums, and artists which are starred</string>
451+
<string name="settings.sync_library">Cache Entire Library</string>
452+
<string name="settings.sync_library_summary">Automatically cache every song in the library and keep the cache in sync with the server. Uses significant storage and bandwidth.</string>
450453
<string name="settings.sync_notification">Show Sync Notification</string>
451454
<string name="settings.sync_notification_summary">Show a notification after new media has been synced</string>
452455
<string name="settings.menu_options.title">Optional Menu Options</string>

0 commit comments

Comments
 (0)