1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.pump.db;
18 
19 import android.Manifest;
20 import android.content.ContentResolver;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.RequiresPermission;
24 import androidx.annotation.UiThread;
25 import androidx.collection.ArraySet;
26 
27 import com.android.pump.concurrent.Executors;
28 import com.android.pump.util.Clog;
29 
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.List;
35 import java.util.Set;
36 import java.util.concurrent.Executor;
37 import java.util.concurrent.atomic.AtomicBoolean;
38 
39 @UiThread
40 public class MediaDb implements MediaProvider {
41     private static final String TAG = Clog.tag(MediaDb.class);
42 
43     private final AtomicBoolean mLoaded = new AtomicBoolean();
44 
45     private final Executor mExecutor;
46 
47     private final AudioStore mAudioStore;
48     private final VideoStore mVideoStore;
49     private final DataProvider mDataProvider;
50 
51     private final List<Audio> mAudios = new ArrayList<>();
52     private final List<Artist> mArtists = new ArrayList<>();
53     private final List<Album> mAlbums = new ArrayList<>();
54     private final List<Genre> mGenres = new ArrayList<>();
55     private final List<Playlist> mPlaylists = new ArrayList<>();
56 
57     private final List<Movie> mMovies = new ArrayList<>();
58     private final List<Series> mSeries = new ArrayList<>();
59     private final List<Episode> mEpisodes = new ArrayList<>();
60     private final List<Other> mOthers = new ArrayList<>();
61 
62     private final Set<UpdateCallback> mAudioUpdateCallbacks = new ArraySet<>();
63     private final Set<UpdateCallback> mArtistUpdateCallbacks = new ArraySet<>();
64     private final Set<UpdateCallback> mAlbumUpdateCallbacks = new ArraySet<>();
65     private final Set<UpdateCallback> mGenreUpdateCallbacks = new ArraySet<>();
66     private final Set<UpdateCallback> mPlaylistUpdateCallbacks = new ArraySet<>();
67 
68     private final Set<UpdateCallback> mMovieUpdateCallbacks = new ArraySet<>();
69     private final Set<UpdateCallback> mSeriesUpdateCallbacks = new ArraySet<>();
70     private final Set<UpdateCallback> mEpisodeUpdateCallbacks = new ArraySet<>();
71     private final Set<UpdateCallback> mOtherUpdateCallbacks = new ArraySet<>();
72 
73     public interface UpdateCallback {
onItemsInserted(int index, int count)74         void onItemsInserted(int index, int count);
onItemsUpdated(int index, int count)75         void onItemsUpdated(int index, int count);
onItemsRemoved(int index, int count)76         void onItemsRemoved(int index, int count);
77     }
78 
MediaDb(@onNull ContentResolver contentResolver, @NonNull DataProvider dataProvider, @NonNull Executor executor)79     public MediaDb(@NonNull ContentResolver contentResolver, @NonNull DataProvider dataProvider,
80             @NonNull Executor executor) {
81         Clog.i(TAG, "MediaDb(" + contentResolver + ", " + dataProvider + ", " + executor + ")");
82         mDataProvider = dataProvider;
83         mExecutor = executor;
84 
85         mAudioStore = new AudioStore(contentResolver, new AudioStore.ChangeListener() {
86             @Override
87             public void onAudiosAdded(@NonNull Collection<Audio> audios) {
88                 Executors.uiThreadExecutor().execute(() -> addAudios(audios));
89             }
90 
91             @Override
92             public void onArtistsAdded(@NonNull Collection<Artist> artists) {
93                 Executors.uiThreadExecutor().execute(() -> addArtists(artists));
94             }
95 
96             @Override
97             public void onAlbumsAdded(@NonNull Collection<Album> albums) {
98                 Executors.uiThreadExecutor().execute(() -> addAlbums(albums));
99             }
100 
101             @Override
102             public void onGenresAdded(@NonNull Collection<Genre> genres) {
103                 Executors.uiThreadExecutor().execute(() -> addGenres(genres));
104             }
105 
106             @Override
107             public void onPlaylistsAdded(@NonNull Collection<Playlist> playlists) {
108                 Executors.uiThreadExecutor().execute(() -> addPlaylists(playlists));
109             }
110         }, this);
111 
112         mVideoStore = new VideoStore(contentResolver, new VideoStore.ChangeListener() {
113             @Override
114             public void onMoviesAdded(@NonNull Collection<Movie> movies) {
115                 Executors.uiThreadExecutor().execute(() -> addMovies(movies));
116             }
117 
118             @Override
119             public void onSeriesAdded(@NonNull Collection<Series> series) {
120                 Executors.uiThreadExecutor().execute(() -> addSeries(series));
121             }
122 
123             @Override
124             public void onEpisodesAdded(@NonNull Collection<Episode> episodes) {
125                 Executors.uiThreadExecutor().execute(() -> addEpisodes(episodes));
126             }
127 
128             @Override
129             public void onOthersAdded(@NonNull Collection<Other> others) {
130                 Executors.uiThreadExecutor().execute(() -> addOthers(others));
131             }
132         }, this);
133     }
134 
addAudioUpdateCallback(@onNull UpdateCallback callback)135     public void addAudioUpdateCallback(@NonNull UpdateCallback callback) {
136         addUpdateCallback(mAudioUpdateCallbacks, callback);
137     }
138 
removeAudioUpdateCallback(@onNull UpdateCallback callback)139     public void removeAudioUpdateCallback(@NonNull UpdateCallback callback) {
140         removeUpdateCallback(mAudioUpdateCallbacks, callback);
141     }
142 
addArtistUpdateCallback(@onNull UpdateCallback callback)143     public void addArtistUpdateCallback(@NonNull UpdateCallback callback) {
144         addUpdateCallback(mArtistUpdateCallbacks, callback);
145     }
146 
removeArtistUpdateCallback(@onNull UpdateCallback callback)147     public void removeArtistUpdateCallback(@NonNull UpdateCallback callback) {
148         removeUpdateCallback(mArtistUpdateCallbacks, callback);
149     }
150 
addAlbumUpdateCallback(@onNull UpdateCallback callback)151     public void addAlbumUpdateCallback(@NonNull UpdateCallback callback) {
152         addUpdateCallback(mAlbumUpdateCallbacks, callback);
153     }
154 
removeAlbumUpdateCallback(@onNull UpdateCallback callback)155     public void removeAlbumUpdateCallback(@NonNull UpdateCallback callback) {
156         removeUpdateCallback(mAlbumUpdateCallbacks, callback);
157     }
158 
addGenreUpdateCallback(@onNull UpdateCallback callback)159     public void addGenreUpdateCallback(@NonNull UpdateCallback callback) {
160         addUpdateCallback(mGenreUpdateCallbacks, callback);
161     }
162 
removeGenreUpdateCallback(@onNull UpdateCallback callback)163     public void removeGenreUpdateCallback(@NonNull UpdateCallback callback) {
164         removeUpdateCallback(mGenreUpdateCallbacks, callback);
165     }
166 
addPlaylistUpdateCallback(@onNull UpdateCallback callback)167     public void addPlaylistUpdateCallback(@NonNull UpdateCallback callback) {
168         addUpdateCallback(mPlaylistUpdateCallbacks, callback);
169     }
170 
removePlaylistUpdateCallback(@onNull UpdateCallback callback)171     public void removePlaylistUpdateCallback(@NonNull UpdateCallback callback) {
172         removeUpdateCallback(mPlaylistUpdateCallbacks, callback);
173     }
174 
addMovieUpdateCallback(@onNull UpdateCallback callback)175     public void addMovieUpdateCallback(@NonNull UpdateCallback callback) {
176         addUpdateCallback(mMovieUpdateCallbacks, callback);
177     }
178 
removeMovieUpdateCallback(@onNull UpdateCallback callback)179     public void removeMovieUpdateCallback(@NonNull UpdateCallback callback) {
180         removeUpdateCallback(mMovieUpdateCallbacks, callback);
181     }
182 
addSeriesUpdateCallback(@onNull UpdateCallback callback)183     public void addSeriesUpdateCallback(@NonNull UpdateCallback callback) {
184         addUpdateCallback(mSeriesUpdateCallbacks, callback);
185     }
186 
removeSeriesUpdateCallback(@onNull UpdateCallback callback)187     public void removeSeriesUpdateCallback(@NonNull UpdateCallback callback) {
188         removeUpdateCallback(mSeriesUpdateCallbacks, callback);
189     }
190 
addEpisodeUpdateCallback(@onNull UpdateCallback callback)191     public void addEpisodeUpdateCallback(@NonNull UpdateCallback callback) {
192         addUpdateCallback(mEpisodeUpdateCallbacks, callback);
193     }
194 
removeEpisodeUpdateCallback(@onNull UpdateCallback callback)195     public void removeEpisodeUpdateCallback(@NonNull UpdateCallback callback) {
196         removeUpdateCallback(mEpisodeUpdateCallbacks, callback);
197     }
198 
addOtherUpdateCallback(@onNull UpdateCallback callback)199     public void addOtherUpdateCallback(@NonNull UpdateCallback callback) {
200         addUpdateCallback(mOtherUpdateCallbacks, callback);
201     }
202 
removeOtherUpdateCallback(@onNull UpdateCallback callback)203     public void removeOtherUpdateCallback(@NonNull UpdateCallback callback) {
204         removeUpdateCallback(mOtherUpdateCallbacks, callback);
205     }
206 
207     @RequiresPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
load()208     public void load() {
209         Clog.i(TAG, "load()");
210         if (mLoaded.getAndSet(true)) {
211             return;
212         }
213 
214         mExecutor.execute(mAudioStore::load);
215         mExecutor.execute(mVideoStore::load);
216     }
217 
getAudios()218     public @NonNull List<Audio> getAudios() {
219         return Collections.unmodifiableList(mAudios);
220     }
getArtists()221     public @NonNull List<Artist> getArtists() {
222         return Collections.unmodifiableList(mArtists);
223     }
getAlbums()224     public @NonNull List<Album> getAlbums() {
225         return Collections.unmodifiableList(mAlbums);
226     }
getGenres()227     public @NonNull List<Genre> getGenres() {
228         return Collections.unmodifiableList(mGenres);
229     }
getPlaylists()230     public @NonNull List<Playlist> getPlaylists() {
231         return Collections.unmodifiableList(mPlaylists);
232     }
233 
getMovies()234     public @NonNull List<Movie> getMovies() {
235         return Collections.unmodifiableList(mMovies);
236     }
getSeries()237     public @NonNull List<Series> getSeries() {
238         return Collections.unmodifiableList(mSeries);
239     }
getEpisodes()240     public @NonNull List<Episode> getEpisodes() {
241         return Collections.unmodifiableList(mEpisodes);
242     }
getOthers()243     public @NonNull List<Other> getOthers() {
244         return Collections.unmodifiableList(mOthers);
245     }
246 
loadData(@onNull Audio audio)247     public void loadData(@NonNull Audio audio) {
248         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
249         if (audio.isLoaded()) return;
250 
251         mExecutor.execute(() -> {
252             boolean updated = mAudioStore.loadData(audio);
253 
254             audio.setLoaded();
255             if (updated) {
256                 Executors.uiThreadExecutor().execute(() -> updateAudio(audio));
257             }
258         });
259     }
260 
loadData(@onNull Artist artist)261     public void loadData(@NonNull Artist artist) {
262         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
263         if (artist.isLoaded()) return;
264 
265         mExecutor.execute(() -> {
266             try {
267                 boolean updated = mDataProvider.populateArtist(artist);
268 
269                 updated |= mAudioStore.loadData(artist);
270 
271                 artist.setLoaded();
272                 if (updated) {
273                     Executors.uiThreadExecutor().execute(() -> updateArtist(artist));
274                 }
275             } catch (IOException e) {
276                 Clog.e(TAG, "Search for " + artist + " failed", e);
277             }
278         });
279     }
280 
loadData(@onNull Album album)281     public void loadData(@NonNull Album album) {
282         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
283         if (album.isLoaded()) return;
284 
285         mExecutor.execute(() -> {
286             try {
287                 boolean updated = mDataProvider.populateAlbum(album);
288 
289                 updated |= mAudioStore.loadData(album);
290 
291                 album.setLoaded();
292                 if (updated) {
293                     Executors.uiThreadExecutor().execute(() -> updateAlbum(album));
294                 }
295             } catch (IOException e) {
296                 Clog.e(TAG, "Search for " + album + " failed", e);
297             }
298         });
299     }
300 
loadData(@onNull Genre genre)301     public void loadData(@NonNull Genre genre) {
302         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
303         if (genre.isLoaded()) return;
304 
305         mExecutor.execute(() -> {
306             boolean updated = mAudioStore.loadData(genre);
307 
308             genre.setLoaded();
309             if (updated) {
310                 Executors.uiThreadExecutor().execute(() -> updateGenre(genre));
311             }
312         });
313     }
314 
loadData(@onNull Playlist playlist)315     public void loadData(@NonNull Playlist playlist) {
316         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
317         if (playlist.isLoaded()) return;
318 
319         mExecutor.execute(() -> {
320             boolean updated = mAudioStore.loadData(playlist);
321 
322             playlist.setLoaded();
323             if (updated) {
324                 Executors.uiThreadExecutor().execute(() -> updatePlaylist(playlist));
325             }
326         });
327     }
328 
329     // TODO(b/123707018) Merge with loadData(episode)/loadData(other)
loadData(@onNull Movie movie)330     public void loadData(@NonNull Movie movie) {
331         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
332         if (movie.isLoaded()) return;
333 
334         mExecutor.execute(() -> {
335             try {
336                 boolean updated = mDataProvider.populateMovie(movie);
337 
338                 updated |= mVideoStore.loadData(movie);
339 
340                 movie.setLoaded();
341                 if (updated) {
342                     Executors.uiThreadExecutor().execute(() -> updateMovie(movie));
343                 }
344             } catch (IOException e) {
345                 Clog.e(TAG, "Search for " + movie + " failed", e);
346             }
347         });
348     }
349 
loadData(@onNull Series series)350     public void loadData(@NonNull Series series) {
351         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
352         if (series.isLoaded()) return;
353 
354         mExecutor.execute(() -> {
355             try {
356                 boolean updated = mDataProvider.populateSeries(series);
357 
358                 updated |= mVideoStore.loadData(series);
359 
360                 series.setLoaded();
361                 if (updated) {
362                     Executors.uiThreadExecutor().execute(() -> updateSeries(series));
363                 }
364             } catch (IOException e) {
365                 Clog.e(TAG, "Search for " + series + " failed", e);
366             }
367         });
368     }
369 
370     // TODO(b/123707018) Merge with loadData(movie)/loadData(other)
loadData(@onNull Episode episode)371     public void loadData(@NonNull Episode episode) {
372         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
373         if (episode.isLoaded()) return;
374 
375         mExecutor.execute(() -> {
376             try {
377                 boolean updated = mDataProvider.populateEpisode(episode);
378 
379                 updated |= mVideoStore.loadData(episode);
380 
381                 episode.setLoaded();
382                 if (updated) {
383                     Executors.uiThreadExecutor().execute(() -> updateEpisode(episode));
384                 }
385             } catch (IOException e) {
386                 Clog.e(TAG, "Search for " + episode + " failed", e);
387             }
388         });
389     }
390 
391     // TODO(b/123707018) Merge with loadData(movie)/loadData(episode)
loadData(@onNull Other other)392     public void loadData(@NonNull Other other) {
393         // TODO(b/123707632) Ensure no concurrent runs for the same item !!
394         if (other.isLoaded()) return;
395 
396         mExecutor.execute(() -> {
397             boolean updated = mVideoStore.loadData(other);
398 
399             other.setLoaded();
400             if (updated) {
401                 Executors.uiThreadExecutor().execute(() -> updateOther(other));
402             }
403         });
404     }
405 
406     @Override
getAudioById(long id)407     public @NonNull Audio getAudioById(long id) {
408         for (Audio audio : mAudios) {
409             if (audio.getId() == id) {
410                 return audio;
411             }
412         }
413         throw new IllegalArgumentException("Audio with id " + id + " was not found");
414     }
415 
416     @Override
getArtistById(long id)417     public @NonNull Artist getArtistById(long id) {
418         for (Artist artist : mArtists) {
419             if (artist.getId() == id) {
420                 return artist;
421             }
422         }
423         throw new IllegalArgumentException("Artist with id " + id + " was not found");
424     }
425 
426     @Override
getAlbumById(long id)427     public @NonNull Album getAlbumById(long id) {
428         for (Album album : mAlbums) {
429             if (album.getId() == id) {
430                 return album;
431             }
432         }
433         throw new IllegalArgumentException("Album with id " + id + " was not found");
434     }
435 
436     @Override
getGenreById(long id)437     public @NonNull Genre getGenreById(long id) {
438         for (Genre genre : mGenres) {
439             if (genre.getId() == id) {
440                 return genre;
441             }
442         }
443         throw new IllegalArgumentException("Genre with id " + id + " was not found");
444     }
445 
446     @Override
getPlaylistById(long id)447     public @NonNull Playlist getPlaylistById(long id) {
448         for (Playlist playlist : mPlaylists) {
449             if (playlist.getId() == id) {
450                 return playlist;
451             }
452         }
453         throw new IllegalArgumentException("Playlist with id " + id + " was not found");
454     }
455 
456     @Override
getMovieById(long id)457     public @NonNull Movie getMovieById(long id) {
458         for (Movie movie : mMovies) {
459             if (movie.getId() == id) {
460                 return movie;
461             }
462         }
463         throw new IllegalArgumentException("Movie with id " + id + " was not found");
464     }
465 
466     @Override
getSeriesById(@onNull String title)467     public @NonNull Series getSeriesById(@NonNull String title) {
468         for (Series series : mSeries) {
469             if (!series.hasYear() && series.getTitle().equals(title)) {
470                 return series;
471             }
472         }
473         throw new IllegalArgumentException("Series '" + title + "' was not found");
474     }
475 
476     @Override
getSeriesById(@onNull String title, int year)477     public @NonNull Series getSeriesById(@NonNull String title, int year) {
478         for (Series series : mSeries) {
479             if (series.hasYear() && series.getTitle().equals(title) && series.getYear() == year) {
480                 return series;
481             }
482         }
483         throw new IllegalArgumentException("Series '" + title + "' (" + year + ") was not found");
484     }
485 
486     @Override
getEpisodeById(long id)487     public @NonNull Episode getEpisodeById(long id) {
488         for (Episode episode : mEpisodes) {
489             if (episode.getId() == id) {
490                 return episode;
491             }
492         }
493         throw new IllegalArgumentException("Episode with id " + id + " was not found");
494     }
495 
496     @Override
getOtherById(long id)497     public @NonNull Other getOtherById(long id) {
498         for (Other other : mOthers) {
499             if (other.getId() == id) {
500                 return other;
501             }
502         }
503         throw new IllegalArgumentException("Other with id " + id + " was not found");
504     }
505 
addUpdateCallback(@onNull Set<UpdateCallback> callbacks, @NonNull UpdateCallback callback)506     private void addUpdateCallback(@NonNull Set<UpdateCallback> callbacks,
507             @NonNull UpdateCallback callback) {
508         if (!callbacks.add(callback)) {
509             throw new IllegalArgumentException("Callback " + callback + " already added in " +
510                     callbacks);
511         }
512     }
513 
removeUpdateCallback(@onNull Set<UpdateCallback> callbacks, @NonNull UpdateCallback callback)514     private void removeUpdateCallback(@NonNull Set<UpdateCallback> callbacks,
515             @NonNull UpdateCallback callback) {
516         if (!callbacks.remove(callback)) {
517             throw new IllegalArgumentException("Callback " + callback + " not found in " +
518                     callbacks);
519         }
520     }
521 
addAudios(@onNull Collection<Audio> audios)522     private void addAudios(@NonNull Collection<Audio> audios) {
523         int audiosIndex = mAudios.size();
524         int audiosCount = 0;
525 
526         mAudios.addAll(audios);
527         audiosCount += audios.size();
528 
529         if (audiosCount > 0) {
530             for (UpdateCallback callback : mAudioUpdateCallbacks) {
531                 callback.onItemsInserted(audiosIndex, audiosCount);
532             }
533         }
534     }
535 
addArtists(@onNull Collection<Artist> artists)536     private void addArtists(@NonNull Collection<Artist> artists) {
537         int artistsIndex = mArtists.size();
538         int artistsCount = 0;
539 
540         mArtists.addAll(artists);
541         artistsCount += artists.size();
542 
543         if (artistsCount > 0) {
544             for (UpdateCallback callback : mArtistUpdateCallbacks) {
545                 callback.onItemsInserted(artistsIndex, artistsCount);
546             }
547         }
548     }
549 
addAlbums(@onNull Collection<Album> albums)550     private void addAlbums(@NonNull Collection<Album> albums) {
551         int albumsIndex = mAlbums.size();
552         int albumsCount = 0;
553 
554         mAlbums.addAll(albums);
555         albumsCount += albums.size();
556 
557         if (albumsCount > 0) {
558             for (UpdateCallback callback : mAlbumUpdateCallbacks) {
559                 callback.onItemsInserted(albumsIndex, albumsCount);
560             }
561         }
562     }
563 
addGenres(@onNull Collection<Genre> genres)564     private void addGenres(@NonNull Collection<Genre> genres) {
565         int genresIndex = mGenres.size();
566         int genresCount = 0;
567 
568         mGenres.addAll(genres);
569         genresCount += genres.size();
570 
571         if (genresCount > 0) {
572             for (UpdateCallback callback : mGenreUpdateCallbacks) {
573                 callback.onItemsInserted(genresIndex, genresCount);
574             }
575         }
576     }
577 
addPlaylists(@onNull Collection<Playlist> playlists)578     private void addPlaylists(@NonNull Collection<Playlist> playlists) {
579         int playlistsIndex = mPlaylists.size();
580         int playlistsCount = 0;
581 
582         mPlaylists.addAll(playlists);
583         playlistsCount += playlists.size();
584 
585         if (playlistsCount > 0) {
586             for (UpdateCallback callback : mPlaylistUpdateCallbacks) {
587                 callback.onItemsInserted(playlistsIndex, playlistsCount);
588             }
589         }
590     }
591 
addMovies(@onNull Collection<Movie> movies)592     private void addMovies(@NonNull Collection<Movie> movies) {
593         int moviesIndex = mMovies.size();
594         int moviesCount = 0;
595 
596         mMovies.addAll(movies);
597         moviesCount += movies.size();
598 
599         if (moviesCount > 0) {
600             for (UpdateCallback callback : mMovieUpdateCallbacks) {
601                 callback.onItemsInserted(moviesIndex, moviesCount);
602             }
603         }
604     }
605 
addSeries(@onNull Collection<Series> series)606     private void addSeries(@NonNull Collection<Series> series) {
607         int seriesIndex = mSeries.size();
608         int seriesCount = 0;
609 
610         mSeries.addAll(series);
611         seriesCount += series.size();
612 
613         if (seriesCount > 0) {
614             for (UpdateCallback callback : mSeriesUpdateCallbacks) {
615                 callback.onItemsInserted(seriesIndex, seriesCount);
616             }
617         }
618     }
619 
addEpisodes(@onNull Collection<Episode> episodes)620     private void addEpisodes(@NonNull Collection<Episode> episodes) {
621         int episodesIndex = mEpisodes.size();
622         int episodesCount = 0;
623 
624         mEpisodes.addAll(episodes);
625         episodesCount += episodes.size();
626 
627         if (episodesCount > 0) {
628             for (UpdateCallback callback : mEpisodeUpdateCallbacks) {
629                 callback.onItemsInserted(episodesIndex, episodesCount);
630             }
631         }
632     }
633 
addOthers(@onNull Collection<Other> others)634     private void addOthers(@NonNull Collection<Other> others) {
635         int othersIndex = mOthers.size();
636         int othersCount = 0;
637 
638         mOthers.addAll(others);
639         othersCount += others.size();
640 
641         if (othersCount > 0) {
642             for (UpdateCallback callback : mOtherUpdateCallbacks) {
643                 callback.onItemsInserted(othersIndex, othersCount);
644             }
645         }
646     }
647 
updateAudio(@onNull Audio audio)648     private void updateAudio(@NonNull Audio audio) {
649         int index = mAudios.indexOf(audio);
650         if (index != -1) {
651             for (UpdateCallback callback : mAudioUpdateCallbacks) {
652                 callback.onItemsUpdated(index, 1);
653             }
654         }
655     }
656 
updateArtist(@onNull Artist artist)657     private void updateArtist(@NonNull Artist artist) {
658         int index = mArtists.indexOf(artist);
659         if (index != -1) {
660             for (UpdateCallback callback : mArtistUpdateCallbacks) {
661                 callback.onItemsUpdated(index, 1);
662             }
663         }
664     }
665 
updateAlbum(@onNull Album album)666     private void updateAlbum(@NonNull Album album) {
667         int index = mAlbums.indexOf(album);
668         if (index != -1) {
669             for (UpdateCallback callback : mAlbumUpdateCallbacks) {
670                 callback.onItemsUpdated(index, 1);
671             }
672         }
673     }
674 
updateGenre(@onNull Genre genre)675     private void updateGenre(@NonNull Genre genre) {
676         int index = mGenres.indexOf(genre);
677         if (index != -1) {
678             for (UpdateCallback callback : mGenreUpdateCallbacks) {
679                 callback.onItemsUpdated(index, 1);
680             }
681         }
682     }
683 
updatePlaylist(@onNull Playlist playlist)684     private void updatePlaylist(@NonNull Playlist playlist) {
685         int index = mPlaylists.indexOf(playlist);
686         if (index != -1) {
687             for (UpdateCallback callback : mPlaylistUpdateCallbacks) {
688                 callback.onItemsUpdated(index, 1);
689             }
690         }
691     }
692 
updateMovie(@onNull Movie movie)693     private void updateMovie(@NonNull Movie movie) {
694         int index = mMovies.indexOf(movie);
695         if (index != -1) {
696             for (UpdateCallback callback : mMovieUpdateCallbacks) {
697                 callback.onItemsUpdated(index, 1);
698             }
699         }
700     }
701 
updateSeries(@onNull Series series)702     private void updateSeries(@NonNull Series series) {
703         int index = mSeries.indexOf(series);
704         if (index != -1) {
705             for (UpdateCallback callback : mSeriesUpdateCallbacks) {
706                 callback.onItemsUpdated(index, 1);
707             }
708         }
709     }
710 
updateEpisode(@onNull Episode episode)711     private void updateEpisode(@NonNull Episode episode) {
712         int index = mEpisodes.indexOf(episode);
713         if (index != -1) {
714             for (UpdateCallback callback : mEpisodeUpdateCallbacks) {
715                 callback.onItemsUpdated(index, 1);
716             }
717         }
718     }
719 
updateOther(@onNull Other other)720     private void updateOther(@NonNull Other other) {
721         int index = mOthers.indexOf(other);
722         if (index != -1) {
723             for (UpdateCallback callback : mOtherUpdateCallbacks) {
724                 callback.onItemsUpdated(index, 1);
725             }
726         }
727     }
728 }
729