diff --git a/app/build.gradle b/app/build.gradle index a9e44c03..b3d7f366 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { minSdkVersion 19 targetSdkVersion 30 - versionCode 8 - versionName '1.2.0' + versionCode 9 + versionName '1.2.1' multiDexEnabled true vectorDrawables { diff --git a/app/src/main/java/com/dkanada/gramophone/App.java b/app/src/main/java/com/dkanada/gramophone/App.java index 6dd0cdb2..ca36012f 100644 --- a/app/src/main/java/com/dkanada/gramophone/App.java +++ b/app/src/main/java/com/dkanada/gramophone/App.java @@ -33,9 +33,8 @@ public class App extends Application { public void onCreate() { super.onCreate(); - if (BuildConfig.DEBUG) { - RedScreenOfDeath.init(this); - } + // enable for all builds to help with bug reports + RedScreenOfDeath.init(this); app = this; database = createDatabase(this); diff --git a/app/src/main/java/com/dkanada/gramophone/adapter/MusicLibraryPagerAdapter.java b/app/src/main/java/com/dkanada/gramophone/adapter/MusicLibraryPagerAdapter.java index 1f93edde..0729f818 100644 --- a/app/src/main/java/com/dkanada/gramophone/adapter/MusicLibraryPagerAdapter.java +++ b/app/src/main/java/com/dkanada/gramophone/adapter/MusicLibraryPagerAdapter.java @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; +import com.dkanada.gramophone.fragments.mainactivity.library.pager.FavoritesFragment; import com.dkanada.gramophone.model.CategoryInfo; import com.dkanada.gramophone.fragments.mainactivity.library.pager.AlbumsFragment; import com.dkanada.gramophone.fragments.mainactivity.library.pager.ArtistsFragment; @@ -154,7 +155,8 @@ public class MusicLibraryPagerAdapter extends FragmentPagerAdapter { ALBUMS(AlbumsFragment.class), ARTISTS(ArtistsFragment.class), GENRES(GenresFragment.class), - PLAYLISTS(PlaylistsFragment.class); + PLAYLISTS(PlaylistsFragment.class), + FAVORITES(FavoritesFragment.class); private final Class mFragmentClass; diff --git a/app/src/main/java/com/dkanada/gramophone/fragments/mainactivity/library/LibraryFragment.java b/app/src/main/java/com/dkanada/gramophone/fragments/mainactivity/library/LibraryFragment.java index fa8fb365..67a5ad9b 100644 --- a/app/src/main/java/com/dkanada/gramophone/fragments/mainactivity/library/LibraryFragment.java +++ b/app/src/main/java/com/dkanada/gramophone/fragments/mainactivity/library/LibraryFragment.java @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.dkanada.gramophone.databinding.FragmentLibraryBinding; +import com.dkanada.gramophone.fragments.mainactivity.library.pager.FavoritesFragment; import com.google.android.material.appbar.AppBarLayout; import com.afollestad.materialcab.MaterialCab; import com.kabouzeid.appthemehelper.ThemeStore; @@ -354,7 +355,7 @@ public class LibraryFragment extends AbsMainActivityFragment implements CabHolde .setChecked(currentSortMethod.equals(SortMethod.ADDED)); sortMethodMenu.add(0, R.id.action_sort_method_random, 4, R.string.sort_method_random) .setChecked(currentSortMethod.equals(SortMethod.RANDOM)); - } else if (fragment instanceof SongsFragment) { + } else if (fragment instanceof SongsFragment || fragment instanceof FavoritesFragment) { sortMethodMenu.add(0, R.id.action_sort_method_name, 0, R.string.sort_method_name) .setChecked(currentSortMethod.equals(SortMethod.NAME)); sortMethodMenu.add(0, R.id.action_sort_method_album, 1, R.string.sort_method_album) diff --git a/app/src/main/java/com/dkanada/gramophone/fragments/mainactivity/library/pager/FavoritesFragment.java b/app/src/main/java/com/dkanada/gramophone/fragments/mainactivity/library/pager/FavoritesFragment.java new file mode 100644 index 00000000..0dc9c134 --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/fragments/mainactivity/library/pager/FavoritesFragment.java @@ -0,0 +1,30 @@ +package com.dkanada.gramophone.fragments.mainactivity.library.pager; + +import androidx.annotation.NonNull; +import com.dkanada.gramophone.App; +import com.dkanada.gramophone.util.PreferenceUtil; +import com.dkanada.gramophone.util.QueryUtil; +import org.jellyfin.apiclient.model.querying.ItemFields; +import org.jellyfin.apiclient.model.querying.ItemFilter; +import org.jellyfin.apiclient.model.querying.ItemQuery; + +public class FavoritesFragment extends SongsFragment { + @NonNull + @Override + protected ItemQuery createQuery() { + ItemQuery query = new ItemQuery(); + + query.setIncludeItemTypes(new String[]{"Audio"}); + query.setFields(new ItemFields[]{ItemFields.MediaSources}); + query.setUserId(App.getApiClient().getCurrentUserId()); + query.setRecursive(true); + query.setLimit(PreferenceUtil.getInstance(App.getInstance()).getPageSize()); + query.setStartIndex(getAdapter().getItemCount()); + query.setParentId(QueryUtil.currentLibrary.getId()); + query.setFilters(new ItemFilter[]{ItemFilter.IsFavorite}); + + QueryUtil.applySortMethod(query, PreferenceUtil.getInstance(App.getInstance()).getSongSortMethod()); + QueryUtil.applySortOrder(query, PreferenceUtil.getInstance(App.getInstance()).getSongSortOrder()); + return query; + } +} diff --git a/app/src/main/java/com/dkanada/gramophone/model/CategoryInfo.java b/app/src/main/java/com/dkanada/gramophone/model/CategoryInfo.java index 214c8221..0b112a6e 100644 --- a/app/src/main/java/com/dkanada/gramophone/model/CategoryInfo.java +++ b/app/src/main/java/com/dkanada/gramophone/model/CategoryInfo.java @@ -44,7 +44,8 @@ public class CategoryInfo implements Parcelable { ALBUMS(R.string.albums), ARTISTS(R.string.artists), GENRES(R.string.genres), - PLAYLISTS(R.string.playlists); + PLAYLISTS(R.string.playlists), + FAVORITES(R.string.favorites); public final int stringRes; diff --git a/app/src/main/java/com/dkanada/gramophone/service/MultiPlayer.java b/app/src/main/java/com/dkanada/gramophone/service/MultiPlayer.java index cf74a75f..24c63c4f 100644 --- a/app/src/main/java/com/dkanada/gramophone/service/MultiPlayer.java +++ b/app/src/main/java/com/dkanada/gramophone/service/MultiPlayer.java @@ -13,12 +13,10 @@ import com.dkanada.gramophone.util.PreferenceUtil; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.database.ExoDatabaseProvider; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSource; @@ -28,27 +26,13 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto import com.google.android.exoplayer2.upstream.cache.SimpleCache; import java.io.File; -import java.io.IOException; - -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Dispatcher; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.internal.annotations.EverythingIsNonNull; public class MultiPlayer implements Playback { public static final String TAG = MultiPlayer.class.getSimpleName(); private final Context context; - private final OkHttpClient httpClient; - - private SimpleExoPlayer exoPlayer; - private ConcatenatingMediaSource mediaSource; - + private final SimpleExoPlayer exoPlayer; private final SimpleCache simpleCache; - private final DataSource.Factory dataSource; private PlaybackCallbacks callbacks; @@ -68,7 +52,7 @@ public class MultiPlayer implements Playback { int windowIndex = exoPlayer.getCurrentWindowIndex(); if (windowIndex == 1) { - mediaSource.removeMediaSource(0); + exoPlayer.removeMediaItem(0); if (exoPlayer.isPlaying()) { // there are still songs left in the queue callbacks.onTrackWentToNext(); @@ -88,17 +72,11 @@ public class MultiPlayer implements Playback { public MultiPlayer(Context context) { this.context = context; - Dispatcher dispatcher = new Dispatcher(); - dispatcher.setMaxRequests(1); - - httpClient = new OkHttpClient.Builder().dispatcher(dispatcher).build(); - - exoPlayer = new SimpleExoPlayer.Builder(context).build(); - mediaSource = new ConcatenatingMediaSource(); + MediaSourceFactory mediaSourceFactory = new UnknownMediaSourceFactory(buildDataSourceFactory()); + exoPlayer = new SimpleExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory).build(); exoPlayer.addListener(eventListener); - exoPlayer.prepare(mediaSource); - exoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF); + exoPlayer.prepare(); long cacheSize = PreferenceUtil.getInstance(context).getMediaCacheSize(); LeastRecentlyUsedCacheEvictor recentlyUsedCache = new LeastRecentlyUsedCacheEvictor(cacheSize); @@ -106,23 +84,19 @@ public class MultiPlayer implements Playback { File cacheDirectory = new File(context.getCacheDir(), "exoplayer"); simpleCache = new SimpleCache(cacheDirectory, recentlyUsedCache, databaseProvider); - dataSource = buildDataSourceFactory(); } @Override public void setDataSource(Song song) { - mediaSource = new ConcatenatingMediaSource(); - - exoPlayer.addListener(eventListener); - exoPlayer.prepare(mediaSource); - + exoPlayer.clearMediaItems(); appendDataSource(MusicUtil.getSongFileUri(song)); + exoPlayer.seekTo(0, 0); } @Override public void queueDataSource(Song song) { - while (mediaSource.getSize() > 1) { - mediaSource.removeMediaSource(1); + while (exoPlayer.getMediaItemCount() > 1) { + exoPlayer.removeMediaItem(1); } appendDataSource(MusicUtil.getSongFileUri(song)); @@ -130,36 +104,9 @@ public class MultiPlayer implements Playback { private void appendDataSource(String path) { Uri uri = Uri.parse(path); + MediaItem mediaItem = MediaItem.fromUri(uri); - httpClient.newCall(new Request.Builder().url(path).head().build()).enqueue(new Callback() { - @Override - @EverythingIsNonNull - public void onFailure(Call call, IOException e) { - Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); - e.printStackTrace(); - } - - @Override - @EverythingIsNonNull - public void onResponse(Call call, Response response) { - String type = response.header("Content-Type"); - if (type == null) return; - - MediaSource source; - if (type.equals("application/x-mpegURL")) { - source = new HlsMediaSource.Factory(dataSource) - .setTag(path) - .setAllowChunklessPreparation(true) - .createMediaSource(uri); - } else { - source = new ProgressiveMediaSource.Factory(dataSource) - .setTag(path) - .createMediaSource(uri); - } - - mediaSource.addMediaSource(source); - } - }); + exoPlayer.addMediaItem(mediaItem); } private DataSource.Factory buildDataSourceFactory() { diff --git a/app/src/main/java/com/dkanada/gramophone/service/MusicService.java b/app/src/main/java/com/dkanada/gramophone/service/MusicService.java index 79ffc13e..57027e31 100644 --- a/app/src/main/java/com/dkanada/gramophone/service/MusicService.java +++ b/app/src/main/java/com/dkanada/gramophone/service/MusicService.java @@ -449,8 +449,8 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP if (restoredPositionInTrack > 0) seek(restoredPositionInTrack); notHandledMetaChangedForCurrentTrack = true; - sendChangeInternal(META_CHANGED); - sendChangeInternal(QUEUE_CHANGED); + handleChangeInternal(META_CHANGED); + handleChangeInternal(QUEUE_CHANGED); } } @@ -1291,7 +1291,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP info.setItemId(mService.get().getCurrentSong().id); info.setPositionTicks(progress * 10000); - task.cancel(true); + if (task != null) task.cancel(true); executorService.shutdownNow(); } } diff --git a/app/src/main/java/com/dkanada/gramophone/service/UnknownMediaSourceFactory.kt b/app/src/main/java/com/dkanada/gramophone/service/UnknownMediaSourceFactory.kt new file mode 100644 index 00000000..100a7d03 --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/service/UnknownMediaSourceFactory.kt @@ -0,0 +1,73 @@ +package com.dkanada.gramophone.service + +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.drm.DrmSessionManager +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory +import com.google.android.exoplayer2.source.MediaSource +import com.google.android.exoplayer2.source.MediaSourceFactory +import com.google.android.exoplayer2.source.ProgressiveMediaSource +import com.google.android.exoplayer2.source.hls.HlsMediaSource +import com.google.android.exoplayer2.upstream.* + +import kotlinx.coroutines.* + +import java.net.HttpURLConnection +import java.net.URL + +class UnknownMediaSourceFactory(dataSourceFactory: DataSource.Factory) : MediaSourceFactory { + private val hlsMediaSource : HlsMediaSource.Factory + private val progressiveMediaSource : ProgressiveMediaSource.Factory + + private var loadErrorHandlingPolicy: LoadErrorHandlingPolicy + + override fun setDrmSessionManager(drmSessionManager: DrmSessionManager?): MediaSourceFactory { + return this + } + + override fun setDrmHttpDataSourceFactory(drmHttpDataSourceFactory: HttpDataSource.Factory?): MediaSourceFactory { + return this + } + + override fun setDrmUserAgent(drmUserAgent: String?): MediaSourceFactory { + return this + } + + override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy?): MediaSourceFactory { + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy!! + return this + } + + override fun getSupportedTypes(): IntArray { + return intArrayOf() + } + + override fun createMediaSource(mediaItem: MediaItem): MediaSource { + val type: String? = runBlocking { + httpGet(mediaItem.playbackProperties!!.uri.toString()) + } + + val sourceFactory: MediaSourceFactory = if (type == "application/x-mpegURL") { + hlsMediaSource + } else { + progressiveMediaSource + } + + return sourceFactory.createMediaSource(mediaItem) + } + + private suspend fun httpGet(url: String?): String? { + return withContext(Dispatchers.IO) { + val request = URL(url) + val conn = request.openConnection() as HttpURLConnection + + return@withContext conn.getHeaderField("Content-Type") + } + } + + init { + hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory) + progressiveMediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, DefaultExtractorsFactory()) + + loadErrorHandlingPolicy = DefaultLoadErrorHandlingPolicy() + } +} diff --git a/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java b/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java index 7217d2f6..da2ff95f 100644 --- a/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java +++ b/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java @@ -429,6 +429,7 @@ public final class PreferenceUtil { defaultCategories.add(new CategoryInfo(CategoryInfo.Category.ARTISTS, true)); defaultCategories.add(new CategoryInfo(CategoryInfo.Category.GENRES, true)); defaultCategories.add(new CategoryInfo(CategoryInfo.Category.PLAYLISTS, true)); + defaultCategories.add(new CategoryInfo(CategoryInfo.Category.FAVORITES, true)); return defaultCategories; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6c53142..b88c2079 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ Genres Songs Playlists + Favorites No playlists No albums No songs diff --git a/metadata/en-US/changelogs/9.txt b/metadata/en-US/changelogs/9.txt new file mode 100644 index 00000000..82d0692b --- /dev/null +++ b/metadata/en-US/changelogs/9.txt @@ -0,0 +1,8 @@ +- move glide cache to the proper location +- fix issue with lock screen cover display +- change default sort method to random +- replace queue store with room database +- huge improvements for login activity style +- add crash activity to display errors +- refactor ExoPlayer wrapper +- implement favorites tab on home activity