From bfc11f9924d40e945c6f702cf63ab788019236d2 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 27 Mar 2022 12:03:59 +0200 Subject: [PATCH 01/67] Don't manage ID manually, but use autoGenerate to ensure uniqueness (across the lifetime of the db) --- .../main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt | 6 +++--- .../main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt | 5 +++-- .../kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt | 6 ------ .../org/moire/ultrasonic/model/ServerSettingsModel.kt | 4 +--- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt index 125f9a31..067aa079 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -10,9 +10,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase * This could be settings or data that are not specific to any remote music database */ @Database( - entities = [ServerSetting::class], - version = 4, - exportSchema = true + entities = [ServerSetting::class], + version = 5, + exportSchema = true ) abstract class AppDatabase : RoomDatabase() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt index e3bf722c..05e2ff1d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt @@ -19,7 +19,8 @@ import androidx.room.PrimaryKey */ @Entity data class ServerSetting( - @PrimaryKey var id: Int, + // Default ID is 0, which will trigger SQLite to generate a unique ID. + @PrimaryKey(autoGenerate = true) var id: Int = 0, @ColumnInfo(name = "index") var index: Int, @ColumnInfo(name = "name") var name: String, @ColumnInfo(name = "url") var url: String, @@ -37,6 +38,6 @@ data class ServerSetting( @ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null ) { constructor() : this ( - -1, 0, "", "", null, "", "", false, false, false, null, null + 0, 0, "", "", null, "", "", false, false, false, null, null ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt index b09149cc..660c6c2f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt @@ -69,12 +69,6 @@ interface ServerSettingDao { @Query("SELECT COUNT(*) FROM serverSetting") fun liveServerCount(): LiveData - /** - * Retrieves the greatest value of the Id column in the table - */ - @Query("SELECT MAX([id]) FROM serverSetting") - suspend fun getMaxId(): Int? - /** * Retrieves the greatest value of the Index column in the table */ diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt index 99ea5890..4f4d1226 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt @@ -127,7 +127,6 @@ class ServerSettingsModel( appScope.launch { serverSetting.index = (repository.count() ?: 0) + 1 - serverSetting.id = (repository.getMaxId() ?: 0) + 1 repository.insert(serverSetting) Timber.d("saveNewItem saved server setting: $serverSetting") } @@ -142,12 +141,11 @@ class ServerSettingsModel( runBlocking { demo.index = (repository.count() ?: 0) + 1 - demo.id = (repository.getMaxId() ?: 0) + 1 repository.insert(demo) Timber.d("Added demo server") } - return demo.id + return demo.index } /** From 922022ab032741d6dbd2e0d2621c0dec543b3ebc Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 3 Apr 2022 23:57:50 +0200 Subject: [PATCH 02/67] Switch to Media3 --- .../org/moire/ultrasonic/domain/RepeatMode.kt | 15 - gradle/libs.versions.toml | 4 + ultrasonic/build.gradle | 3 + ultrasonic/src/main/AndroidManifest.xml | 14 +- .../receiver/A2dpIntentReceiver.java | 57 -- .../service/JukeboxMediaPlayer.java | 489 ----------- .../ultrasonic/util/ShufflePlayBuffer.java | 125 --- .../moire/ultrasonic/util/StreamProxy.java | 290 ------- .../moire/ultrasonic/view/VisualizerView.java | 2 +- .../ultrasonic/activity/NavigationActivity.kt | 8 +- .../ultrasonic/adapters/TrackViewBinder.kt | 59 +- .../ultrasonic/adapters/TrackViewHolder.kt | 2 +- .../moire/ultrasonic/di/ApplicationModule.kt | 2 - .../moire/ultrasonic/di/MediaPlayerModule.kt | 10 +- .../ultrasonic/fragment/DownloadsFragment.kt | 2 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 11 +- .../ultrasonic/fragment/PlayerFragment.kt | 359 ++++---- .../ultrasonic/fragment/SearchFragment.kt | 11 +- .../fragment/ServerSelectorFragment.kt | 8 +- .../ultrasonic/fragment/SettingsFragment.kt | 22 +- .../fragment/TrackCollectionFragment.kt | 2 +- .../ultrasonic/playback/APIDataSource.kt | 398 +++++++++ .../playback/LegacyPlaylistManager.kt | 127 +++ .../ultrasonic/playback/MediaItemTree.kt | 254 ++++++ .../playback/MediaNotificationProvider.kt | 158 ++++ .../org/moire/ultrasonic/playback/Plan.md | 18 + .../ultrasonic/playback/PlaybackService.kt | 244 ++++++ .../ultrasonic/playback/UltrasonicCache.kt | 99 +++ .../ultrasonic/service/AudioFocusHandler.kt | 118 --- .../service/AutoMediaBrowserService.kt | 29 +- .../moire/ultrasonic/service/DownloadFile.kt | 271 +----- .../ultrasonic/service/DownloadService.kt | 256 ++++++ .../moire/ultrasonic/service/Downloader.kt | 526 ++++++------ .../ultrasonic/service/JukeboxMediaPlayer.kt | 337 ++++++++ .../ultrasonic/service/LocalMediaPlayer.kt | 745 ----------------- .../service/MediaPlayerController.kt | 536 +++++++----- .../service/MediaPlayerLifecycleSupport.kt | 126 +-- .../ultrasonic/service/MediaPlayerService.kt | 769 ------------------ .../service/PlaybackStateSerializer.kt | 2 +- .../org/moire/ultrasonic/service/RxBus.kt | 7 +- .../ultrasonic/subsonic/DownloadHandler.kt | 25 +- .../org/moire/ultrasonic/util/CacheCleaner.kt | 4 +- .../org/moire/ultrasonic/util/FileUtil.kt | 2 +- .../ultrasonic/util/MediaSessionHandler.kt | 332 -------- .../org/moire/ultrasonic/util/Settings.kt | 39 - .../drawable/media3_notification_pause.xml | 5 + .../res/drawable/media3_notification_play.xml | 5 + .../media3_notification_seek_to_next.xml | 5 + .../media3_notification_seek_to_previous.xml | 5 + .../media3_notification_small_icon.xml | 9 + 50 files changed, 2871 insertions(+), 4075 deletions(-) delete mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/RepeatMode.kt delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt create mode 100644 ultrasonic/src/main/res/drawable/media3_notification_pause.xml create mode 100644 ultrasonic/src/main/res/drawable/media3_notification_play.xml create mode 100644 ultrasonic/src/main/res/drawable/media3_notification_seek_to_next.xml create mode 100644 ultrasonic/src/main/res/drawable/media3_notification_seek_to_previous.xml create mode 100644 ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/RepeatMode.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/RepeatMode.kt deleted file mode 100644 index f9005eb9..00000000 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/RepeatMode.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.moire.ultrasonic.domain - -enum class RepeatMode { - OFF { - override operator fun next(): RepeatMode = ALL - }, - ALL { - override operator fun next(): RepeatMode = SINGLE - }, - SINGLE { - override operator fun next(): RepeatMode = OFF - }; - - abstract operator fun next(): RepeatMode -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 064dd182..ea4b36e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ detekt = "1.19.0" jacoco = "0.8.7" preferences = "1.1.1" media = "1.3.1" +media3 = "1.0.0-alpha03" androidSupport = "28.0.0" androidLegacySupport = "1.0.0" @@ -66,6 +67,9 @@ navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", ve navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" } preferences = { module = "androidx.preference:preference", version.ref = "preferences" } media = { module = "androidx.media:media", version.ref = "media" } +media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } +media3okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" } +media3session = { module = "androidx.media3:media3-session", version.ref = "media3" } kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index f6fc5607..57836a3b 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -100,6 +100,9 @@ dependencies { implementation libs.constraintLayout implementation libs.preferences implementation libs.media + implementation libs.media3exoplayer + implementation libs.media3session + implementation libs.media3okhttp implementation libs.navigationFragment implementation libs.navigationUi diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 768e7571..757d335f 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -56,18 +56,17 @@ - + @@ -146,13 +145,6 @@ android:name=".provider.SearchSuggestionProvider" android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/> - - - - - diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java deleted file mode 100644 index e122ca31..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.moire.ultrasonic.receiver; - -import static org.koin.java.KoinJavaComponent.inject; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import org.moire.ultrasonic.domain.Track; -import org.moire.ultrasonic.service.MediaPlayerController; - -import kotlin.Lazy; - -public class A2dpIntentReceiver extends BroadcastReceiver -{ - private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse"; - private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); - - @Override - public void onReceive(Context context, Intent intent) - { - if (mediaPlayerControllerLazy.getValue().getCurrentPlaying() == null) return; - - Track song = mediaPlayerControllerLazy.getValue().getCurrentPlaying().getTrack(); - if (song == null) return; - - Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE); - - Integer duration = song.getDuration(); - int playerPosition = mediaPlayerControllerLazy.getValue().getPlayerPosition(); - int listSize = mediaPlayerControllerLazy.getValue().getPlaylistSize(); - - if (duration != null) - { - avrcpIntent.putExtra("duration", (long) duration); - } - - avrcpIntent.putExtra("position", (long) playerPosition); - avrcpIntent.putExtra("ListSize", (long) listSize); - - switch (mediaPlayerControllerLazy.getValue().getPlayerState()) - { - case STARTED: - avrcpIntent.putExtra("playing", true); - break; - case STOPPED: - case PAUSED: - case COMPLETED: - avrcpIntent.putExtra("playing", false); - break; - default: - return; - } - - context.sendBroadcast(avrcpIntent); - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java deleted file mode 100644 index 46e62147..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java +++ /dev/null @@ -1,489 +0,0 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.service; - -import android.content.Context; -import android.os.Handler; -import timber.log.Timber; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ProgressBar; -import android.widget.Toast; - -import org.jetbrains.annotations.NotNull; -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; -import org.moire.ultrasonic.api.subsonic.SubsonicRESTException; -import org.moire.ultrasonic.app.UApp; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.JukeboxStatus; -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.util.Util; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -/** - * Provides an asynchronous interface to the remote jukebox on the Subsonic server. - * - * @author Sindre Mehus - * @version $Id$ - */ -public class JukeboxMediaPlayer -{ - private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L; - - private final TaskQueue tasks = new TaskQueue(); - private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); - private ScheduledFuture statusUpdateFuture; - private final AtomicLong timeOfLastUpdate = new AtomicLong(); - private JukeboxStatus jukeboxStatus; - private float gain = 0.5f; - private VolumeToast volumeToast; - private final AtomicBoolean running = new AtomicBoolean(); - private Thread serviceThread; - private boolean enabled = false; - - // TODO: These create circular references, try to refactor - private final Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private final Downloader downloader; - - // TODO: Report warning if queue fills up. - // TODO: Create shutdown method? - // TODO: Disable repeat. - // TODO: Persist RC state? - // TODO: Minimize status updates. - - public JukeboxMediaPlayer(Downloader downloader) - { - this.downloader = downloader; - } - - public void startJukeboxService() - { - if (running.get()) - { - return; - } - - running.set(true); - startProcessTasks(); - Timber.d("Started Jukebox Service"); - } - - public void stopJukeboxService() - { - running.set(false); - Util.sleepQuietly(1000); - - if (serviceThread != null) - { - serviceThread.interrupt(); - } - Timber.d("Stopped Jukebox Service"); - } - - private void startProcessTasks() - { - serviceThread = new Thread() - { - @Override - public void run() - { - processTasks(); - } - }; - - serviceThread.start(); - } - - private synchronized void startStatusUpdate() - { - stopStatusUpdate(); - - Runnable updateTask = new Runnable() - { - @Override - public void run() - { - tasks.remove(GetStatus.class); - tasks.add(new GetStatus()); - } - }; - - statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS, STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS); - } - - private synchronized void stopStatusUpdate() - { - if (statusUpdateFuture != null) - { - statusUpdateFuture.cancel(false); - statusUpdateFuture = null; - } - } - - private void processTasks() - { - while (running.get()) - { - JukeboxTask task = null; - - try - { - if (!ActiveServerProvider.Companion.isOffline()) - { - task = tasks.take(); - JukeboxStatus status = task.execute(); - onStatusUpdate(status); - } - } - catch (InterruptedException ignored) - { - - } - catch (Throwable x) - { - onError(task, x); - } - - Util.sleepQuietly(1); - } - } - - private void onStatusUpdate(JukeboxStatus jukeboxStatus) - { - timeOfLastUpdate.set(System.currentTimeMillis()); - this.jukeboxStatus = jukeboxStatus; - - // Track change? - Integer index = jukeboxStatus.getCurrentPlayingIndex(); - - if (index != null && index != -1 && index != downloader.getCurrentPlayingIndex()) - { - mediaPlayerControllerLazy.getValue().setCurrentPlaying(index); - } - } - - private void onError(JukeboxTask task, Throwable x) - { - if (x instanceof ApiNotSupportedException && !(task instanceof Stop)) - { - disableJukeboxOnError(x, R.string.download_jukebox_server_too_old); - } - else if (x instanceof OfflineException && !(task instanceof Stop)) - { - disableJukeboxOnError(x, R.string.download_jukebox_offline); - } - else if (x instanceof SubsonicRESTException && ((SubsonicRESTException) x).getCode() == 50 && !(task instanceof Stop)) - { - disableJukeboxOnError(x, R.string.download_jukebox_not_authorized); - } - else - { - Timber.e(x, "Failed to process jukebox task"); - } - } - - private void disableJukeboxOnError(Throwable x, final int resourceId) - { - Timber.w(x.toString()); - Context context = UApp.Companion.applicationContext(); - new Handler().post(() -> Util.toast(context, resourceId, false)); - - mediaPlayerControllerLazy.getValue().setJukeboxEnabled(false); - } - - public void updatePlaylist() - { - if (!enabled) return; - - tasks.remove(Skip.class); - tasks.remove(Stop.class); - tasks.remove(Start.class); - - List ids = new ArrayList<>(); - for (DownloadFile file : downloader.getAll()) - { - ids.add(file.getTrack().getId()); - } - - tasks.add(new SetPlaylist(ids)); - } - - public void skip(final int index, final int offsetSeconds) - { - tasks.remove(Skip.class); - tasks.remove(Stop.class); - tasks.remove(Start.class); - - startStatusUpdate(); - - if (jukeboxStatus != null) - { - jukeboxStatus.setPositionSeconds(offsetSeconds); - } - - tasks.add(new Skip(index, offsetSeconds)); - mediaPlayerControllerLazy.getValue().setPlayerState(PlayerState.STARTED); - } - - public void stop() - { - tasks.remove(Stop.class); - tasks.remove(Start.class); - - stopStatusUpdate(); - - tasks.add(new Stop()); - } - - public void start() - { - tasks.remove(Stop.class); - tasks.remove(Start.class); - - startStatusUpdate(); - tasks.add(new Start()); - } - - public synchronized void adjustVolume(boolean up) - { - float delta = up ? 0.05f : -0.05f; - gain += delta; - gain = Math.max(gain, 0.0f); - gain = Math.min(gain, 1.0f); - - tasks.remove(SetGain.class); - tasks.add(new SetGain(gain)); - - Context context = UApp.Companion.applicationContext(); - if (volumeToast == null) volumeToast = new VolumeToast(context); - - volumeToast.setVolume(gain); - } - - private MusicService getMusicService() - { - return MusicServiceFactory.getMusicService(); - } - - public int getPositionSeconds() - { - if (jukeboxStatus == null || jukeboxStatus.getPositionSeconds() == null || timeOfLastUpdate.get() == 0) - { - return 0; - } - - if (jukeboxStatus.isPlaying()) - { - int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L); - return jukeboxStatus.getPositionSeconds() + secondsSinceLastUpdate; - } - - return jukeboxStatus.getPositionSeconds(); - } - - public void setEnabled(boolean enabled) - { - Timber.d("Jukebox Service setting enabled to %b", enabled); - this.enabled = enabled; - - tasks.clear(); - if (enabled) - { - updatePlaylist(); - } - - stop(); - } - - public boolean isEnabled() - { - return enabled; - } - - private static class TaskQueue - { - private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); - - void add(JukeboxTask jukeboxTask) - { - queue.add(jukeboxTask); - } - - JukeboxTask take() throws InterruptedException - { - return queue.take(); - } - - void remove(Class taskClass) - { - try - { - Iterator iterator = queue.iterator(); - - while (iterator.hasNext()) - { - JukeboxTask task = iterator.next(); - - if (taskClass.equals(task.getClass())) - { - iterator.remove(); - } - } - } - catch (Throwable x) - { - Timber.w(x, "Failed to clean-up task queue."); - } - } - - void clear() - { - queue.clear(); - } - } - - private abstract static class JukeboxTask - { - abstract JukeboxStatus execute() throws Exception; - - @NotNull - @Override - public String toString() - { - return getClass().getSimpleName(); - } - } - - private class GetStatus extends JukeboxTask - { - @Override - JukeboxStatus execute() throws Exception - { - return getMusicService().getJukeboxStatus(); - } - } - - private class SetPlaylist extends JukeboxTask - { - private final List ids; - - SetPlaylist(List ids) - { - this.ids = ids; - } - - @Override - JukeboxStatus execute() throws Exception - { - return getMusicService().updateJukeboxPlaylist(ids); - } - } - - private class Skip extends JukeboxTask - { - private final int index; - private final int offsetSeconds; - - Skip(int index, int offsetSeconds) - { - this.index = index; - this.offsetSeconds = offsetSeconds; - } - - @Override - JukeboxStatus execute() throws Exception - { - return getMusicService().skipJukebox(index, offsetSeconds); - } - } - - private class Stop extends JukeboxTask - { - @Override - JukeboxStatus execute() throws Exception - { - return getMusicService().stopJukebox(); - } - } - - private class Start extends JukeboxTask - { - @Override - JukeboxStatus execute() throws Exception - { - return getMusicService().startJukebox(); - } - } - - private class SetGain extends JukeboxTask - { - - private final float gain; - - private SetGain(float gain) - { - this.gain = gain; - } - - @Override - JukeboxStatus execute() throws Exception - { - return getMusicService().setJukeboxGain(gain); - } - } - - private static class VolumeToast extends Toast - { - - private final ProgressBar progressBar; - - public VolumeToast(Context context) - { - super(context); - setDuration(Toast.LENGTH_SHORT); - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(R.layout.jukebox_volume, null); - progressBar = (ProgressBar) view.findViewById(R.id.jukebox_volume_progress_bar); - setView(view); - setGravity(Gravity.TOP, 0, 0); - } - - public void setVolume(float volume) - { - progressBar.setProgress(Math.round(100 * volume)); - show(); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java deleted file mode 100644 index 23b013af..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.util; - -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.Track; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import timber.log.Timber; - -/** - * @author Sindre Mehus - * @version $Id$ - */ -public class ShufflePlayBuffer -{ - private static final int CAPACITY = 50; - private static final int REFILL_THRESHOLD = 40; - - private final List buffer = new ArrayList<>(); - private ScheduledExecutorService executorService; - private int currentServer; - - public boolean isEnabled = false; - - public ShufflePlayBuffer() - { - } - - public void onCreate() - { - executorService = Executors.newSingleThreadScheduledExecutor(); - Runnable runnable = this::refill; - executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS); - Timber.i("ShufflePlayBuffer created"); - } - - public void onDestroy() - { - executorService.shutdown(); - Timber.i("ShufflePlayBuffer destroyed"); - } - - public List get(int size) - { - clearBufferIfNecessary(); - - List result = new ArrayList<>(size); - synchronized (buffer) - { - while (!buffer.isEmpty() && result.size() < size) - { - result.add(buffer.remove(buffer.size() - 1)); - } - } - Timber.i("Taking %d songs from shuffle play buffer. %d remaining.", result.size(), buffer.size()); - return result; - } - - private void refill() - { - if (!isEnabled) return; - - // Check if active server has changed. - clearBufferIfNecessary(); - - if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected() && !ActiveServerProvider.Companion.isOffline())) - { - return; - } - - try - { - MusicService service = MusicServiceFactory.getMusicService(); - int n = CAPACITY - buffer.size(); - MusicDirectory songs = service.getRandomSongs(n); - - synchronized (buffer) - { - buffer.addAll(songs.getTracks()); - Timber.i("Refilled shuffle play buffer with %d songs.", songs.getTracks().size()); - } - } - catch (Exception x) - { - Timber.w(x, "Failed to refill shuffle play buffer."); - } - } - - private void clearBufferIfNecessary() - { - synchronized (buffer) - { - if (currentServer != ActiveServerProvider.Companion.getActiveServerId()) - { - currentServer = ActiveServerProvider.Companion.getActiveServerId(); - buffer.clear(); - } - } - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java deleted file mode 100644 index 227e3d2c..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java +++ /dev/null @@ -1,290 +0,0 @@ -package org.moire.ultrasonic.util; - -import org.moire.ultrasonic.domain.Track; -import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.Supplier; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.URLDecoder; -import java.net.UnknownHostException; -import java.util.StringTokenizer; - -import timber.log.Timber; - -public class StreamProxy implements Runnable -{ - private Thread thread; - private boolean isRunning; - private ServerSocket socket; - private int port; - private Supplier currentPlaying; - - public StreamProxy(Supplier currentPlaying) - { - - // Create listening socket - try - { - socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1})); - socket.setSoTimeout(5000); - port = socket.getLocalPort(); - this.currentPlaying = currentPlaying; - } - catch (UnknownHostException e) - { // impossible - } - catch (IOException e) - { - Timber.e(e, "IOException initializing server"); - } - } - - public int getPort() - { - return port; - } - - public void start() - { - thread = new Thread(this); - thread.start(); - } - - public void stop() - { - isRunning = false; - thread.interrupt(); - } - - @Override - public void run() - { - isRunning = true; - while (isRunning) - { - try - { - Socket client = socket.accept(); - if (client == null) - { - continue; - } - Timber.i("Client connected"); - - StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client); - if (task.processRequest()) - { - new Thread(task).start(); - } - - } - catch (SocketTimeoutException e) - { - // Do nothing - } - catch (IOException e) - { - Timber.e(e, "Error connecting to client"); - } - } - Timber.i("Proxy interrupted. Shutting down."); - } - - private class StreamToMediaPlayerTask implements Runnable { - String localPath; - Socket client; - int cbSkip; - - StreamToMediaPlayerTask(Socket client) { - this.client = client; - } - - private String readRequest() { - InputStream is; - String firstLine; - try { - is = client.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192); - firstLine = reader.readLine(); - } catch (IOException e) { - Timber.e(e, "Error parsing request"); - return null; - } - - if (firstLine == null) { - Timber.i("Proxy client closed connection without a request."); - return null; - } - - StringTokenizer st = new StringTokenizer(firstLine); - st.nextToken(); // method - String uri = st.nextToken(); - String realUri = uri.substring(1); - Timber.i(realUri); - - return realUri; - } - - boolean processRequest() { - final String uri = readRequest(); - if (uri == null || uri.isEmpty()) { - return false; - } - - // Read HTTP headers - Timber.i("Processing request: %s", uri); - - try { - localPath = URLDecoder.decode(uri, Constants.UTF_8); - } catch (UnsupportedEncodingException e) { - Timber.e(e, "Unsupported encoding"); - return false; - } - - Timber.i("Processing request for file %s", localPath); - if (Storage.INSTANCE.isPathExists(localPath)) return true; - - // Usually the .partial file will be requested here, but sometimes it has already - // been renamed, so check if it is completed since - String saveFileName = FileUtil.INSTANCE.getSaveFile(localPath); - String completeFileName = FileUtil.INSTANCE.getCompleteFile(saveFileName); - - if (Storage.INSTANCE.isPathExists(saveFileName)) { - localPath = saveFileName; - return true; - } - - if (Storage.INSTANCE.isPathExists(completeFileName)) { - localPath = completeFileName; - return true; - } - - Timber.e("File %s does not exist", localPath); - return false; - - } - - @Override - public void run() - { - Timber.i("Streaming song in background"); - DownloadFile downloadFile = currentPlaying == null? null : currentPlaying.get(); - Track song = downloadFile.getTrack(); - long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8; - Timber.i("Streaming fileSize: %d", fileSize); - - // Create HTTP header - String headers = "HTTP/1.0 200 OK\r\n"; - headers += "Content-Type: application/octet-stream\r\n"; - headers += "Connection: close\r\n"; - headers += "\r\n"; - - long cbToSend = fileSize - cbSkip; - OutputStream output = null; - byte[] buff = new byte[64 * 1024]; - - try - { - output = new BufferedOutputStream(client.getOutputStream(), 32 * 1024); - output.write(headers.getBytes()); - - if (!downloadFile.isWorkDone()) - { - // Loop as long as there's stuff to send - while (isRunning && !client.isClosed()) - { - // See if there's more to send - String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile(); - int cbSentThisBatch = 0; - - AbstractFile storageFile = Storage.INSTANCE.getFromPath(file); - if (storageFile != null) - { - InputStream input = storageFile.getFileInputStream(); - - try - { - long skip = input.skip(cbSkip); - int cbToSendThisBatch = input.available(); - - while (cbToSendThisBatch > 0) - { - int cbToRead = Math.min(cbToSendThisBatch, buff.length); - int cbRead = input.read(buff, 0, cbToRead); - - if (cbRead == -1) - { - break; - } - - cbToSendThisBatch -= cbRead; - cbToSend -= cbRead; - output.write(buff, 0, cbRead); - output.flush(); - cbSkip += cbRead; - cbSentThisBatch += cbRead; - } - } - finally - { - input.close(); - } - - // Done regardless of whether or not it thinks it is - if (downloadFile.isWorkDone() && cbSkip >= file.length()) - { - break; - } - } - - // If we did nothing this batch, block for a second - if (cbSentThisBatch == 0) - { - Timber.d("Blocking until more data appears (%d)", cbToSend); - Util.sleepQuietly(1000L); - } - } - } - else - { - Timber.w("Requesting data for completely downloaded file"); - } - } - catch (SocketException socketException) - { - Timber.e("SocketException() thrown, proxy client has probably closed. This can exit harmlessly"); - } - catch (Exception e) - { - Timber.e("Exception thrown from streaming task:"); - Timber.e("%s : %s", e.getClass().getName(), e.getLocalizedMessage()); - } - - // Cleanup - try - { - if (output != null) - { - output.close(); - } - client.close(); - } - catch (IOException e) - { - Timber.e("IOException while cleaning up streaming task:"); - Timber.e("%s : %s", e.getClass().getName(), e.getLocalizedMessage()); - } - } - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java index 1de45b44..3a567399 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java @@ -130,7 +130,7 @@ public class VisualizerView extends View return; } - if (mediaPlayerControllerLazy.getValue().getPlayerState() != PlayerState.STARTED) + if (mediaPlayerControllerLazy.getValue().getLegacyPlayerState() != PlayerState.STARTED) { return; } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index a82d93d1..fac3e504 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -27,6 +27,8 @@ import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.FragmentContainerView +import androidx.media3.common.Player.STATE_BUFFERING +import androidx.media3.common.Player.STATE_READY import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment @@ -414,9 +416,9 @@ class NavigationActivity : AppCompatActivity() { } if (nowPlayingView != null) { - val playerState: PlayerState = mediaPlayerController.playerState - if (playerState == PlayerState.PAUSED || playerState == PlayerState.STARTED) { - val file: DownloadFile? = mediaPlayerController.currentPlaying + val playerState: Int = mediaPlayerController.playbackState + if (playerState == STATE_BUFFERING || playerState == STATE_READY) { + val file: DownloadFile? = mediaPlayerController.currentPlayingLegacy if (file != null) { nowPlayingView?.visibility = View.VISIBLE } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 0c9028f0..4efe0338 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -17,7 +17,7 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.Downloader class TrackViewBinder( - val onItemClick: (DownloadFile) -> Unit, + val onItemClick: (DownloadFile, Int) -> Unit, val onContextMenuClick: ((MenuItem, DownloadFile) -> Boolean)? = null, val checkable: Boolean, val draggable: Boolean, @@ -29,7 +29,7 @@ class TrackViewBinder( // Set our layout files val layout = R.layout.list_item_track - val contextMenuLayout = R.menu.context_menu_track + private val contextMenuLayout = R.menu.context_menu_track private val downloader: Downloader by inject() private val imageHelper: Utils.ImageHelper = Utils.ImageHelper(context) @@ -41,15 +41,14 @@ class TrackViewBinder( @SuppressLint("ClickableViewAccessibility") @Suppress("LongMethod") override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { - val downloadFile: DownloadFile? val diffAdapter = adapter as BaseAdapter<*> - when (item) { + val downloadFile: DownloadFile = when (item) { is Track -> { - downloadFile = downloader.getDownloadFileForSong(item) + downloader.getDownloadFileForSong(item) } is DownloadFile -> { - downloadFile = item + item } else -> { return @@ -90,7 +89,7 @@ class TrackViewBinder( val nowChecked = !holder.check.isChecked holder.isChecked = nowChecked } else { - onItemClick(downloadFile) + onItemClick(downloadFile, holder.bindingAdapterPosition) } } @@ -103,41 +102,37 @@ class TrackViewBinder( // Notify the adapter of selection changes holder.observableChecked.observe( - lifecycleOwner, - { isCheckedNow -> - if (isCheckedNow) { - diffAdapter.notifySelected(holder.entry!!.longId) - } else { - diffAdapter.notifyUnselected(holder.entry!!.longId) - } + lifecycleOwner + ) { isCheckedNow -> + if (isCheckedNow) { + diffAdapter.notifySelected(holder.entry!!.longId) + } else { + diffAdapter.notifyUnselected(holder.entry!!.longId) } - ) + } // Listen to changes in selection status and update ourselves diffAdapter.selectionRevision.observe( - lifecycleOwner, - { - val newStatus = diffAdapter.isSelected(item.longId) + lifecycleOwner + ) { + val newStatus = diffAdapter.isSelected(item.longId) - if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus - } - ) + if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus + } // Observe download status downloadFile.status.observe( - lifecycleOwner, - { - holder.updateStatus(it) - diffAdapter.notifyChanged() - } - ) + lifecycleOwner + ) { + holder.updateStatus(it) + diffAdapter.notifyChanged() + } downloadFile.progress.observe( - lifecycleOwner, - { - holder.updateProgress(it) - } - ) + lifecycleOwner + ) { + holder.updateProgress(it) + } } override fun onViewRecycled(holder: TrackViewHolder) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 23a885d2..2b9c2ec5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -109,7 +109,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } rxSubscription = RxBus.playerStateObservable.subscribe { - setPlayIcon(it.track == downloadFile) + setPlayIcon(it.index == bindingAdapterPosition && it.track == downloadFile) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt index 3d0298e5..9bffd3f8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt @@ -4,7 +4,6 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider -import org.moire.ultrasonic.util.MediaSessionHandler /** * This Koin module contains the registration of general classes needed for Ultrasonic @@ -12,5 +11,4 @@ import org.moire.ultrasonic.util.MediaSessionHandler val applicationModule = module { single { ActiveServerProvider(get()) } single { ImageLoaderProvider(androidContext()) } - single { MediaSessionHandler() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt index d08c8f20..658893ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt @@ -1,15 +1,13 @@ package org.moire.ultrasonic.di import org.koin.dsl.module -import org.moire.ultrasonic.service.AudioFocusHandler +import org.moire.ultrasonic.playback.LegacyPlaylistManager import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.service.ExternalStorageMonitor import org.moire.ultrasonic.service.JukeboxMediaPlayer -import org.moire.ultrasonic.service.LocalMediaPlayer import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.PlaybackStateSerializer -import org.moire.ultrasonic.util.ShufflePlayBuffer /** * This Koin module contains the registration of classes related to the media player @@ -19,10 +17,8 @@ val mediaPlayerModule = module { single { MediaPlayerLifecycleSupport() } single { PlaybackStateSerializer() } single { ExternalStorageMonitor() } - single { ShufflePlayBuffer() } - single { Downloader(get(), get(), get()) } - single { LocalMediaPlayer() } - single { AudioFocusHandler(get()) } + single { LegacyPlaylistManager() } + single { Downloader(get(), get()) } // TODO Ideally this can be cleaned up when all circular references are removed. single { MediaPlayerController(get(), get(), get(), get(), get()) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 86847435..913154b4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -54,7 +54,7 @@ class DownloadsFragment : MultiListFragment() { viewAdapter.register( TrackViewBinder( - { }, + { _, _ -> }, { _, _ -> true }, checkable = false, draggable = false, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 95724b59..9a1f2489 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -47,7 +47,7 @@ class NowPlayingFragment : Fragment() { private var nowPlayingTrack: TextView? = null private var nowPlayingArtist: TextView? = null - private var playerStateSubscription: Disposable? = null + private var rxBusSubscription: Disposable? = null private val mediaPlayerController: MediaPlayerController by inject() private val imageLoader: ImageLoaderProvider by inject() @@ -69,8 +69,7 @@ class NowPlayingFragment : Fragment() { nowPlayingAlbumArtImage = view.findViewById(R.id.now_playing_image) nowPlayingTrack = view.findViewById(R.id.now_playing_trackname) nowPlayingArtist = view.findViewById(R.id.now_playing_artist) - playerStateSubscription = - RxBus.playerStateObservable.subscribe { update() } + rxBusSubscription = RxBus.playerStateObservable.subscribe { update() } } override fun onResume() { @@ -80,13 +79,13 @@ class NowPlayingFragment : Fragment() { override fun onDestroy() { super.onDestroy() - playerStateSubscription!!.dispose() + rxBusSubscription!!.dispose() } @SuppressLint("ClickableViewAccessibility") private fun update() { try { - val playerState = mediaPlayerController.playerState + val playerState = mediaPlayerController.legacyPlayerState if (playerState === PlayerState.PAUSED) { playButton!!.setImageDrawable( @@ -102,7 +101,7 @@ class NowPlayingFragment : Fragment() { ) } - val file = mediaPlayerController.currentPlaying + val file = mediaPlayerController.currentPlayingLegacy if (file != null) { val song = file.track diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 53913818..f5b266e2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -13,6 +13,7 @@ import android.graphics.Point import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Handler +import android.os.Looper import android.view.ContextMenu import android.view.ContextMenu.ContextMenuInfo import android.view.GestureDetector @@ -35,24 +36,15 @@ import android.widget.TextView import android.widget.ViewFlipper import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.media3.common.Player +import androidx.media3.common.Timeline import androidx.navigation.Navigation import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView -import io.reactivex.rxjava3.disposables.Disposable -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.ArrayList -import java.util.Date -import java.util.Locale -import java.util.concurrent.CancellationException -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit -import kotlin.math.abs -import kotlin.math.max +import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -66,15 +58,13 @@ import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.Identifiable -import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.domain.RepeatMode import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.service.DownloadFile -import org.moire.ultrasonic.service.LocalMediaPlayer import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler @@ -86,9 +76,20 @@ import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.view.AutoRepeatButton import org.moire.ultrasonic.view.VisualizerView import timber.log.Timber +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException +import kotlin.math.abs +import kotlin.math.max /** * Contains the Music Player screen of Ultrasonic with playback controls and the playlist + * TODO: Add timeline lister -> updateProgressBar(). */ @Suppress("LargeClass", "TooManyFunctions", "MagicNumber") class PlayerFragment : @@ -113,14 +114,13 @@ class PlayerFragment : // Data & Services private val networkAndStorageChecker: NetworkAndStorageChecker by inject() private val mediaPlayerController: MediaPlayerController by inject() - private val localMediaPlayer: LocalMediaPlayer by inject() private val shareHandler: ShareHandler by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() - private lateinit var executorService: ScheduledExecutorService private var currentPlaying: DownloadFile? = null private var currentSong: Track? = null private lateinit var viewManager: LinearLayoutManager - private var rxBusSubscription: Disposable? = null + private var rxBusSubscription: CompositeDisposable = CompositeDisposable() + private lateinit var executorService: ScheduledExecutorService private var ioScope = CoroutineScope(Dispatchers.IO) // Views and UI Elements @@ -148,7 +148,7 @@ class PlayerFragment : private lateinit var durationTextView: TextView private lateinit var pauseButton: View private lateinit var stopButton: View - private lateinit var startButton: View + private lateinit var playButton: View private lateinit var repeatButton: ImageView private lateinit var hollowStar: Drawable private lateinit var fullStar: Drawable @@ -189,7 +189,7 @@ class PlayerFragment : pauseButton = view.findViewById(R.id.button_pause) stopButton = view.findViewById(R.id.button_stop) - startButton = view.findViewById(R.id.button_start) + playButton = view.findViewById(R.id.button_start) repeatButton = view.findViewById(R.id.button_repeat) visualizerViewLayout = view.findViewById(R.id.current_playing_visualizer_layout) fiveStar1ImageView = view.findViewById(R.id.song_five_star_1) @@ -216,13 +216,6 @@ class PlayerFragment : swipeVelocity = swipeDistance gestureScanner = GestureDetector(context, this) - // The secondary progress is an indicator of how far the song is cached. - localMediaPlayer.secondaryProgress.observe( - viewLifecycleOwner, - { - progressBar.secondaryProgress = it - } - ) findViews(view) val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous) @@ -291,34 +284,40 @@ class PlayerFragment : } } - startButton.setOnClickListener { + playButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - start() + mediaPlayerController.play() onCurrentChanged() onSliderProgressChanged() } } shuffleButton.setOnClickListener { - mediaPlayerController.shuffle() + mediaPlayerController.toggleShuffle() Util.toast(activity, R.string.download_menu_shuffle_notification) } repeatButton.setOnClickListener { - val repeatMode = mediaPlayerController.repeatMode.next() - mediaPlayerController.repeatMode = repeatMode + var newRepeat = mediaPlayerController.repeatMode + 1 + if (newRepeat == 3) { + newRepeat = 0 + } + + mediaPlayerController.repeatMode = newRepeat + onPlaylistChanged() - when (repeatMode) { - RepeatMode.OFF -> Util.toast( + + when (newRepeat) { + 0 -> Util.toast( context, R.string.download_repeat_off ) - RepeatMode.ALL -> Util.toast( - context, R.string.download_repeat_all - ) - RepeatMode.SINGLE -> Util.toast( + 1 -> Util.toast( context, R.string.download_repeat_single ) + 2 -> Util.toast( + context, R.string.download_repeat_all + ) else -> { } } @@ -351,53 +350,62 @@ class PlayerFragment : visualizerViewLayout.isVisible = false VisualizerController.get().observe( - requireActivity(), - { visualizerController -> - if (visualizerController != null) { - Timber.d("VisualizerController Observer.onChanged received controller") - visualizerView = VisualizerView(context) - visualizerViewLayout.addView( - visualizerView, - LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT - ) + requireActivity() + ) { visualizerController -> + if (visualizerController != null) { + Timber.d("VisualizerController Observer.onChanged received controller") + visualizerView = VisualizerView(context) + visualizerViewLayout.addView( + visualizerView, + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT ) + ) - visualizerViewLayout.isVisible = visualizerView.isActive + visualizerViewLayout.isVisible = visualizerView.isActive - visualizerView.setOnTouchListener { _, _ -> - visualizerView.isActive = !visualizerView.isActive - mediaPlayerController.showVisualization = visualizerView.isActive - true - } - isVisualizerAvailable = true - } else { - Timber.d("VisualizerController Observer.onChanged has no controller") - visualizerViewLayout.isVisible = false - isVisualizerAvailable = false + visualizerView.setOnTouchListener { _, _ -> + visualizerView.isActive = !visualizerView.isActive + mediaPlayerController.showVisualization = visualizerView.isActive + true } + isVisualizerAvailable = true + } else { + Timber.d("VisualizerController Observer.onChanged has no controller") + visualizerViewLayout.isVisible = false + isVisualizerAvailable = false } - ) + } EqualizerController.get().observe( - requireActivity(), - { equalizerController -> - isEqualizerAvailable = if (equalizerController != null) { - Timber.d("EqualizerController Observer.onChanged received controller") - true - } else { - Timber.d("EqualizerController Observer.onChanged has no controller") - false - } + requireActivity() + ) { equalizerController -> + isEqualizerAvailable = if (equalizerController != null) { + Timber.d("EqualizerController Observer.onChanged received controller") + true + } else { + Timber.d("EqualizerController Observer.onChanged has no controller") + false } - ) + } // Observe playlist changes and update the UI - rxBusSubscription = RxBus.playlistObservable.subscribe { + // FIXME + rxBusSubscription += RxBus.playlistObservable.subscribe { onPlaylistChanged() } + rxBusSubscription += RxBus.playerStateObservable.subscribe { + update() + } + + mediaPlayerController.controller?.addListener(object : Player.Listener { + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + onSliderProgressChanged() + } + }) + // Query the Jukebox state in an IO Context ioScope.launch(CommunicationError.getHandler(context)) { try { @@ -412,16 +420,15 @@ class PlayerFragment : override fun onResume() { super.onResume() - if (mediaPlayerController.currentPlaying == null) { + if (mediaPlayerController.currentPlayingLegacy == null) { playlistFlipper.displayedChild = 1 } else { - // Download list and Album art must be updated when Resumed + // Download list and Album art must be updated when resumed onPlaylistChanged() onCurrentChanged() } - val handler = Handler() - // TODO Use Rx for Update instead of polling! + val handler = Handler(Looper.getMainLooper()) val runnable = Runnable { handler.post { update(cancellationToken) } } executorService = Executors.newSingleThreadScheduledExecutor() executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS) @@ -441,7 +448,7 @@ class PlayerFragment : // Scroll to current playing. private fun scrollToCurrent() { - val index = mediaPlayerController.playList.indexOf(currentPlaying) + val index = mediaPlayerController.currentMediaItemIndex if (index != -1) { val smoothScroller = LinearSmoothScroller(context) @@ -459,7 +466,7 @@ class PlayerFragment : } override fun onDestroyView() { - rxBusSubscription?.dispose() + rxBusSubscription.dispose() cancel("CoroutineScope cancelled because the view was destroyed") cancellationToken.cancel() super.onDestroyView() @@ -504,7 +511,7 @@ class PlayerFragment : visualizerMenuItem.isVisible = isVisualizerAvailable } val mediaPlayerController = mediaPlayerController - val downloadFile = mediaPlayerController.currentPlaying + val downloadFile = mediaPlayerController.currentPlayingLegacy if (downloadFile != null) { currentSong = downloadFile.track @@ -631,7 +638,7 @@ class PlayerFragment : return true } R.id.menu_shuffle -> { - mediaPlayerController.shuffle() + mediaPlayerController.toggleShuffle() Util.toast(context, R.string.download_menu_shuffle_notification) return true } @@ -768,10 +775,10 @@ class PlayerFragment : } } - private fun update(cancel: CancellationToken?) { - if (cancel!!.isCancellationRequested) return + private fun update(cancel: CancellationToken? = null) { + if (cancel?.isCancellationRequested == true) return val mediaPlayerController = mediaPlayerController - if (currentPlaying != mediaPlayerController.currentPlaying) { + if (currentPlaying != mediaPlayerController.currentPlayingLegacy) { onCurrentChanged() } onSliderProgressChanged() @@ -822,23 +829,6 @@ class PlayerFragment : scrollToCurrent() } - private fun start() { - val service = mediaPlayerController - val state = service.playerState - if (state === PlayerState.PAUSED || - state === PlayerState.COMPLETED || state === PlayerState.STOPPED - ) { - service.start() - } else if (state === PlayerState.IDLE) { - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - val current = mediaPlayerController.currentPlayingNumberOnPlaylist - if (current == -1) { - service.play(0) - } else { - service.play(current) - } - } - } private fun initPlaylistDisplay() { // Create a View Manager @@ -852,17 +842,17 @@ class PlayerFragment : } // Create listener - val listener: ((DownloadFile) -> Unit) = { file -> - val list = mediaPlayerController.playList - val index = list.indexOf(file) - mediaPlayerController.play(index) + val clickHandler: ((DownloadFile, Int) -> Unit) = { _, pos -> + mediaPlayerController.seekTo(pos, 0) + mediaPlayerController.prepare() + mediaPlayerController.play() onCurrentChanged() onSliderProgressChanged() } viewAdapter.register( TrackViewBinder( - onItemClick = listener, + onItemClick = clickHandler, checkable = false, draggable = true, context = requireContext(), @@ -879,62 +869,63 @@ class PlayerFragment : ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { - val from = viewHolder.bindingAdapterPosition - val to = target.bindingAdapterPosition + val from = viewHolder.bindingAdapterPosition + val to = target.bindingAdapterPosition - // Move it in the data set - mediaPlayerController.moveItemInPlaylist(from, to) - viewAdapter.submitList(mediaPlayerController.playList) + // Move it in the data set + mediaPlayerController.moveItemInPlaylist(from, to) + viewAdapter.submitList(mediaPlayerController.playList) - return true - } + return true + } - // Swipe to delete from playlist - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - val pos = viewHolder.bindingAdapterPosition - val file = mediaPlayerController.playList[pos] - mediaPlayerController.removeFromPlaylist(file) + // Swipe to delete from playlist + @SuppressLint("NotifyDataSetChanged") + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val pos = viewHolder.bindingAdapterPosition + val file = mediaPlayerController.playList[pos] + mediaPlayerController.removeFromPlaylist(file) - val songRemoved = String.format( - resources.getString(R.string.download_song_removed), - file.track.title - ) - Util.toast(context, songRemoved) + val songRemoved = String.format( + resources.getString(R.string.download_song_removed), + file.track.title + ) + Util.toast(context, songRemoved) - viewAdapter.submitList(mediaPlayerController.playList) - viewAdapter.notifyDataSetChanged() - } + viewAdapter.submitList(mediaPlayerController.playList) + viewAdapter.notifyDataSetChanged() + } - override fun onSelectedChanged( - viewHolder: RecyclerView.ViewHolder?, - actionState: Int - ) { - super.onSelectedChanged(viewHolder, actionState) + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) - if (actionState == ACTION_STATE_DRAG) { - viewHolder?.itemView?.alpha = 0.6f - } - } - - override fun clearView( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ) { - super.clearView(recyclerView, viewHolder) - - viewHolder.itemView.alpha = 1.0f - } - - override fun isLongPressDragEnabled(): Boolean { - return false + if (actionState == ACTION_STATE_DRAG) { + viewHolder?.itemView?.alpha = 0.6f } } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + + viewHolder.itemView.alpha = 1.0f + } + + override fun isLongPressDragEnabled(): Boolean { + return false + } + } ) dragTouchHelper.attachToRecyclerView(playlistView) @@ -950,32 +941,33 @@ class PlayerFragment : emptyTextView.isVisible = list.isEmpty() when (mediaPlayerController.repeatMode) { - RepeatMode.OFF -> repeatButton.setImageDrawable( + 0 -> repeatButton.setImageDrawable( Util.getDrawableFromAttribute( requireContext(), R.attr.media_repeat_off ) ) - RepeatMode.ALL -> repeatButton.setImageDrawable( - Util.getDrawableFromAttribute( - requireContext(), R.attr.media_repeat_all - ) - ) - RepeatMode.SINGLE -> repeatButton.setImageDrawable( + 1 -> repeatButton.setImageDrawable( Util.getDrawableFromAttribute( requireContext(), R.attr.media_repeat_single ) ) + 2 -> repeatButton.setImageDrawable( + Util.getDrawableFromAttribute( + requireContext(), R.attr.media_repeat_all + ) + ) else -> { } } } private fun onCurrentChanged() { - currentPlaying = mediaPlayerController.currentPlaying + currentPlaying = mediaPlayerController.currentPlayingLegacy + scrollToCurrent() val totalDuration = mediaPlayerController.playListDuration val totalSongs = mediaPlayerController.playlistSize.toLong() - val currentSongIndex = mediaPlayerController.currentPlayingNumberOnPlaylist + 1 + val currentSongIndex = mediaPlayerController.currentMediaItemIndex + 1 val duration = Util.formatTotalDuration(totalDuration) val trackFormat = String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs) @@ -992,7 +984,7 @@ class PlayerFragment : genreTextView.isVisible = (currentSong!!.genre != null && currentSong!!.genre!!.isNotBlank()) - var bitRate: String = "" + var bitRate = "" if (currentSong!!.bitRate != null && currentSong!!.bitRate!! > 0) bitRate = String.format( Util.appContext().getString(R.string.song_details_kbps), @@ -1027,14 +1019,14 @@ class PlayerFragment : } } - @Suppress("LongMethod", "ComplexMethod") @Synchronized private fun onSliderProgressChanged() { val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) val duration: Int = mediaPlayerController.playerDuration - val playerState: PlayerState = mediaPlayerController.playerState + val playbackState: Int = mediaPlayerController.playbackState + val isPlaying = mediaPlayerController.isPlaying if (cancellationToken.isCancellationRequested) return if (currentPlaying != null) { @@ -1043,7 +1035,7 @@ class PlayerFragment : progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug. progressBar.progress = millisPlayed - progressBar.isEnabled = currentPlaying!!.isWorkDone || isJukeboxEnabled + progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled } else { positionTextView.setText(R.string.util_zero_time) durationTextView.setText(R.string.util_no_time) @@ -1052,21 +1044,20 @@ class PlayerFragment : progressBar.isEnabled = false } - when (playerState) { - PlayerState.DOWNLOADING -> { - val progress = - if (currentPlaying != null) currentPlaying!!.progress.value!! else 0 + val progress = mediaPlayerController.bufferedPercentage + + when (playbackState) { + Player.STATE_BUFFERING -> { + val downloadStatus = resources.getString( R.string.download_playerstate_downloading, Util.formatPercentage(progress) ) + progressBar.secondaryProgress = progress setTitle(this@PlayerFragment, downloadStatus) } - PlayerState.PREPARING -> setTitle( - this@PlayerFragment, - R.string.download_playerstate_buffering - ) - PlayerState.STARTED -> { + Player.STATE_READY -> { + progressBar.secondaryProgress = progress if (mediaPlayerController.isShufflePlayEnabled) { setTitle( this@PlayerFragment, @@ -1076,30 +1067,28 @@ class PlayerFragment : setTitle(this@PlayerFragment, R.string.common_appname) } } - PlayerState.IDLE, - PlayerState.PREPARED, - PlayerState.STOPPED, - PlayerState.PAUSED, - PlayerState.COMPLETED -> { + Player.STATE_IDLE, + Player.STATE_ENDED, + -> { } else -> setTitle(this@PlayerFragment, R.string.common_appname) } - when (playerState) { - PlayerState.STARTED -> { - pauseButton.isVisible = true + when (playbackState) { + Player.STATE_READY -> { + pauseButton.isVisible = isPlaying stopButton.isVisible = false - startButton.isVisible = false + playButton.isVisible = !isPlaying } - PlayerState.DOWNLOADING, PlayerState.PREPARING -> { + Player.STATE_BUFFERING -> { pauseButton.isVisible = false stopButton.isVisible = true - startButton.isVisible = false + playButton.isVisible = false } else -> { pauseButton.isVisible = false stopButton.isVisible = false - startButton.isVisible = true + playButton.isVisible = true } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index bfca7917..72baa959 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -109,7 +109,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { viewAdapter.register( TrackViewBinder( - onItemClick = ::onItemClick, + onItemClick = { file, _ -> onItemClick(file) }, onContextMenuClick = ::onContextMenuItemSelected, checkable = false, draggable = false, @@ -151,7 +151,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { val arguments = arguments val autoPlay = arguments != null && - arguments.getBoolean(Constants.INTENT_AUTOPLAY, false) + arguments.getBoolean(Constants.INTENT_AUTOPLAY, false) val query = arguments?.getString(Constants.INTENT_QUERY) // If started with a query, enter it to the searchView @@ -303,13 +303,12 @@ class SearchFragment : MultiListFragment(), KoinComponent { } mediaPlayerController.addToPlaylist( listOf(song), - save = false, + cachePermanently = false, autoPlay = false, - playNext = false, shuffle = false, - newPlaylist = false + insertionMode = MediaPlayerController.InsertionMode.APPEND ) - mediaPlayerController.play(mediaPlayerController.playlistSize - 1) + mediaPlayerController.play(mediaPlayerController.mediaItemCount - 1) toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index d676e0ee..730d8bd1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -34,7 +34,7 @@ class ServerSelectorFragment : Fragment() { private var listView: ListView? = null private val serverSettingsModel: ServerSettingsModel by viewModel() - private val service: MediaPlayerController by inject() + private val controller: MediaPlayerController by inject() private val activeServerProvider: ActiveServerProvider by inject() private var serverRowAdapter: ServerRowAdapter? = null @@ -117,14 +117,14 @@ class ServerSelectorFragment : Fragment() { // TODO this is still a blocking call - we shouldn't leave this activity before the active server is updated. // Maybe this can be refactored by using LiveData, or this can be made more user friendly with a ProgressDialog runBlocking { + controller.clearIncomplete() withContext(Dispatchers.IO) { if (activeServerProvider.getActiveServer().index != index) { - service.clearIncomplete() activeServerProvider.setActiveServerByIndex(index) - service.isJukeboxEnabled = - activeServerProvider.getActiveServer().jukeboxByDefault } } + controller.isJukeboxEnabled = + activeServerProvider.getActiveServer().jukeboxByDefault } Timber.i("Active server was set to: $index") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index b140e701..061d6f13 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -23,7 +23,7 @@ import androidx.preference.PreferenceFragmentCompat import java.io.File import kotlin.math.ceil import org.koin.core.component.KoinComponent -import org.koin.java.KoinJavaComponent.inject +import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle @@ -40,7 +40,6 @@ import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.FileUtil.ultrasonicDirectory import org.moire.ultrasonic.util.InfoDialog -import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings.preferences import org.moire.ultrasonic.util.Settings.shareGreeting @@ -89,12 +88,7 @@ class SettingsFragment : private var debugLogToFile: CheckBoxPreference? = null private var customCacheLocation: CheckBoxPreference? = null - private val mediaPlayerControllerLazy = inject( - MediaPlayerController::class.java - ) - private val mediaSessionHandler = inject( - MediaSessionHandler::class.java - ) + private val mediaPlayerController: MediaPlayerController by inject() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.settings, rootKey) @@ -221,9 +215,6 @@ class SettingsFragment : Constants.PREFERENCES_KEY_HIDE_MEDIA -> { setHideMedia(sharedPreferences.getBoolean(key, false)) } - Constants.PREFERENCES_KEY_MEDIA_BUTTONS -> { - setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true)) - } Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS -> { setBluetoothPreferences(sharedPreferences.getBoolean(key, true)) } @@ -433,11 +424,6 @@ class SettingsFragment : toast(activity, R.string.settings_hide_media_toast, false) } - private fun setMediaButtonsEnabled(enabled: Boolean) { - lockScreenEnabled!!.isEnabled = enabled - mediaSessionHandler.value.updateMediaButtonReceiver() - } - private fun setBluetoothPreferences(enabled: Boolean) { sendBluetoothAlbumArt!!.isEnabled = enabled } @@ -451,8 +437,8 @@ class SettingsFragment : Settings.cacheLocationUri = path // Clear download queue. - mediaPlayerControllerLazy.value.clear() - mediaPlayerControllerLazy.value.clearCaches() + mediaPlayerController.clear() + mediaPlayerController.clearCaches() Storage.reset() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index e0e990a9..3c9fcb39 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -122,7 +122,7 @@ open class TrackCollectionFragment : MultiListFragment() { viewAdapter.register( TrackViewBinder( - onItemClick = { onItemClick(it.track) }, + onItemClick = { file, _ -> onItemClick(file.track) }, onContextMenuClick = { menu, id -> onContextMenuItemSelected(menu, id.track) }, checkable = true, draggable = false, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt new file mode 100644 index 00000000..92f3244a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -0,0 +1,398 @@ +/* + * APIDataSource.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.playback + +import android.net.Uri +import androidx.core.net.toUri +import androidx.media3.common.C +import androidx.media3.common.MediaLibraryInfo +import androidx.media3.common.PlaybackException +import androidx.media3.common.util.Assertions +import androidx.media3.common.util.UnstableApi +import androidx.media3.common.util.Util +import androidx.media3.datasource.BaseDataSource +import androidx.media3.datasource.DataSourceException +import androidx.media3.datasource.DataSpec +import androidx.media3.datasource.HttpDataSource +import androidx.media3.datasource.HttpDataSource.HttpDataSourceException +import androidx.media3.datasource.HttpDataSource.InvalidResponseCodeException +import androidx.media3.datasource.HttpDataSource.RequestProperties +import androidx.media3.datasource.HttpUtil +import androidx.media3.datasource.TransferListener +import com.google.common.net.HttpHeaders +import okhttp3.CacheControl +import okhttp3.Call +import okhttp3.ResponseBody +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.moire.ultrasonic.api.subsonic.throwOnFailure +import org.moire.ultrasonic.api.subsonic.toStreamResponse +import org.moire.ultrasonic.util.AbstractFile +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.Storage +import timber.log.Timber +import java.io.IOException +import java.io.InputStream +import java.io.InterruptedIOException + +/** + * An [HttpDataSource] that delegates to Square's [Call.Factory]. + * + * + * Note: HTTP request headers will be set using all parameters passed via (in order of decreasing + * priority) the `dataSpec`, [.setRequestProperty] and the default parameters used to + * construct the instance. + */ +@UnstableApi +open class OkHttpDataSource private constructor( + subsonicAPIClient: SubsonicAPIClient, + userAgent: String?, + cacheControl: CacheControl?, + defaultRequestProperties: RequestProperties? +) : BaseDataSource(true), + HttpDataSource { + companion object { + init { + MediaLibraryInfo.registerModule("media3.datasource.okhttp") + } + } + + /** [DataSource.Factory] for [OkHttpDataSource] instances. */ + class Factory(private val subsonicAPIClient: SubsonicAPIClient) : HttpDataSource.Factory { + private val defaultRequestProperties: RequestProperties = RequestProperties() + private var userAgent: String? = null + private var transferListener: TransferListener? = null + private var cacheControl: CacheControl? = null + + override fun setDefaultRequestProperties(defaultRequestProperties: Map): Factory { + this.defaultRequestProperties.clearAndSet(defaultRequestProperties) + return this + } + + + /** + * Sets the [TransferListener] that will be used. + * + * + * The default is `null`. + * + * + * See [DataSource.addTransferListener]. + * + * @param transferListener The listener that will be used. + * @return This factory. + */ + fun setTransferListener(transferListener: TransferListener?): Factory { + this.transferListener = transferListener + return this + } + + override fun createDataSource(): OkHttpDataSource { + val dataSource = OkHttpDataSource( + subsonicAPIClient, + userAgent, + cacheControl, + defaultRequestProperties + ) + if (transferListener != null) { + dataSource.addTransferListener(transferListener!!) + } + return dataSource + } + + } + + + private val subsonicAPIClient: SubsonicAPIClient = Assertions.checkNotNull(subsonicAPIClient) + private val requestProperties: RequestProperties + private val userAgent: String? + private val cacheControl: CacheControl? + private val defaultRequestProperties: RequestProperties? + private var dataSpec: DataSpec? = null + private var response: retrofit2.Response? = null + private var responseByteStream: InputStream? = null + private var openedNetwork = false + private var openedFile = false + private var cachePath: String? = null + private var cacheFile: AbstractFile? = null + private var bytesToRead: Long = 0 + private var bytesRead: Long = 0 + + override fun getUri(): Uri? { + return when { + cachePath != null -> cachePath!!.toUri() + response == null -> null + else -> response!!.raw().request.url.toString().toUri() + } + } + + override fun getResponseCode(): Int { + return if (response == null) -1 else response!!.code() + } + + override fun getResponseHeaders(): Map> { + return if (response == null) emptyMap() else response!!.headers().toMultimap() + } + + override fun setRequestProperty(name: String, value: String) { + Assertions.checkNotNull(name) + Assertions.checkNotNull(value) + requestProperties[name] = value + } + + override fun clearRequestProperty(name: String) { + Assertions.checkNotNull(name) + requestProperties.remove(name) + } + + override fun clearAllRequestProperties() { + requestProperties.clear() + } + + @Throws(HttpDataSourceException::class) + override fun open(dataSpec: DataSpec): Long { + this.dataSpec = dataSpec + bytesRead = 0 + bytesToRead = 0 + + transferInitializing(dataSpec) + val components = dataSpec.uri.toString().split('|') + val id = components[0] + val bitrate = components[1].toInt() + val path = components[2] + + val cacheLength = checkCache(path) + + // We have found an item in the cache, return early + if (cacheLength > 0) { + bytesToRead = cacheLength + return bytesToRead + } + + Timber.i("DATASOURCE: %s", "Start") + val request = subsonicAPIClient.api.stream(id, bitrate, offset = 0) + val response: retrofit2.Response? + val streamResponse: StreamResponse + Timber.i("DATASOURCE: %s", "Start2") + try { + this.response = request.execute() + Timber.i("DATASOURCE: %s", "Start3") + response = this.response + streamResponse = response!!.toStreamResponse() + Timber.i("DATASOURCE: %s", "Start4") + responseByteStream = streamResponse.stream + Timber.i("DATASOURCE: %s", "Start5") + } catch (e: IOException) { + throw HttpDataSourceException.createForIOException( + e, dataSpec, HttpDataSourceException.TYPE_OPEN + ) + } + + streamResponse.throwOnFailure() + + val responseCode = response.code() + + // Check for a valid response code. + if (!response.isSuccessful) { + if (responseCode == 416) { + val documentSize = + HttpUtil.getDocumentSize(response.headers()[HttpHeaders.CONTENT_RANGE]) + if (dataSpec.position == documentSize) { + openedNetwork = true + transferStarted(dataSpec) + return if (dataSpec.length != C.LENGTH_UNSET.toLong()) dataSpec.length else 0 + } + } + val errorResponseBody: ByteArray = try { + Util.toByteArray(Assertions.checkNotNull(responseByteStream)) + } catch (e: IOException) { + Util.EMPTY_BYTE_ARRAY + } + val headers = response.headers().toMultimap() + closeConnectionQuietly() + val cause: IOException? = + if (responseCode == 416) DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) else null + throw InvalidResponseCodeException( + responseCode, response.message(), cause, headers, dataSpec, errorResponseBody + ) + } + + Timber.i("DATASOURCE: %s", "Start6") + + // If we requested a range starting from a non-zero position and received a 200 rather than a + // 206, then the server does not support partial requests. We'll need to manually skip to the + // requested position. + val bytesToSkip = + if (responseCode == 200 && dataSpec.position != 0L) dataSpec.position else 0 + + // Determine the length of the data to be read, after skipping. + bytesToRead = if (dataSpec.length != C.LENGTH_UNSET.toLong()) { + dataSpec.length + } else { + val contentLength = response.body()!!.contentLength() + if (contentLength != -1L) contentLength - bytesToSkip else C.LENGTH_UNSET.toLong() + } + openedNetwork = true + transferStarted(dataSpec) + try { + skipFully(bytesToSkip, dataSpec) + } catch (e: HttpDataSourceException) { + closeConnectionQuietly() + throw e + } + Timber.i("DATASOURCE: %s", "Start7") + + return bytesToRead + } + + @Throws(HttpDataSourceException::class) + override fun read(buffer: ByteArray, offset: Int, length: Int): Int { + return try { + readInternal(buffer, offset, length) + } catch (e: IOException) { + throw HttpDataSourceException.createForIOException( + e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ + ) + } + } + + override fun close() { + if (openedNetwork) { + openedNetwork = false + transferEnded() + closeConnectionQuietly() + } else if (openedFile) { + openedFile = false + responseByteStream?.close() + responseByteStream = null + } + } + + /** + * Checks our cache for a matching media file + */ + private fun checkCache(path: String): Long { + var filePath: String = path + var found = Storage.isPathExists(path) + + if (!found) { + filePath = FileUtil.getCompleteFile(path) + found = Storage.isPathExists(filePath) + } + + if (!found) return -1 + + cachePath = filePath + + cacheFile = Storage.getFromPath(filePath)!! + responseByteStream = cacheFile!!.getFileInputStream() + + return cacheFile!!.getDocumentFileDescriptor("r")!!.length + } + + /** + * Attempts to skip the specified number of bytes in full. + * + * @param bytesToSkip The number of bytes to skip. + * @param dataSpec The [DataSpec]. + * @throws HttpDataSourceException If the thread is interrupted during the operation, or an error + * occurs while reading from the source, or if the data ended before skipping the specified + * number of bytes. + */ + @Throws(HttpDataSourceException::class) + private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) { + var bytesToSkip = bytesToSkip + if (bytesToSkip == 0L) { + return + } + val skipBuffer = ByteArray(4096) + try { + while (bytesToSkip > 0) { + val readLength = + bytesToSkip.coerceAtMost(skipBuffer.size.toLong()).toInt() + val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength) + if (Thread.currentThread().isInterrupted) { + throw InterruptedIOException() + } + if (read == -1) { + throw HttpDataSourceException( + dataSpec, + PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, + HttpDataSourceException.TYPE_OPEN + ) + } + bytesToSkip -= read.toLong() + bytesTransferred(read) + } + return + } catch (e: IOException) { + if (e is HttpDataSourceException) { + throw e + } else { + throw HttpDataSourceException( + dataSpec, + PlaybackException.ERROR_CODE_IO_UNSPECIFIED, + HttpDataSourceException.TYPE_OPEN + ) + } + } + } + + /** + * Reads up to `length` bytes of data and stores them into `buffer`, starting at index + * `offset`. + * + * + * This method blocks until at least one byte of data can be read, the end of the opened range + * is detected, or an exception is thrown. + * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into `buffer` at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The number of bytes read, or [C.RESULT_END_OF_INPUT] if the end of the opened + * range is reached. + * @throws IOException If an error occurs reading from the source. + */ + @Throws(IOException::class) + private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int { + var readLength = readLength + if (readLength == 0) { + return 0 + } + if (bytesToRead != C.LENGTH_UNSET.toLong()) { + val bytesRemaining = bytesToRead - bytesRead + if (bytesRemaining == 0L) { + return C.RESULT_END_OF_INPUT + } + readLength = readLength.toLong().coerceAtMost(bytesRemaining).toInt() + } + val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLength) + if (read == -1) { + return C.RESULT_END_OF_INPUT + } + bytesRead += read.toLong() + // bytesTransferred(read) + return read + } + + /** Closes the current connection quietly, if there is one. */ + private fun closeConnectionQuietly() { + if (response != null) { + Assertions.checkNotNull(response!!.body()).close() + response = null + } + responseByteStream = null + } + + init { + this.userAgent = userAgent + this.cacheControl = cacheControl + this.defaultRequestProperties = defaultRequestProperties + requestProperties = RequestProperties() + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt new file mode 100644 index 00000000..ebe17602 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -0,0 +1,127 @@ +/* + * LegacyPlaylist.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.playback + +import androidx.media3.common.MediaItem +import androidx.media3.session.MediaController +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.service.DownloadFile +import org.moire.ultrasonic.service.Downloader +import org.moire.ultrasonic.service.JukeboxMediaPlayer +import org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.util.LRUCache +import timber.log.Timber + +/** + * This class keeps a legacy playlist maintained which + * reflects the internal timeline of the Media3.Player + */ +class LegacyPlaylistManager : KoinComponent { + + private val _playlist = mutableListOf() + + @JvmField + var currentPlaying: DownloadFile? = null + + private val mediaItemCache = LRUCache(1000) + + val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() + val downloader: Downloader by inject() + + private var playlistUpdateRevision: Long = 0 + private set(value) { + field = value + RxBus.playlistPublisher.onNext(_playlist) + } + + fun rebuildPlaylist(controller: MediaController) { + _playlist.clear() + + val n = controller.mediaItemCount + + for (i in 0 until n) { + val item = controller.getMediaItemAt(i) + val file = mediaItemCache[item.mediaMetadata.mediaUri.toString()] + if (file != null) + _playlist.add(file) + } + + playlistUpdateRevision++ + } + + fun addToCache(item: MediaItem, file: DownloadFile) { + mediaItemCache.put(item.mediaMetadata.mediaUri.toString(), file) + } + + fun updateCurrentPlaying(item: MediaItem?) { + currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()] + } + + @Synchronized + fun clearIncomplete() { + val iterator = _playlist.iterator() + var changedPlaylist = false + while (iterator.hasNext()) { + val downloadFile = iterator.next() + if (!downloadFile.isCompleteFileAvailable) { + iterator.remove() + changedPlaylist = true + } + } + if (changedPlaylist) playlistUpdateRevision++ + } + + @Synchronized + fun clearPlaylist() { + _playlist.clear() + playlistUpdateRevision++ + } + + fun onDestroy() { + clearPlaylist() + Timber.i("PlaylistManager destroyed") + } + + // Public facing playlist (immutable) + val playlist: List + get() = _playlist + + // FIXME: Returns wrong count if item is twice in queue + @get:Synchronized + val currentPlayingIndex: Int + get() = _playlist.indexOf(currentPlaying) + + @get:Synchronized + val playlistDuration: Long + get() { + var totalDuration: Long = 0 + for (downloadFile in _playlist) { + val song = downloadFile.track + if (!song.isDirectory) { + if (song.artist != null) { + if (song.duration != null) { + totalDuration += song.duration!!.toLong() + } + } + } + } + return totalDuration + } + + /** + * Extension function + * Gathers the download file for a given song, and modifies shouldSave if provided. + */ + fun Track.getDownloadFile(save: Boolean? = null): DownloadFile { + return downloader.getDownloadFileForSong(this).apply { + if (save != null) this.shouldSave = save + } + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt new file mode 100644 index 00000000..f3232bb8 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt @@ -0,0 +1,254 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.moire.ultrasonic.playback + +import android.content.res.AssetManager +import android.net.Uri +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS +import androidx.media3.common.util.Util +import com.google.common.collect.ImmutableList +import org.json.JSONObject + +/** + * A sample media catalog that represents media items as a tree. + * + * It fetched the data from {@code catalog.json}. The root's children are folders containing media + * items from the same album/artist/genre. + * + * Each app should have their own way of representing the tree. MediaItemTree is used for + * demonstration purpose only. + */ +object MediaItemTree { + private var treeNodes: MutableMap = mutableMapOf() + private var titleMap: MutableMap = mutableMapOf() + private var isInitialized = false + private const val ROOT_ID = "[rootID]" + private const val ALBUM_ID = "[albumID]" + private const val GENRE_ID = "[genreID]" + private const val ARTIST_ID = "[artistID]" + private const val ALBUM_PREFIX = "[album]" + private const val GENRE_PREFIX = "[genre]" + private const val ARTIST_PREFIX = "[artist]" + private const val ITEM_PREFIX = "[item]" + + private class MediaItemNode(val item: MediaItem) { + private val children: MutableList = ArrayList() + + fun addChild(childID: String) { + this.children.add(treeNodes[childID]!!.item) + } + + fun getChildren(): List { + return ImmutableList.copyOf(children) + } + } + + private fun buildMediaItem( + title: String, + mediaId: String, + isPlayable: Boolean, + @MediaMetadata.FolderType folderType: Int, + album: String? = null, + artist: String? = null, + genre: String? = null, + sourceUri: Uri? = null, + imageUri: Uri? = null, + ): MediaItem { + // TODO(b/194280027): add artwork + val metadata = + MediaMetadata.Builder() + .setAlbumTitle(album) + .setTitle(title) + .setArtist(artist) + .setGenre(genre) + .setFolderType(folderType) + .setIsPlayable(isPlayable) + .setArtworkUri(imageUri) + .build() + return MediaItem.Builder() + .setMediaId(mediaId) + .setMediaMetadata(metadata) + .setUri(sourceUri) + .build() + } + + @androidx.media3.common.util.UnstableApi + private fun loadJSONFromAsset(assets: AssetManager): String { + val buffer = assets.open("catalog.json").use { Util.toByteArray(it) } + return String(buffer, Charsets.UTF_8) + } + + fun initialize(assets: AssetManager) { + if (isInitialized) return + isInitialized = true + // create root and folders for album/artist/genre. + treeNodes[ROOT_ID] = + MediaItemNode( + buildMediaItem( + title = "Root Folder", + mediaId = ROOT_ID, + isPlayable = false, + folderType = FOLDER_TYPE_MIXED + ) + ) + treeNodes[ALBUM_ID] = + MediaItemNode( + buildMediaItem( + title = "Album Folder", + mediaId = ALBUM_ID, + isPlayable = false, + folderType = FOLDER_TYPE_MIXED + ) + ) + treeNodes[ARTIST_ID] = + MediaItemNode( + buildMediaItem( + title = "Artist Folder", + mediaId = ARTIST_ID, + isPlayable = false, + folderType = FOLDER_TYPE_MIXED + ) + ) + treeNodes[GENRE_ID] = + MediaItemNode( + buildMediaItem( + title = "Genre Folder", + mediaId = GENRE_ID, + isPlayable = false, + folderType = FOLDER_TYPE_MIXED + ) + ) + treeNodes[ROOT_ID]!!.addChild(ALBUM_ID) + treeNodes[ROOT_ID]!!.addChild(ARTIST_ID) + treeNodes[ROOT_ID]!!.addChild(GENRE_ID) + + // Here, parse the json file in asset for media list. + // We use a file in asset for demo purpose +// val jsonObject = JSONObject(loadJSONFromAsset(assets)) +// val mediaList = jsonObject.getJSONArray("media") +// +// // create subfolder with same artist, album, etc. +// for (i in 0 until mediaList.length()) { +// addNodeToTree(mediaList.getJSONObject(i)) +// } + } + + private fun addNodeToTree(mediaObject: JSONObject) { + + val id = mediaObject.getString("id") + val album = mediaObject.getString("album") + val title = mediaObject.getString("title") + val artist = mediaObject.getString("artist") + val genre = mediaObject.getString("genre") + val sourceUri = Uri.parse(mediaObject.getString("source")) + val imageUri = Uri.parse(mediaObject.getString("image")) + // key of such items in tree + val idInTree = ITEM_PREFIX + id + val albumFolderIdInTree = ALBUM_PREFIX + album + val artistFolderIdInTree = ARTIST_PREFIX + artist + val genreFolderIdInTree = GENRE_PREFIX + genre + + treeNodes[idInTree] = + MediaItemNode( + buildMediaItem( + title = title, + mediaId = idInTree, + isPlayable = true, + album = album, + artist = artist, + genre = genre, + sourceUri = sourceUri, + imageUri = imageUri, + folderType = FOLDER_TYPE_NONE + ) + ) + + titleMap[title.lowercase()] = treeNodes[idInTree]!! + + if (!treeNodes.containsKey(albumFolderIdInTree)) { + treeNodes[albumFolderIdInTree] = + MediaItemNode( + buildMediaItem( + title = album, + mediaId = albumFolderIdInTree, + isPlayable = true, + folderType = FOLDER_TYPE_PLAYLISTS + ) + ) + treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree) + } + treeNodes[albumFolderIdInTree]!!.addChild(idInTree) + + // add into artist folder + if (!treeNodes.containsKey(artistFolderIdInTree)) { + treeNodes[artistFolderIdInTree] = + MediaItemNode( + buildMediaItem( + title = artist, + mediaId = artistFolderIdInTree, + isPlayable = true, + folderType = FOLDER_TYPE_PLAYLISTS + ) + ) + treeNodes[ARTIST_ID]!!.addChild(artistFolderIdInTree) + } + treeNodes[artistFolderIdInTree]!!.addChild(idInTree) + + // add into genre folder + if (!treeNodes.containsKey(genreFolderIdInTree)) { + treeNodes[genreFolderIdInTree] = + MediaItemNode( + buildMediaItem( + title = genre, + mediaId = genreFolderIdInTree, + isPlayable = true, + folderType = FOLDER_TYPE_PLAYLISTS + ) + ) + treeNodes[GENRE_ID]!!.addChild(genreFolderIdInTree) + } + treeNodes[genreFolderIdInTree]!!.addChild(idInTree) + } + + fun getItem(id: String): MediaItem? { + return treeNodes[id]?.item + } + + fun getRootItem(): MediaItem { + return treeNodes[ROOT_ID]!!.item + } + + fun getChildren(id: String): List? { + return treeNodes[id]?.getChildren() + } + + fun getRandomItem(): MediaItem { + var curRoot = getRootItem() + while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) { + val children = getChildren(curRoot.mediaId)!! + curRoot = children.random() + } + return curRoot + } + + fun getItemFromTitle(title: String): MediaItem? { + return titleMap[title]?.item + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt new file mode 100644 index 00000000..4afd316d --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -0,0 +1,158 @@ +/* + * MediaNotificationProvider.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.playback + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.graphics.BitmapFactory +import android.os.Bundle +import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.media3.common.Player +import androidx.media3.common.util.Assertions +import androidx.media3.common.util.UnstableApi +import androidx.media3.common.util.Util +import androidx.media3.session.MediaController +import androidx.media3.session.MediaNotification +import androidx.media3.session.MediaNotification.ActionFactory +import org.moire.ultrasonic.R + +/* +* This is a copy of DefaultMediaNotificationProvider.java with some small changes +* I have opened a bug https://github.com/androidx/media/issues/65 to make it easier to customize +* the icons and actions without creating our own copy of this class.. + */ +@UnstableApi +/* package */ +internal class MediaNotificationProvider(context: Context) : + MediaNotification.Provider { + private val context: Context = context.applicationContext + private val notificationManager: NotificationManager = Assertions.checkStateNotNull( + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + ) + + override fun createNotification( + mediaController: MediaController, + actionFactory: ActionFactory, + onNotificationChangedCallback: MediaNotification.Provider.Callback + ): MediaNotification { + ensureNotificationChannel() + val builder: NotificationCompat.Builder = NotificationCompat.Builder( + context, + NOTIFICATION_CHANNEL_ID + ) + // TODO(b/193193926): Filter actions depending on the player's available commands. + // Skip to previous action. + builder.addAction( + actionFactory.createMediaAction( + IconCompat.createWithResource( + context, + R.drawable.media3_notification_seek_to_previous + ), + context.getString(R.string.media3_controls_seek_to_previous_description), + ActionFactory.COMMAND_SKIP_TO_PREVIOUS + ) + ) + if (mediaController.playbackState == Player.STATE_ENDED + || !mediaController.playWhenReady + ) { + // Play action. + builder.addAction( + actionFactory.createMediaAction( + IconCompat.createWithResource(context, R.drawable.media3_notification_play), + context.getString(R.string.media3_controls_play_description), + ActionFactory.COMMAND_PLAY + ) + ) + } else { + // Pause action. + builder.addAction( + actionFactory.createMediaAction( + IconCompat.createWithResource(context, R.drawable.media3_notification_pause), + context.getString(R.string.media3_controls_pause_description), + ActionFactory.COMMAND_PAUSE + ) + ) + } + // Skip to next action. + builder.addAction( + actionFactory.createMediaAction( + IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next), + context.getString(R.string.media3_controls_seek_to_next_description), + ActionFactory.COMMAND_SKIP_TO_NEXT + ) + ) + + // Set metadata info in the notification. + val metadata = mediaController.mediaMetadata + builder.setContentTitle(metadata.title).setContentText(metadata.artist) + if (metadata.artworkData != null) { + val artworkBitmap = + BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData!!.size) + builder.setLargeIcon(artworkBitmap) + } + val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle() + .setCancelButtonIntent( + actionFactory.createMediaActionPendingIntent( + ActionFactory.COMMAND_STOP + ) + ) + .setShowActionsInCompactView(0, 1, 2) + val notification: Notification = builder + .setContentIntent(mediaController.sessionActivity) + .setDeleteIntent( + actionFactory.createMediaActionPendingIntent( + ActionFactory.COMMAND_STOP + ) + ) + .setOnlyAlertOnce(true) + .setSmallIcon(getSmallIconResId()) + .setStyle(mediaStyle) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setOngoing(false) + .build() + return MediaNotification( + NOTIFICATION_ID, + notification + ) + } + + override fun handleCustomAction( + mediaController: MediaController, + action: String, + extras: Bundle + ) { + // We don't handle custom commands. + } + + private fun ensureNotificationChannel() { + if (Util.SDK_INT < 26 + || notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null + ) { + return + } + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(channel) + } + + companion object { + private const val NOTIFICATION_ID = 1001 + private const val NOTIFICATION_CHANNEL_ID = "default_channel_id" + private const val NOTIFICATION_CHANNEL_NAME = "Now playing" + private fun getSmallIconResId(): Int { + return R.drawable.ic_stat_ultrasonic + } + } + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md new file mode 100644 index 00000000..7f9a4100 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md @@ -0,0 +1,18 @@ + + + +UI: +[x] Display tracks +[x] On selection: Translate Tracks to MediaItems +[x] Move playlist val to Controller: Keep it around for easier migration!! +[x] Also make a LRU Cache to help with translation between MediaItem and DownloadFile +[x] Hand MediaItems to Service +[] If wanted also hand them to Downloader.kt +[x] Service plays MediaItem through OkHttp +[x] UI needs to receive info from service +[] Create a Cache Layer +[] Translate AutoMediaBrowserService +[] Add new shuffle icon.... + +DownloadNotificationHelper +convertToPlaybackStateCompatState() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt new file mode 100644 index 00000000..bd98fb84 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.moire.ultrasonic.playback + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.C.CONTENT_TYPE_MUSIC +import androidx.media3.common.C.USAGE_MEDIA +import androidx.media3.common.MediaItem +import androidx.media3.datasource.DataSource +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +import androidx.media3.session.SessionResult +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.activity.NavigationActivity +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.util.Constants + + +class PlaybackService : MediaLibraryService(), KoinComponent { + private lateinit var player: ExoPlayer + private lateinit var mediaLibrarySession: MediaLibrarySession + private lateinit var dataSourceFactory: DataSource.Factory + + private val librarySessionCallback = CustomMediaLibrarySessionCallback() + + companion object { + private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" + private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" + } + + private inner class CustomMediaLibrarySessionCallback : + MediaLibrarySession.MediaLibrarySessionCallback { + override fun onGetLibraryRoot( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: LibraryParams? + ): ListenableFuture> { + return Futures.immediateFuture( + LibraryResult.ofItem( + MediaItemTree.getRootItem(), + params + ) + ) + } + + override fun onGetItem( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + mediaId: String + ): ListenableFuture> { + val item = + MediaItemTree.getItem(mediaId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) + } + + override fun onGetChildren( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: LibraryParams? + ): ListenableFuture>> { + val children = + MediaItemTree.getChildren(parentId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + + return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) + } + + private fun setMediaItemFromSearchQuery(query: String) { + // Only accept query with pattern "play [Title]" or "[Title]" + // Where [Title]: must be exactly matched + // If no media with exact name found, play a random media instead + val mediaTitle = + if (query.startsWith("play ", ignoreCase = true)) { + query.drop(5) + } else { + query + } + + val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() + player.setMediaItem(item) + } + + override fun onSetMediaUri( + session: MediaSession, + controller: MediaSession.ControllerInfo, + uri: Uri, + extras: Bundle + ): Int { + + if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || + uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) + ) { + val searchQuery = + uri.getQueryParameter("query") + ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED + setMediaItemFromSearchQuery(searchQuery) + + return SessionResult.RESULT_SUCCESS + } else { + return SessionResult.RESULT_ERROR_NOT_SUPPORTED + } + } + } + + /* + * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, + * and thereby customarily it is required to rebuild it.. + */ + private class CustomMediaItemFiller : MediaSession.MediaItemFiller { + override fun fillInLocalConfiguration( + session: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItem: MediaItem + ): MediaItem { + // Again, set the Uri, so that it will get a LocalConfiguration + val item = mediaItem.buildUpon() + .setUri(mediaItem.mediaMetadata.mediaUri) + .build() + + return item + + } + } + + override fun onCreate() { + super.onCreate() + initializeSessionAndPlayer() + } + + override fun onDestroy() { + player.release() + mediaLibrarySession.release() + super.onDestroy() + } + + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession { + return mediaLibrarySession + } + + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) + private fun initializeSessionAndPlayer() { + /* + * TODO: + * * Could be refined to use WAKE_MODE_LOCAL when offline.... + */ + + + setMediaNotificationProvider(MediaNotificationProvider(UApp.applicationContext())) + + + val subsonicAPIClient: SubsonicAPIClient by inject() + + // Create a MediaSource which passes calls through our OkHttp Stack + dataSourceFactory = OkHttpDataSource.Factory(subsonicAPIClient) + + + // A download cache should not evict media, so should use a NoopCacheEvictor. + // A download cache should not evict media, so should use a NoopCacheEvictor. + // TODO: Add cache: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer +// var cache = UltrasonicCache() +// +// val cacheDataSourceFactory: DataSource.Factory = CacheDataSource.Factory() +// .setCache(cache) +// .setUpstreamDataSourceFactory(dataSourceFactory) +// .setCacheWriteDataSinkFactory(null) // Disable writing. + + + // Create a renderer with HW rendering support + val renderer = DefaultRenderersFactory(this) + renderer.setEnableAudioOffload(true) + + // Create the player + player = ExoPlayer.Builder(this) + .setAudioAttributes(getAudioAttributes(), true) + .setWakeMode(C.WAKE_MODE_NETWORK) + .setHandleAudioBecomingNoisy(true) + .setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory)) + //.setRenderersFactory(renderer) + .build() + + // Enable audio offload + //player.experimentalSetOffloadSchedulingEnabled(true) + + MediaItemTree.initialize(assets) + + // THIS Will need to use the AutoCalls + mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) + .setMediaItemFiller(CustomMediaItemFiller()) + .setSessionActivity(getPendingIntentForContent()) + .build() + } + + @SuppressLint("UnspecifiedImmutableFlag") + private fun getPendingIntentForContent(): PendingIntent { + val intent = Intent(this, NavigationActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + val flags = PendingIntent.FLAG_UPDATE_CURRENT + intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) + return PendingIntent.getActivity(this, 0, intent, flags) + } + + private fun getAudioAttributes(): AudioAttributes { + return AudioAttributes.Builder() + .setUsage(USAGE_MEDIA) + .setContentType(CONTENT_TYPE_MUSIC) + .build() + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt new file mode 100644 index 00000000..f3b59499 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt @@ -0,0 +1,99 @@ +/* + * UltrasonicCache.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.playback + +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.cache.Cache +import androidx.media3.datasource.cache.CacheSpan +import androidx.media3.datasource.cache.ContentMetadata +import androidx.media3.datasource.cache.ContentMetadataMutations +import java.io.File +import java.util.NavigableSet + +@UnstableApi + +class UltrasonicCache : Cache { + override fun getUid(): Long { + TODO("Not yet implemented") + } + + override fun release() { + // R/O Cache Implementation + } + + override fun addListener(key: String, listener: Cache.Listener): NavigableSet { + // Not (yet?) implemented + return emptySet() as NavigableSet + } + + override fun removeListener(key: String, listener: Cache.Listener) { + // Not (yet?) implemented + } + + override fun getCachedSpans(key: String): NavigableSet { + TODO("Not yet implemented") + } + + override fun getKeys(): MutableSet { + TODO("Not yet implemented") + } + + override fun getCacheSpace(): Long { + TODO("Not yet implemented") + } + + override fun startReadWrite(key: String, position: Long, length: Long): CacheSpan { + TODO("Not yet implemented") + } + + override fun startReadWriteNonBlocking(key: String, position: Long, length: Long): CacheSpan? { + TODO("Not yet implemented") + } + + override fun startFile(key: String, position: Long, length: Long): File { + // R/O Cache Implementation + return File("NONE") + } + + override fun commitFile(file: File, length: Long) { + // R/O Cache Implementation + } + + override fun releaseHoleSpan(holeSpan: CacheSpan) { + TODO("Not yet implemented") + } + + override fun removeResource(key: String) { + // R/O Cache Implementation + } + + override fun removeSpan(span: CacheSpan) { + // R/O Cache Implementation + } + + override fun isCached(key: String, position: Long, length: Long): Boolean { + TODO("Not yet implemented") + } + + override fun getCachedLength(key: String, position: Long, length: Long): Long { + TODO("Not yet implemented") + } + + override fun getCachedBytes(key: String, position: Long, length: Long): Long { + TODO("Not yet implemented") + } + + override fun applyContentMetadataMutations(key: String, mutations: ContentMetadataMutations) { + TODO("Not yet implemented") + } + + override fun getContentMetadata(key: String): ContentMetadata { + TODO("Not yet implemented") + } + +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt deleted file mode 100644 index 34ad87cc..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt +++ /dev/null @@ -1,118 +0,0 @@ -package org.moire.ultrasonic.service - -import android.content.Context -import android.media.AudioAttributes -import android.media.AudioManager -import android.media.AudioManager.OnAudioFocusChangeListener -import androidx.media.AudioAttributesCompat -import androidx.media.AudioFocusRequestCompat -import androidx.media.AudioManagerCompat -import org.koin.java.KoinJavaComponent.inject -import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.util.Settings -import timber.log.Timber - -class AudioFocusHandler(private val context: Context) { - // TODO: This is a circular reference, try to remove it - // This should be doable by using the native MediaController framework - private val mediaPlayerControllerLazy = - inject(MediaPlayerController::class.java) - - private val audioManager by lazy { - context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - } - - private val lossPref: Int - get() = Settings.tempLoss - - private val audioAttributesCompat by lazy { - AudioAttributesCompat.Builder() - .setUsage(AudioAttributesCompat.USAGE_MEDIA) - .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) - .setLegacyStreamType(AudioManager.STREAM_MUSIC) - .build() - } - - fun requestAudioFocus() { - if (!hasFocus) { - hasFocus = true - AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) - } - } - - private val listener = OnAudioFocusChangeListener { focusChange -> - - val mediaPlayerController = mediaPlayerControllerLazy.value - - when (focusChange) { - AudioManager.AUDIOFOCUS_GAIN -> { - Timber.v("Regained Audio Focus") - if (pauseFocus) { - pauseFocus = false - mediaPlayerController.start() - } else if (lowerFocus) { - lowerFocus = false - mediaPlayerController.setVolume(1.0f) - } - } - AudioManager.AUDIOFOCUS_LOSS -> { - if (!mediaPlayerController.isJukeboxEnabled) { - hasFocus = false - mediaPlayerController.pause() - AudioManagerCompat.abandonAudioFocusRequest(audioManager, focusRequest) - Timber.v("Abandoned Audio Focus") - } - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - if (!mediaPlayerController.isJukeboxEnabled) { - Timber.v("Lost Audio Focus") - - if (mediaPlayerController.playerState === PlayerState.STARTED) { - if (lossPref == 0 || lossPref == 1) { - pauseFocus = true - mediaPlayerController.pause() - } - } - } - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - if (!mediaPlayerController.isJukeboxEnabled) { - Timber.v("Lost Audio Focus") - - if (mediaPlayerController.playerState === PlayerState.STARTED) { - if (lossPref == 2 || lossPref == 1) { - lowerFocus = true - mediaPlayerController.setVolume(0.1f) - } else if (lossPref == 0 || lossPref == 1) { - pauseFocus = true - mediaPlayerController.pause() - } - } - } - } - } - } - - private val focusRequest: AudioFocusRequestCompat by lazy { - AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) - .setAudioAttributes(audioAttributesCompat) - .setWillPauseWhenDucked(true) - .setOnAudioFocusChangeListener(listener) - .build() - } - - companion object { - private var hasFocus = false - private var pauseFocus = false - private var lowerFocus = false - - // TODO: This can be removed if we switch to androidx.media2.player - fun getAudioAttributes(): AudioAttributes { - return AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setLegacyStreamType(AudioManager.STREAM_MUSIC) - .build() - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index b0c5c6e1..f700ee8a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -9,6 +9,7 @@ package org.moire.ultrasonic.service import android.os.Bundle import android.os.Handler +import android.os.Looper import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat import androidx.media.MediaBrowserServiceCompat @@ -26,7 +27,6 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -73,7 +73,6 @@ private const val SEARCH_LIMIT = 10 class AutoMediaBrowserService : MediaBrowserServiceCompat() { private val lifecycleSupport by inject() - private val mediaSessionHandler by inject() private val mediaPlayerController by inject() private val activeServerProvider: ActiveServerProvider by inject() private val musicService = MusicServiceFactory.getMusicService() @@ -108,9 +107,8 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { playFromSearchCommand(it.first) } - mediaSessionHandler.initialize() - val handler = Handler() + val handler = Handler(Looper.getMainLooper()) handler.postDelayed( { // Ultrasonic may be started from Android Auto. This boots up the necessary components. @@ -118,7 +116,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { "AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..." ) lifecycleSupport.onCreate() - MediaPlayerService.getInstance() + DownloadService.getInstance() }, 100 ) @@ -186,7 +184,6 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { override fun onDestroy() { super.onDestroy() rxBusSubscription.dispose() - mediaSessionHandler.release() serviceJob.cancel() Timber.i("AutoMediaBrowserService onDestroy finished") @@ -662,7 +659,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val content = callWithErrorHandling { musicService.getPlaylist(id, name) } playlistCache = content?.getTracks() } - if (playlistCache != null) playSongs(playlistCache) + if (playlistCache != null) playSongs(playlistCache!!) } } @@ -905,7 +902,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val content = listStarredSongsInMusicService() starredSongsCache = content?.songs } - if (starredSongsCache != null) playSongs(starredSongsCache) + if (starredSongsCache != null) playSongs(starredSongsCache!!) } } @@ -959,7 +956,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } randomSongsCache = content?.getTracks() } - if (randomSongsCache != null) playSongs(randomSongsCache) + if (randomSongsCache != null) playSongs(randomSongsCache!!) } } @@ -1071,27 +1068,25 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { return section.toString() } - private fun playSongs(songs: List?) { + private fun playSongs(songs: List) { mediaPlayerController.addToPlaylist( songs, - save = false, + cachePermanently = false, autoPlay = true, - playNext = false, shuffle = false, - newPlaylist = true + insertionMode = MediaPlayerController.InsertionMode.CLEAR ) } private fun playSong(song: Track) { mediaPlayerController.addToPlaylist( listOf(song), - save = false, + cachePermanently = false, autoPlay = false, - playNext = true, shuffle = false, - newPlaylist = false + insertionMode = MediaPlayerController.InsertionMode.AFTER_CURRENT ) - if (mediaPlayerController.playlistSize > 1) mediaPlayerController.next() + if (mediaPlayerController.mediaItemCount > 1) mediaPlayerController.next() else mediaPlayerController.play() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 9b99eb9b..95985926 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -7,27 +7,18 @@ package org.moire.ultrasonic.service -import android.text.TextUtils import androidx.lifecycle.MutableLiveData +import androidx.media3.common.MediaItem import java.io.IOException -import java.io.InputStream -import java.io.OutputStream import java.util.Locale import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.moire.ultrasonic.data.ActiveServerProvider -import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.subsonic.ImageLoaderProvider -import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.Util -import org.moire.ultrasonic.util.Util.safeClose import timber.log.Timber /** @@ -44,29 +35,22 @@ class DownloadFile( ) : KoinComponent, Identifiable { val partialFile: String lateinit var completeFile: String - val saveFile: String = FileUtil.getSongFile(track) + val pinnedFile: String = FileUtil.getSongFile(track) var shouldSave = save - private var downloadTask: CancellableTask? = null + internal var downloadTask: CancellableTask? = null var isFailed = false - private var retryCount = MAX_RETRIES + internal var retryCount = MAX_RETRIES - private val desiredBitRate: Int = Settings.maxBitRate + val desiredBitRate: Int = Settings.maxBitRate var priority = 100 var downloadPrepared = false @Volatile - private var isPlaying = false + internal var saveWhenDone = false @Volatile - private var saveWhenDone = false - - @Volatile - private var completeWhenDone = false - - private val downloader: Downloader by inject() - private val imageLoaderProvider: ImageLoaderProvider by inject() - private val activeServerProvider: ActiveServerProvider by inject() + var completeWhenDone = false val progress: MutableLiveData = MutableLiveData(0) @@ -78,7 +62,7 @@ class DownloadFile( private val lazyInitialStatus: Lazy = lazy { when { - Storage.isPathExists(saveFile) -> { + Storage.isPathExists(pinnedFile) -> { DownloadStatus.PINNED } Storage.isPathExists(completeFile) -> { @@ -95,10 +79,10 @@ class DownloadFile( } init { - partialFile = FileUtil.getParentPath(saveFile) + "/" + - FileUtil.getPartialFile(FileUtil.getNameFromPath(saveFile)) - completeFile = FileUtil.getParentPath(saveFile) + "/" + - FileUtil.getCompleteFile(FileUtil.getNameFromPath(saveFile)) + partialFile = FileUtil.getParentPath(pinnedFile) + "/" + + FileUtil.getPartialFile(FileUtil.getNameFromPath(pinnedFile)) + completeFile = FileUtil.getParentPath(pinnedFile) + "/" + + FileUtil.getCompleteFile(FileUtil.getNameFromPath(pinnedFile)) } /** @@ -115,13 +99,6 @@ class DownloadFile( downloadPrepared = true } - @Synchronized - fun download() { - FileUtil.createDirectoryForParent(saveFile) - isFailed = false - downloadTask = DownloadTask() - downloadTask!!.start() - } @Synchronized fun cancelDownload() { @@ -129,30 +106,23 @@ class DownloadFile( } val completeOrSaveFile: String - get() = if (Storage.isPathExists(saveFile)) { - saveFile + get() = if (Storage.isPathExists(pinnedFile)) { + pinnedFile } else { completeFile } - val completeOrPartialFile: String - get() = if (isCompleteFileAvailable) { - completeOrSaveFile - } else { - partialFile - } - val isSaved: Boolean - get() = Storage.isPathExists(saveFile) + get() = Storage.isPathExists(pinnedFile) @get:Synchronized val isCompleteFileAvailable: Boolean - get() = Storage.isPathExists(completeFile) || Storage.isPathExists(saveFile) + get() = Storage.isPathExists(completeFile) || Storage.isPathExists(pinnedFile) @get:Synchronized val isWorkDone: Boolean get() = Storage.isPathExists(completeFile) && !shouldSave || - Storage.isPathExists(saveFile) || saveWhenDone || completeWhenDone + Storage.isPathExists(pinnedFile) || saveWhenDone || completeWhenDone @get:Synchronized val isDownloading: Boolean @@ -170,54 +140,66 @@ class DownloadFile( cancelDownload() Storage.delete(partialFile) Storage.delete(completeFile) - Storage.delete(saveFile) + Storage.delete(pinnedFile) status.postValue(DownloadStatus.IDLE) - Util.scanMedia(saveFile) + Util.scanMedia(pinnedFile) } fun unpin() { - val file = Storage.getFromPath(saveFile) ?: return + Timber.e("CLEANING") + val file = Storage.getFromPath(pinnedFile) ?: return Storage.rename(file, completeFile) status.postValue(DownloadStatus.DONE) } fun cleanup(): Boolean { + Timber.e("CLEANING") var ok = true - if (Storage.isPathExists(completeFile) || Storage.isPathExists(saveFile)) { + if (Storage.isPathExists(completeFile) || Storage.isPathExists(pinnedFile)) { ok = Storage.delete(partialFile) } - if (Storage.isPathExists(saveFile)) { + if (Storage.isPathExists(pinnedFile)) { ok = ok and Storage.delete(completeFile) } return ok } - fun setPlaying(isPlaying: Boolean) { - if (!isPlaying) doPendingRename() - this.isPlaying = isPlaying + /** + * Create a MediaItem instance representing the data inside this DownloadFile + */ + val mediaItem: MediaItem by lazy { + track.toMediaItem() } + var isPlaying: Boolean = false + get() = field + set(isPlaying) { + if (!isPlaying) doPendingRename() + field = isPlaying + } + // Do a pending rename after the song has stopped playing private fun doPendingRename() { try { + Timber.e("CLEANING") if (saveWhenDone) { - Storage.rename(completeFile, saveFile) + Storage.rename(completeFile, pinnedFile) saveWhenDone = false } else if (completeWhenDone) { if (shouldSave) { - Storage.rename(partialFile, saveFile) - Util.scanMedia(saveFile) + Storage.rename(partialFile, pinnedFile) + Util.scanMedia(pinnedFile) } else { Storage.rename(partialFile, completeFile) } completeWhenDone = false } } catch (e: IOException) { - Timber.w(e, "Failed to rename file %s to %s", completeFile, saveFile) + Timber.w(e, "Failed to rename file %s to %s", completeFile, pinnedFile) } } @@ -225,176 +207,7 @@ class DownloadFile( return String.format(Locale.ROOT, "DownloadFile (%s)", track) } - private inner class DownloadTask : CancellableTask() { - val musicService = getMusicService() - - override fun execute() { - - downloadPrepared = false - var inputStream: InputStream? = null - var outputStream: OutputStream? = null - try { - if (Storage.isPathExists(saveFile)) { - Timber.i("%s already exists. Skipping.", saveFile) - status.postValue(DownloadStatus.PINNED) - return - } - - if (Storage.isPathExists(completeFile)) { - var newStatus: DownloadStatus = DownloadStatus.DONE - if (shouldSave) { - if (isPlaying) { - saveWhenDone = true - } else { - Storage.rename(completeFile, saveFile) - newStatus = DownloadStatus.PINNED - } - } else { - Timber.i("%s already exists. Skipping.", completeFile) - } - status.postValue(newStatus) - return - } - - status.postValue(DownloadStatus.DOWNLOADING) - - // Some devices seem to throw error on partial file which doesn't exist - val needsDownloading: Boolean - val duration = track.duration - val fileLength = Storage.getFromPath(partialFile)?.length ?: 0 - - needsDownloading = ( - desiredBitRate == 0 || duration == null || duration == 0 || fileLength == 0L - ) - - if (needsDownloading) { - // Attempt partial HTTP GET, appending to the file if it exists. - val (inStream, isPartial) = musicService.getDownloadInputStream( - track, fileLength, desiredBitRate, shouldSave - ) - - inputStream = inStream - - if (isPartial) { - Timber.i("Executed partial HTTP GET, skipping %d bytes", fileLength) - } - - outputStream = Storage.getOrCreateFileFromPath(partialFile) - .getFileOutputStream(isPartial) - - val len = inputStream.copyTo(outputStream) { totalBytesCopied -> - setProgress(totalBytesCopied) - } - - Timber.i("Downloaded %d bytes to %s", len, partialFile) - - inputStream.close() - outputStream.flush() - outputStream.close() - - if (isCancelled) { - status.postValue(DownloadStatus.CANCELLED) - throw RuntimeException( - String.format(Locale.ROOT, "Download of '%s' was cancelled", track) - ) - } - - if (track.artistId != null) { - cacheMetadata(track.artistId!!) - } - - downloadAndSaveCoverArt() - } - - if (isPlaying) { - completeWhenDone = true - } else { - if (shouldSave) { - Storage.rename(partialFile, saveFile) - status.postValue(DownloadStatus.PINNED) - Util.scanMedia(saveFile) - } else { - Storage.rename(partialFile, completeFile) - status.postValue(DownloadStatus.DONE) - } - } - } catch (all: Exception) { - outputStream.safeClose() - Storage.delete(completeFile) - Storage.delete(saveFile) - if (!isCancelled) { - isFailed = true - if (retryCount > 1) { - status.postValue(DownloadStatus.RETRYING) - --retryCount - } else if (retryCount == 1) { - status.postValue(DownloadStatus.FAILED) - --retryCount - } - Timber.w(all, "Failed to download '%s'.", track) - } - } finally { - inputStream.safeClose() - outputStream.safeClose() - CacheCleaner().cleanSpace() - downloader.checkDownloads() - } - } - - override fun toString(): String { - return String.format(Locale.ROOT, "DownloadTask (%s)", track) - } - - private fun cacheMetadata(artistId: String) { - // TODO: Right now it's caching the track artist. - // Once the albums are cached in db, we should retrieve the album, - // and then cache the album artist. - if (artistId.isEmpty()) return - var artist: Artist? = - activeServerProvider.getActiveMetaDatabase().artistsDao().get(artistId) - - // If we are downloading a new album, and the user has not visited the Artists list - // recently, then the artist won't be in the database. - if (artist == null) { - val artists: List = musicService.getArtists(true) - artist = artists.find { - it.id == artistId - } - } - - // If we have found an artist, catch it. - if (artist != null) { - activeServerProvider.offlineMetaDatabase.artistsDao().insert(artist) - } - } - - private fun downloadAndSaveCoverArt() { - try { - if (!TextUtils.isEmpty(track.coverArt)) { - // Download the largest size that we can display in the UI - imageLoaderProvider.getImageLoader().cacheCoverArt(track) - } - } catch (all: Exception) { - Timber.e(all, "Failed to get cover art.") - } - } - - @Throws(IOException::class) - fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long) -> Any): Long { - var bytesCopied: Long = 0 - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var bytes = read(buffer) - while (!isCancelled && bytes >= 0) { - out.write(buffer, 0, bytes) - bytesCopied += bytes - onCopy(bytesCopied) - bytes = read(buffer) - } - return bytesCopied - } - } - - private fun setProgress(totalBytesCopied: Long) { + internal fun setProgress(totalBytesCopied: Long) { if (track.size != null) { progress.postValue((totalBytesCopied * 100 / track.size!!).toInt()) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt new file mode 100644 index 00000000..1f1dddda --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -0,0 +1,256 @@ +/* + * MediaPlayerService.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.service + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.support.v4.media.session.MediaSessionCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import org.koin.android.ext.android.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.activity.NavigationActivity +import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.SimpleServiceBinder +import org.moire.ultrasonic.util.Util +import timber.log.Timber + +/** + * Android Foreground service which is used to download tracks even when the app is not visible + * + * "A foreground service is a service that the user is + * actively aware of and isn’t a candidate for the system to kill when low on memory." + * + * TODO: Migrate this to use the Media3 DownloadHelper + */ +class DownloadService : Service() { + private val binder: IBinder = SimpleServiceBinder(this) + + private val downloader by inject() + + private var mediaSession: MediaSessionCompat? = null + + private var isInForeground = false + + override fun onBind(intent: Intent): IBinder { + return binder + } + + override fun onCreate() { + super.onCreate() + + + // Create Notification Channel + createNotificationChannel() + updateNotification() + + instance = this + Timber.i("DownloadService created") + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + return START_NOT_STICKY + } + + override fun onDestroy() { + super.onDestroy() + instance = null + try { + downloader.stop() + + mediaSession?.release() + mediaSession = null + } catch (ignored: Throwable) { + } + Timber.i("DownloadService stopped") + } + + fun notifyDownloaderStopped() { + isInForeground = false + stopForeground(true) + stopSelf() + } + + private fun setupOnSongCompletedHandler() { +// localMediaPlayer.onSongCompleted = { currentPlaying: DownloadFile? -> +// val index = downloader.currentPlayingIndex +// +// if (currentPlaying != null) { +// val song = currentPlaying.track +// if (song.bookmarkPosition > 0 && Settings.shouldClearBookmark) { +// val musicService = getMusicService() +// try { +// musicService.deleteBookmark(song.id) +// } catch (ignored: Exception) { +// } +// } +// } +// if (index != -1) { +// +// if (index + 1 < 0 || index + 1 >= downloader.getPlaylist().size) { +// if (Settings.shouldClearPlaylist) { +// clear(true) +// jukeboxMediaPlayer.updatePlaylist() +// } +// resetPlayback() +// } else { +// play(index + 1) +// } +// } +// null +// } + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + // The suggested importance of a startForeground service notification is IMPORTANCE_LOW + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW + ) + + channel.lightColor = android.R.color.holo_blue_dark + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + channel.setShowBadge(false) + + val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + manager.createNotificationChannel(channel) + } + } + + // We should use a single notification builder, otherwise the notification may not be updated + // Set some values that never change + private val notificationBuilder: NotificationCompat.Builder by lazy { + NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_stat_ultrasonic) + .setAutoCancel(false) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setWhen(System.currentTimeMillis()) + .setShowWhen(false) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(getPendingIntentForContent()) + .setPriority(NotificationCompat.PRIORITY_LOW) + } + + private fun updateNotification() { + + val notification = buildForegroundNotification() + + if (isInForeground) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + manager.notify(NOTIFICATION_ID, notification) + } else { + val manager = NotificationManagerCompat.from(this) + manager.notify(NOTIFICATION_ID, notification) + } + Timber.v("Updated notification") + } else { + startForeground(NOTIFICATION_ID, notification) + isInForeground = true + Timber.v("Created Foreground notification") + } + } + + /** + * This method builds a notification, reusing the Notification Builder if possible + */ + @Suppress("SpreadOperator") + private fun buildForegroundNotification(): Notification { + + if (downloader.started) { + // No song is playing, but Ultrasonic is downloading files + notificationBuilder.setContentTitle( + getString(R.string.notification_downloading_title) + ) + } + + return notificationBuilder.build() + } + + @SuppressLint("UnspecifiedImmutableFlag") + private fun getPendingIntentForContent(): PendingIntent { + val intent = Intent(this, NavigationActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + val flags = PendingIntent.FLAG_UPDATE_CURRENT + intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) + return PendingIntent.getActivity(this, 0, intent, flags) + } + + @Suppress("MagicNumber") + companion object { + + private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic" + private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service" + private const val NOTIFICATION_ID = 3033 + + @Volatile + private var instance: DownloadService? = null + private val instanceLock = Any() + + @JvmStatic + fun getInstance(): DownloadService? { + val context = UApp.applicationContext() + // Try for twenty times to retrieve a running service, + // sleep 100 millis between each try, + // and run the block that creates a service only synchronized. + for (i in 0..19) { + if (instance != null) return instance + synchronized(instanceLock) { + if (instance != null) return instance + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService( + Intent(context, DownloadService::class.java) + ) + } else { + context.startService(Intent(context, DownloadService::class.java)) + } + } + Util.sleepQuietly(100L) + } + return instance + } + + @JvmStatic + val runningInstance: DownloadService? + get() { + synchronized(instanceLock) { return instance } + } + + @JvmStatic + fun executeOnStartedMediaPlayerService( + taskToExecute: (DownloadService) -> Unit + ) { + + val t: Thread = object : Thread() { + override fun run() { + val instance = getInstance() + if (instance == null) { + Timber.e("ExecuteOnStarted.. failed to get a DownloadService instance!") + return + } else { + taskToExecute(instance) + } + } + } + t.start() + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index a0a33c50..9679f03b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -1,119 +1,130 @@ package org.moire.ultrasonic.service import android.net.wifi.WifiManager +import android.os.Handler +import android.os.Looper +import android.text.TextUtils import androidx.lifecycle.MutableLiveData -import java.util.ArrayList -import java.util.PriorityQueue -import java.util.concurrent.Executors -import java.util.concurrent.RejectedExecutionException -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit +import io.reactivex.rxjava3.disposables.CompositeDisposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.playback.LegacyPlaylistManager +import org.moire.ultrasonic.subsonic.ImageLoaderProvider +import org.moire.ultrasonic.util.CacheCleaner +import org.moire.ultrasonic.util.CancellableTask +import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.LRUCache import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.ShufflePlayBuffer +import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.safeClose import timber.log.Timber +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.Locale +import java.util.PriorityQueue /** * This class is responsible for maintaining the playlist and downloading * its items from the network to the filesystem. * - * TODO: Move away from managing the queue with scheduled checks, instead use callbacks when - * Downloads are finished + * TODO: Move entirely to subclass the Media3.DownloadService */ class Downloader( - private val shufflePlayBuffer: ShufflePlayBuffer, - private val externalStorageMonitor: ExternalStorageMonitor, - private val localMediaPlayer: LocalMediaPlayer + private val storageMonitor: ExternalStorageMonitor, + private val legacyPlaylistManager: LegacyPlaylistManager, ) : KoinComponent { - private val playlist = mutableListOf() + // Dependencies + private val imageLoaderProvider: ImageLoaderProvider by inject() + private val activeServerProvider: ActiveServerProvider by inject() + private val mediaController: MediaPlayerController by inject() var started: Boolean = false + var shouldStop: Boolean = false private val downloadQueue = PriorityQueue() private val activelyDownloading = mutableListOf() - // TODO: The playlist is now published with RX, while the observableDownloads is using LiveData. - // Use the same for both + // The generic list models expect a LiveData, so even though we are using Rx for many events + // surrounding playback the list of Downloads is published as LiveData. val observableDownloads = MutableLiveData>() - private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() - // This cache helps us to avoid creating duplicate DownloadFile instances when showing Entries - private val downloadFileCache = LRUCache(100) + private val downloadFileCache = LRUCache(500) - private var executorService: ScheduledExecutorService? = null + private var handler: Handler = Handler(Looper.getMainLooper()) private var wifiLock: WifiManager.WifiLock? = null - private var playlistUpdateRevision: Long = 0 - private set(value) { - field = value - RxBus.playlistPublisher.onNext(playlist) - } + private var backgroundPriorityCounter = 100 - var backgroundPriorityCounter = 100 + private val rxBusSubscription: CompositeDisposable = CompositeDisposable() - val downloadChecker = Runnable { - try { - Timber.w("Checking Downloads") - checkDownloadsInternal() - } catch (all: Exception) { - Timber.e(all, "checkDownloads() failed.") + var downloadChecker = object : Runnable { + override fun run() { + try { + Timber.w("Checking Downloads") + checkDownloadsInternal() + } catch (all: Exception) { + Timber.e(all, "checkDownloads() failed.") + } finally { + if (!shouldStop) { + Handler(Looper.getMainLooper()).postDelayed(this, CHECK_INTERVAL) + } else { + shouldStop = false + } + } } } fun onDestroy() { stop() - clearPlaylist() + rxBusSubscription.dispose() clearBackground() observableDownloads.value = listOf() Timber.i("Downloader destroyed") } + @Synchronized fun start() { started = true - if (executorService == null) { - executorService = Executors.newSingleThreadScheduledExecutor() - executorService!!.scheduleWithFixedDelay( - downloadChecker, 0L, CHECK_INTERVAL, TimeUnit.SECONDS - ) - Timber.i("Downloader started") - } + + // Start our loop + handler.postDelayed(downloadChecker, 100) if (wifiLock == null) { wifiLock = Util.createWifiLock(toString()) wifiLock?.acquire() } + + // Check downloads if the playlist changed + rxBusSubscription += RxBus.playlistObservable.subscribe { + checkDownloads() + } } fun stop() { started = false - executorService?.shutdown() - executorService = null + shouldStop = true wifiLock?.release() wifiLock = null - MediaPlayerService.runningInstance?.notifyDownloaderStopped() + DownloadService.runningInstance?.notifyDownloaderStopped() Timber.i("Downloader stopped") } fun checkDownloads() { - if ( - executorService == null || - executorService!!.isTerminated || - executorService!!.isShutdown - ) { + if (!started) { start() } else { try { - executorService?.execute(downloadChecker) - } catch (exception: RejectedExecutionException) { + handler.postDelayed(downloadChecker, 100) + } catch (all: Exception) { Timber.w( - exception, + all, "checkDownloads() can't run, maybe the Downloader is shutting down..." ) } @@ -121,22 +132,17 @@ class Downloader( } @Synchronized - @Suppress("ComplexMethod", "ComplexCondition") fun checkDownloadsInternal() { - if ( - !Util.isExternalStoragePresent() || - !externalStorageMonitor.isExternalStorageAvailable - ) { + if (!Util.isExternalStoragePresent() || !storageMonitor.isExternalStorageAvailable) { return } - if (shufflePlayBuffer.isEnabled) { - checkShufflePlay() - } - if (jukeboxMediaPlayer.isEnabled || !Util.isNetworkConnected()) { + + if (legacyPlaylistManager.jukeboxMediaPlayer.isEnabled || !Util.isNetworkConnected()) { return } Timber.v("Downloader checkDownloadsInternal checking downloads") + // Check the active downloads for failures or completions and remove them // Store the result in a flag to know if changes have occurred var listChanged = cleanupActiveDownloads() @@ -145,13 +151,14 @@ class Downloader( val preloadCount = Settings.preloadCount // Start preloading at the current playing song - var start = currentPlayingIndex + var start = mediaController.currentMediaItemIndex + if (start == -1) start = 0 - val end = (start + preloadCount).coerceAtMost(playlist.size) + val end = (start + preloadCount).coerceAtMost(mediaController.mediaItemCount) for (i in start until end) { - val download = playlist[i] + val download = legacyPlaylistManager.playlist[i] // Set correct priority (the lower the number, the higher the priority) download.priority = i @@ -173,10 +180,6 @@ class Downloader( activelyDownloading.add(task) startDownloadOnService(task) - // The next file on the playlist is currently downloading - if (playlist.indexOf(task) == 1) { - localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING) - } listChanged = true } @@ -194,10 +197,14 @@ class Downloader( observableDownloads.postValue(downloads) } - private fun startDownloadOnService(task: DownloadFile) { - task.prepare() - MediaPlayerService.executeOnStartedMediaPlayerService { - task.download() + private fun startDownloadOnService(file: DownloadFile) { + if (file.isDownloading) return + file.prepare() + DownloadService.executeOnStartedMediaPlayerService { + FileUtil.createDirectoryForParent(file.pinnedFile) + file.isFailed = false + file.downloadTask = DownloadTask(file) + file.downloadTask!!.start() } } @@ -225,26 +232,6 @@ class Downloader( return (oldSize != activelyDownloading.size) } - @get:Synchronized - val currentPlayingIndex: Int - get() = playlist.indexOf(localMediaPlayer.currentPlaying) - - @get:Synchronized - val downloadListDuration: Long - get() { - var totalDuration: Long = 0 - for (downloadFile in playlist) { - val song = downloadFile.track - if (!song.isDirectory) { - if (song.artist != null) { - if (song.duration != null) { - totalDuration += song.duration!!.toLong() - } - } - } - } - return totalDuration - } @get:Synchronized val all: List @@ -252,7 +239,7 @@ class Downloader( val temp: MutableList = ArrayList() temp.addAll(activelyDownloading) temp.addAll(downloadQueue) - temp.addAll(playlist) + temp.addAll(legacyPlaylistManager.playlist) return temp.distinct().sorted() } @@ -267,7 +254,7 @@ class Downloader( temp.addAll(activelyDownloading) temp.addAll(downloadQueue) temp.addAll( - playlist.filter { + legacyPlaylistManager.playlist.filter { if (!it.isStatusInitialized) false else when (it.status.value) { DownloadStatus.DOWNLOADING -> true @@ -278,37 +265,13 @@ class Downloader( return temp.distinct().sorted() } - // Public facing playlist (immutable) - @Synchronized - fun getPlaylist(): List = playlist - @Synchronized fun clearDownloadFileCache() { downloadFileCache.clear() } @Synchronized - fun clearPlaylist() { - playlist.clear() - - val toRemove = mutableListOf() - - // Cancel all active downloads with a high priority - for (download in activelyDownloading) { - if (download.priority < 100) { - download.cancelDownload() - toRemove.add(download) - } - } - - activelyDownloading.removeAll(toRemove) - - playlistUpdateRevision++ - updateLiveData() - } - - @Synchronized - private fun clearBackground() { + fun clearBackground() { // Clear the pending queue downloadQueue.clear() @@ -333,78 +296,6 @@ class Downloader( updateLiveData() } - @Synchronized - fun removeFromPlaylist(downloadFile: DownloadFile) { - if (activelyDownloading.contains(downloadFile)) { - downloadFile.cancelDownload() - } - playlist.remove(downloadFile) - playlistUpdateRevision++ - checkDownloads() - } - - @Synchronized - fun addToPlaylist( - songs: List, - save: Boolean, - autoPlay: Boolean, - playNext: Boolean, - newPlaylist: Boolean - ) { - shufflePlayBuffer.isEnabled = false - var offset = 1 - if (songs.isEmpty()) { - return - } - if (newPlaylist) { - playlist.clear() - } - if (playNext) { - if (autoPlay && currentPlayingIndex >= 0) { - offset = 0 - } - for (song in songs) { - val downloadFile = song.getDownloadFile(save) - playlist.add(currentPlayingIndex + offset, downloadFile) - offset++ - } - } else { - for (song in songs) { - val downloadFile = song.getDownloadFile(save) - playlist.add(downloadFile) - } - } - playlistUpdateRevision++ - checkDownloads() - } - - fun moveItemInPlaylist(oldPos: Int, newPos: Int) { - val item = playlist[oldPos] - playlist.remove(item) - - if (newPos < oldPos) { - playlist.add(newPos + 1, item) - } else { - playlist.add(newPos - 1, item) - } - - playlistUpdateRevision++ - checkDownloads() - } - - @Synchronized - fun clearIncomplete() { - val iterator = playlist.iterator() - var changedPlaylist = false - while (iterator.hasNext()) { - val downloadFile = iterator.next() - if (!downloadFile.isCompleteFileAvailable) { - iterator.remove() - changedPlaylist = true - } - } - if (changedPlaylist) playlistUpdateRevision++ - } @Synchronized fun downloadBackground(songs: List, save: Boolean) { @@ -413,30 +304,19 @@ class Downloader( for (song in songs) { val file = song.getDownloadFile() file.shouldSave = save - file.priority = backgroundPriorityCounter++ - downloadQueue.add(file) + if (!file.isDownloading) { + file.priority = backgroundPriorityCounter++ + downloadQueue.add(file) + } } checkDownloads() } - @Synchronized - fun shuffle() { - playlist.shuffle() - - // Move the current song to the top.. - if (localMediaPlayer.currentPlaying != null) { - playlist.remove(localMediaPlayer.currentPlaying) - playlist.add(0, localMediaPlayer.currentPlaying!!) - } - - playlistUpdateRevision++ - } - @Synchronized @Suppress("ReturnCount") fun getDownloadFileForSong(song: Track): DownloadFile { - for (downloadFile in playlist) { + for (downloadFile in legacyPlaylistManager.playlist) { if (downloadFile.track == song) { return downloadFile } @@ -459,63 +339,205 @@ class Downloader( return downloadFile } - @Synchronized - private fun checkShufflePlay() { - // Get users desired random playlist size - val listSize = Settings.maxSongs - val wasEmpty = playlist.isEmpty() - val revisionBefore = playlistUpdateRevision - - // First, ensure that list is at least 20 songs long. - val size = playlist.size - if (size < listSize) { - for (song in shufflePlayBuffer[listSize - size]) { - val downloadFile = song.getDownloadFile(false) - playlist.add(downloadFile) - playlistUpdateRevision++ - } - } - - val currIndex = if (localMediaPlayer.currentPlaying == null) 0 else currentPlayingIndex - - // Only shift playlist if playing song #5 or later. - if (currIndex > SHUFFLE_BUFFER_LIMIT) { - val songsToShift = currIndex - 2 - for (song in shufflePlayBuffer[songsToShift]) { - playlist.add(song.getDownloadFile(false)) - playlist[0].cancelDownload() - playlist.removeAt(0) - playlistUpdateRevision++ - } - } - - if (revisionBefore != playlistUpdateRevision) { - jukeboxMediaPlayer.updatePlaylist() - } - - if (wasEmpty && playlist.isNotEmpty()) { - if (jukeboxMediaPlayer.isEnabled) { - jukeboxMediaPlayer.skip(0, 0) - localMediaPlayer.setPlayerState(PlayerState.STARTED, playlist[0]) - } else { - localMediaPlayer.play(playlist[0]) - } - } - } companion object { const val PARALLEL_DOWNLOADS = 3 - const val CHECK_INTERVAL = 5L - const val SHUFFLE_BUFFER_LIMIT = 4 + const val CHECK_INTERVAL = 5000L } /** * Extension function * Gathers the download file for a given song, and modifies shouldSave if provided. */ - fun Track.getDownloadFile(save: Boolean? = null): DownloadFile { + private fun Track.getDownloadFile(save: Boolean? = null): DownloadFile { return getDownloadFileForSong(this).apply { if (save != null) this.shouldSave = save } } + + private inner class DownloadTask(private val downloadFile: DownloadFile) : CancellableTask() { + val musicService = MusicServiceFactory.getMusicService() + + override fun execute() { + + downloadFile.downloadPrepared = false + var inputStream: InputStream? = null + var outputStream: OutputStream? = null + try { + if (Storage.isPathExists(downloadFile.pinnedFile)) { + Timber.i("%s already exists. Skipping.", downloadFile.pinnedFile) + downloadFile.status.postValue(DownloadStatus.PINNED) + return + } + + if (Storage.isPathExists(downloadFile.completeFile)) { + var newStatus: DownloadStatus = DownloadStatus.DONE + if (downloadFile.shouldSave) { + if (downloadFile.isPlaying) { + downloadFile.saveWhenDone = true + } else { + Storage.rename( + downloadFile.completeFile, + downloadFile.pinnedFile + ) + newStatus = DownloadStatus.PINNED + } + } else { + Timber.i( + "%s already exists. Skipping.", + downloadFile.completeFile + ) + } + downloadFile.status.postValue(newStatus) + return + } + + downloadFile.status.postValue(DownloadStatus.DOWNLOADING) + + // Some devices seem to throw error on partial file which doesn't exist + val needsDownloading: Boolean + val duration = downloadFile.track.duration + val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0 + + needsDownloading = ( + downloadFile.desiredBitRate == 0 || duration == null || duration == 0 || fileLength == 0L + ) + + if (needsDownloading) { + // Attempt partial HTTP GET, appending to the file if it exists. + val (inStream, isPartial) = musicService.getDownloadInputStream( + downloadFile.track, fileLength, + downloadFile.desiredBitRate, + downloadFile.shouldSave + ) + + inputStream = inStream + + if (isPartial) { + Timber.i("Executed partial HTTP GET, skipping %d bytes", fileLength) + } + + outputStream = Storage.getOrCreateFileFromPath(downloadFile.partialFile) + .getFileOutputStream(isPartial) + + val len = inputStream.copyTo(outputStream) { totalBytesCopied -> + downloadFile.setProgress(totalBytesCopied) + } + + Timber.i("Downloaded %d bytes to %s", len, downloadFile.partialFile) + + inputStream.close() + outputStream.flush() + outputStream.close() + + if (isCancelled) { + downloadFile.status.postValue(DownloadStatus.CANCELLED) + throw RuntimeException( + String.format( + Locale.ROOT, "Download of '%s' was cancelled", + downloadFile.track + ) + ) + } + + if (downloadFile.track.artistId != null) { + cacheMetadata(downloadFile.track.artistId!!) + } + + downloadAndSaveCoverArt() + } + + if (downloadFile.isPlaying) { + downloadFile.completeWhenDone = true + } else { + if (downloadFile.shouldSave) { + Storage.rename( + downloadFile.partialFile, + downloadFile.pinnedFile + ) + downloadFile.status.postValue(DownloadStatus.PINNED) + Util.scanMedia(downloadFile.pinnedFile) + } else { + Storage.rename( + downloadFile.partialFile, + downloadFile.completeFile + ) + downloadFile.status.postValue(DownloadStatus.DONE) + } + } + } catch (all: Exception) { + outputStream.safeClose() + Storage.delete(downloadFile.completeFile) + Storage.delete(downloadFile.pinnedFile) + if (!isCancelled) { + downloadFile.isFailed = true + if (downloadFile.retryCount > 1) { + downloadFile.status.postValue(DownloadStatus.RETRYING) + --downloadFile.retryCount + } else if (downloadFile.retryCount == 1) { + downloadFile.status.postValue(DownloadStatus.FAILED) + --downloadFile.retryCount + } + Timber.w(all, "Failed to download '%s'.", downloadFile.track) + } + } finally { + inputStream.safeClose() + outputStream.safeClose() + CacheCleaner().cleanSpace() + checkDownloads() + } + } + + override fun toString(): String { + return String.format(Locale.ROOT, "DownloadTask (%s)", downloadFile.track) + } + + private fun cacheMetadata(artistId: String) { + // TODO: Right now it's caching the track artist. + // Once the albums are cached in db, we should retrieve the album, + // and then cache the album artist. + if (artistId.isEmpty()) return + var artist: Artist? = + activeServerProvider.getActiveMetaDatabase().artistsDao().get(artistId) + + // If we are downloading a new album, and the user has not visited the Artists list + // recently, then the artist won't be in the database. + if (artist == null) { + val artists: List = musicService.getArtists(true) + artist = artists.find { + it.id == artistId + } + } + + // If we have found an artist, catch it. + if (artist != null) { + activeServerProvider.offlineMetaDatabase.artistsDao().insert(artist) + } + } + + private fun downloadAndSaveCoverArt() { + try { + if (!TextUtils.isEmpty(downloadFile.track.coverArt)) { + // Download the largest size that we can display in the UI + imageLoaderProvider.getImageLoader().cacheCoverArt(downloadFile.track) + } + } catch (all: Exception) { + Timber.e(all, "Failed to get cover art.") + } + } + + @Throws(IOException::class) + fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long) -> Any): Long { + var bytesCopied: Long = 0 + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var bytes = read(buffer) + while (!isCancelled && bytes >= 0) { + out.write(buffer, 0, bytes) + bytesCopied += bytes + onCopy(bytesCopied) + bytes = read(buffer) + } + return bytesCopied + } + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt new file mode 100644 index 00000000..af14e15d --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -0,0 +1,337 @@ +/* + * JukeboxMediaPlayer.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ +package org.moire.ultrasonic.service + +import android.content.Context +import android.os.Handler +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.ProgressBar +import android.widget.Toast +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException +import org.moire.ultrasonic.api.subsonic.SubsonicRESTException +import org.moire.ultrasonic.app.UApp.Companion.applicationContext +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.JukeboxStatus +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.Util.sleepQuietly +import org.moire.ultrasonic.util.Util.toast +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import kotlin.math.roundToInt + +/** + * Provides an asynchronous interface to the remote jukebox on the Subsonic server. + * + * TODO: Report warning if queue fills up. + * TODO: Create shutdown method? + * TODO: Disable repeat. + * TODO: Persist RC state? + * TODO: Minimize status updates. + */ +class JukeboxMediaPlayer(private val downloader: Downloader) { + private val tasks = TaskQueue() + private val executorService = Executors.newSingleThreadScheduledExecutor() + private var statusUpdateFuture: ScheduledFuture<*>? = null + private val timeOfLastUpdate = AtomicLong() + private var jukeboxStatus: JukeboxStatus? = null + private var gain = 0.5f + private var volumeToast: VolumeToast? = null + private val running = AtomicBoolean() + private var serviceThread: Thread? = null + private var enabled = false + + // TODO: These create circular references, try to refactor + private val mediaPlayerControllerLazy = inject( + MediaPlayerController::class.java + ) + + fun startJukeboxService() { + if (running.get()) { + return + } + running.set(true) + startProcessTasks() + Timber.d("Started Jukebox Service") + } + + fun stopJukeboxService() { + running.set(false) + sleepQuietly(1000) + if (serviceThread != null) { + serviceThread!!.interrupt() + } + Timber.d("Stopped Jukebox Service") + } + + private fun startProcessTasks() { + serviceThread = object : Thread() { + override fun run() { + processTasks() + } + } + (serviceThread as Thread).start() + } + + @Synchronized + private fun startStatusUpdate() { + stopStatusUpdate() + val updateTask = Runnable { + tasks.remove(GetStatus::class.java) + tasks.add(GetStatus()) + } + statusUpdateFuture = executorService.scheduleWithFixedDelay( + updateTask, + STATUS_UPDATE_INTERVAL_SECONDS, + STATUS_UPDATE_INTERVAL_SECONDS, + TimeUnit.SECONDS + ) + } + + @Synchronized + private fun stopStatusUpdate() { + if (statusUpdateFuture != null) { + statusUpdateFuture!!.cancel(false) + statusUpdateFuture = null + } + } + + private fun processTasks() { + while (running.get()) { + var task: JukeboxTask? = null + try { + if (!isOffline()) { + task = tasks.take() + val status = task.execute() + onStatusUpdate(status) + } + } catch (ignored: InterruptedException) { + } catch (x: Throwable) { + onError(task, x) + } + sleepQuietly(1) + } + } + + private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) { + timeOfLastUpdate.set(System.currentTimeMillis()) + this.jukeboxStatus = jukeboxStatus + } + + private fun onError(task: JukeboxTask?, x: Throwable) { + if (x is ApiNotSupportedException && task !is Stop) { + disableJukeboxOnError(x, R.string.download_jukebox_server_too_old) + } else if (x is OfflineException && task !is Stop) { + disableJukeboxOnError(x, R.string.download_jukebox_offline) + } else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) { + disableJukeboxOnError(x, R.string.download_jukebox_not_authorized) + } else { + Timber.e(x, "Failed to process jukebox task") + } + } + + private fun disableJukeboxOnError(x: Throwable, resourceId: Int) { + Timber.w(x.toString()) + val context = applicationContext() + Handler().post { toast(context, resourceId, false) } + mediaPlayerControllerLazy.value.isJukeboxEnabled = false + } + + fun updatePlaylist() { + if (!enabled) return + tasks.remove(Skip::class.java) + tasks.remove(Stop::class.java) + tasks.remove(Start::class.java) + val ids: MutableList = ArrayList() + for (file in downloader.all) { + ids.add(file.track.id) + } + tasks.add(SetPlaylist(ids)) + } + + fun skip(index: Int, offsetSeconds: Int) { + tasks.remove(Skip::class.java) + tasks.remove(Stop::class.java) + tasks.remove(Start::class.java) + startStatusUpdate() + if (jukeboxStatus != null) { + jukeboxStatus!!.positionSeconds = offsetSeconds + } + tasks.add(Skip(index, offsetSeconds)) + } + + fun stop() { + tasks.remove(Stop::class.java) + tasks.remove(Start::class.java) + stopStatusUpdate() + tasks.add(Stop()) + } + + fun start() { + tasks.remove(Stop::class.java) + tasks.remove(Start::class.java) + startStatusUpdate() + tasks.add(Start()) + } + + @Synchronized + fun adjustVolume(up: Boolean) { + val delta = if (up) 0.05f else -0.05f + gain += delta + gain = gain.coerceAtLeast(0.0f) + gain = gain.coerceAtMost(1.0f) + tasks.remove(SetGain::class.java) + tasks.add(SetGain(gain)) + val context = applicationContext() + if (volumeToast == null) volumeToast = VolumeToast(context) + volumeToast!!.setVolume(gain) + } + + private val musicService: MusicService + get() = getMusicService() + + val positionSeconds: Int + get() { + if (jukeboxStatus == null || jukeboxStatus!!.positionSeconds == null || timeOfLastUpdate.get() == 0L) { + return 0 + } + if (jukeboxStatus!!.isPlaying) { + val secondsSinceLastUpdate = + ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L).toInt() + return jukeboxStatus!!.positionSeconds!! + secondsSinceLastUpdate + } + return jukeboxStatus!!.positionSeconds!! + } + + var isEnabled: Boolean + set(enabled) { + Timber.d("Jukebox Service setting enabled to %b", enabled) + this.enabled = enabled + tasks.clear() + if (enabled) { + updatePlaylist() + } + stop() + } + get() { + return enabled + } + + private class TaskQueue { + private val queue = LinkedBlockingQueue() + fun add(jukeboxTask: JukeboxTask) { + queue.add(jukeboxTask) + } + + @Throws(InterruptedException::class) + fun take(): JukeboxTask { + return queue.take() + } + + fun remove(taskClass: Class) { + try { + val iterator = queue.iterator() + while (iterator.hasNext()) { + val task = iterator.next() + if (taskClass == task.javaClass) { + iterator.remove() + } + } + } catch (x: Throwable) { + Timber.w(x, "Failed to clean-up task queue.") + } + } + + fun clear() { + queue.clear() + } + } + + private abstract class JukeboxTask { + @Throws(Exception::class) + abstract fun execute(): JukeboxStatus + override fun toString(): String { + return javaClass.simpleName + } + } + + private inner class GetStatus : JukeboxTask() { + @Throws(Exception::class) + override fun execute(): JukeboxStatus { + return musicService.getJukeboxStatus() + } + } + + private inner class SetPlaylist(private val ids: List) : + JukeboxTask() { + @Throws(Exception::class) + override fun execute(): JukeboxStatus { + return musicService.updateJukeboxPlaylist(ids) + } + } + + private inner class Skip( + private val index: Int, + private val offsetSeconds: Int + ) : JukeboxTask() { + @Throws(Exception::class) + override fun execute(): JukeboxStatus { + return musicService.skipJukebox(index, offsetSeconds) + } + } + + private inner class Stop : JukeboxTask() { + @Throws(Exception::class) + override fun execute(): JukeboxStatus { + return musicService.stopJukebox() + } + } + + private inner class Start : JukeboxTask() { + @Throws(Exception::class) + override fun execute(): JukeboxStatus { + return musicService.startJukebox() + } + } + + private inner class SetGain(private val gain: Float) : JukeboxTask() { + @Throws(Exception::class) + override fun execute(): JukeboxStatus { + return musicService.setJukeboxGain(gain) + } + } + + private class VolumeToast(context: Context) : Toast(context) { + private val progressBar: ProgressBar + fun setVolume(volume: Float) { + progressBar.progress = (100 * volume).roundToInt() + show() + } + + init { + duration = LENGTH_SHORT + val inflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val view = inflater.inflate(R.layout.jukebox_volume, null) + progressBar = view.findViewById(R.id.jukebox_volume_progress_bar) as ProgressBar + setView(view) + setGravity(Gravity.TOP, 0, 0) + } + } + + companion object { + private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt deleted file mode 100644 index 0f69e247..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ /dev/null @@ -1,745 +0,0 @@ -/* - * LocalMediaPlayer.kt - * Copyright (C) 2009-2021 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.service - -import android.content.Context -import android.content.Context.POWER_SERVICE -import android.content.Intent -import android.media.MediaPlayer -import android.media.MediaPlayer.OnCompletionListener -import android.media.audiofx.AudioEffect -import android.os.Handler -import android.os.Looper -import android.os.PowerManager -import android.os.PowerManager.PARTIAL_WAKE_LOCK -import android.os.PowerManager.WakeLock -import androidx.lifecycle.MutableLiveData -import java.net.URLEncoder -import java.util.Locale -import kotlin.math.abs -import kotlin.math.max -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.moire.ultrasonic.audiofx.EqualizerController -import org.moire.ultrasonic.audiofx.VisualizerController -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline -import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.util.CancellableTask -import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.Storage -import org.moire.ultrasonic.util.StreamProxy -import org.moire.ultrasonic.util.Util -import timber.log.Timber - -/** - * Represents a Media Player which uses the mobile's resources for playback - */ -@Suppress("TooManyFunctions") -class LocalMediaPlayer : KoinComponent { - - private val audioFocusHandler by inject() - private val context by inject() - - @JvmField - var onSongCompleted: ((DownloadFile?) -> Unit?)? = null - - @JvmField - var onPrepared: (() -> Any?)? = null - - @JvmField - var onNextSongRequested: Runnable? = null - - @JvmField - @Volatile - var playerState = PlayerState.IDLE - - @JvmField - var currentPlaying: DownloadFile? = null - - @JvmField - var nextPlaying: DownloadFile? = null - - private var nextPlayerState = PlayerState.IDLE - private var nextSetup = false - private var nextPlayingTask: CancellableTask? = null - private var mediaPlayer: MediaPlayer = MediaPlayer() - private var nextMediaPlayer: MediaPlayer? = null - private var mediaPlayerLooper: Looper? = null - private var mediaPlayerHandler: Handler? = null - private var cachedPosition = 0 - private var proxy: StreamProxy? = null - private var bufferTask: CancellableTask? = null - private var positionCache: PositionCache? = null - - private val pm = context.getSystemService(POWER_SERVICE) as PowerManager - private val wakeLock: WakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, this.javaClass.name) - - val secondaryProgress: MutableLiveData = MutableLiveData(0) - - fun init() { - Thread { - Thread.currentThread().name = "MediaPlayerThread" - Looper.prepare() - mediaPlayer.setWakeMode(context, PARTIAL_WAKE_LOCK) - mediaPlayer.setOnErrorListener { _, what, more -> - handleError( - Exception( - String.format( - Locale.getDefault(), - "MediaPlayer error: %d (%d)", what, more - ) - ) - ) - false - } - try { - val i = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId) - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) - context.sendBroadcast(i) - } catch (ignored: Throwable) { - // Froyo or lower - } - mediaPlayerLooper = Looper.myLooper() - mediaPlayerHandler = Handler(mediaPlayerLooper!!) - Looper.loop() - }.start() - - // Create Equalizer and Visualizer on a new thread as this can potentially take some time - Thread { - EqualizerController.create(context, mediaPlayer) - VisualizerController.create(mediaPlayer) - }.start() - - wakeLock.setReferenceCounted(false) - Timber.i("LocalMediaPlayer created") - } - - fun release() { - // Calling reset() will result in changing this player's state. If we allow - // the onPlayerStateChanged callback, then the state change will cause this - // to resurrect the media session which has just been destroyed. - reset() - try { - val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId) - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) - context.sendBroadcast(i) - EqualizerController.release() - VisualizerController.release() - mediaPlayer.release() - - mediaPlayer = MediaPlayer() - - if (nextMediaPlayer != null) { - nextMediaPlayer!!.release() - } - mediaPlayerLooper!!.quit() - if (bufferTask != null) { - bufferTask!!.cancel() - } - if (nextPlayingTask != null) { - nextPlayingTask!!.cancel() - } - - wakeLock.release() - } catch (exception: Throwable) { - Timber.w(exception, "LocalMediaPlayer onDestroy exception: ") - } - Timber.i("LocalMediaPlayer destroyed") - } - - @Synchronized - fun setPlayerState(playerState: PlayerState, track: DownloadFile?) { - Timber.i("%s -> %s (%s)", this.playerState.name, playerState.name, track) - synchronized(playerState) { - this.playerState = playerState - } - if (playerState === PlayerState.STARTED) { - audioFocusHandler.requestAudioFocus() - } - - RxBus.playerStatePublisher.onNext(RxBus.StateWithTrack(playerState, track)) - - if (playerState === PlayerState.STARTED && positionCache == null) { - positionCache = PositionCache() - val thread = Thread(positionCache) - thread.start() - } else if (playerState !== PlayerState.STARTED && positionCache != null) { - positionCache!!.stop() - positionCache = null - } - } - - /* - * Set the current playing file. It's called with null to reset the player. - */ - @Synchronized - fun setCurrentPlaying(currentPlaying: DownloadFile?) { - // In some cases this function is called twice - if (this.currentPlaying == currentPlaying) return - this.currentPlaying = currentPlaying - RxBus.playerStatePublisher.onNext(RxBus.StateWithTrack(playerState, currentPlaying)) - } - - /* - * Set the next playing file. nextToPlay cannot be null - */ - @Synchronized - fun setNextPlaying(nextToPlay: DownloadFile) { - nextPlaying = nextToPlay - nextPlayingTask = CheckCompletionTask(nextPlaying) - nextPlayingTask?.start() - } - - /* - * Clear the next playing file. setIdle controls whether the playerState is affected as well - */ - @Synchronized - fun clearNextPlaying(setIdle: Boolean) { - nextSetup = false - nextPlaying = null - if (nextPlayingTask != null) { - nextPlayingTask!!.cancel() - nextPlayingTask = null - } - - if (setIdle) { - setNextPlayerState(PlayerState.IDLE) - } - } - - @Synchronized - fun setNextPlayerState(playerState: PlayerState) { - Timber.i("Next: %s -> %s (%s)", nextPlayerState.name, playerState.name, nextPlaying) - nextPlayerState = playerState - } - - /* - * Public method to play a given file. - * Optionally specify a position to start at. - */ - @Synchronized - @JvmOverloads - fun play(fileToPlay: DownloadFile?, position: Int = 0, autoStart: Boolean = true) { - if (nextPlayingTask != null) { - nextPlayingTask!!.cancel() - nextPlayingTask = null - } - setCurrentPlaying(fileToPlay) - - if (fileToPlay != null) { - bufferAndPlay(fileToPlay, position, autoStart) - } - } - - @Synchronized - fun playNext() { - if (nextMediaPlayer == null || nextPlaying == null) return - - mediaPlayer = nextMediaPlayer!! - - setCurrentPlaying(nextPlaying) - setPlayerState(PlayerState.STARTED, currentPlaying) - - attachHandlersToPlayer(mediaPlayer, nextPlaying!!, false) - - postRunnable(onNextSongRequested) - - // Proxy should not be being used here since the next player was already setup to play - proxy?.stop() - proxy = null - } - - @Synchronized - fun pause() { - try { - mediaPlayer.pause() - } catch (x: Exception) { - handleError(x) - } - } - - @Synchronized - fun start() { - try { - mediaPlayer.start() - } catch (x: Exception) { - handleError(x) - } - } - - @Synchronized - fun seekTo(position: Int) { - try { - mediaPlayer.seekTo(position) - cachedPosition = position - } catch (x: Exception) { - handleError(x) - } - } - - @get:Synchronized - val playerPosition: Int - get() = try { - when (playerState) { - PlayerState.IDLE -> 0 - PlayerState.DOWNLOADING -> 0 - PlayerState.PREPARING -> 0 - else -> cachedPosition - } - } catch (x: Exception) { - handleError(x) - 0 - } - - @get:Synchronized - val playerDuration: Int - get() { - if (currentPlaying != null) { - val duration = currentPlaying!!.track.duration - if (duration != null) { - return duration * 1000 - } - } - if (playerState !== PlayerState.IDLE && - playerState !== PlayerState.DOWNLOADING && - playerState !== PlayerState.PREPARING - ) { - try { - return mediaPlayer.duration - } catch (x: Exception) { - handleError(x) - } - } - return 0 - } - - fun setVolume(volume: Float) { - mediaPlayer.setVolume(volume, volume) - } - - @Synchronized - private fun bufferAndPlay(fileToPlay: DownloadFile, position: Int, autoStart: Boolean) { - if (playerState !== PlayerState.PREPARED && !fileToPlay.isWorkDone) { - reset() - bufferTask = BufferTask(fileToPlay, position, autoStart) - bufferTask!!.start() - } else { - doPlay(fileToPlay, position, autoStart) - } - } - - @Synchronized - private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) { - setPlayerState(PlayerState.IDLE, downloadFile) - - // In many cases we will be resetting the mediaPlayer a second time here. - // figure out if we can remove this call... - resetMediaPlayer() - - try { - downloadFile.setPlaying(false) - - val file = Storage.getFromPath(downloadFile.completeOrPartialFile) - val partial = !downloadFile.isCompleteFileAvailable - - // TODO this won't work with SAF, we should use something else, e.g. a recent list - // downloadFile.updateModificationDate() - mediaPlayer.setOnCompletionListener(null) - - setAudioAttributes(mediaPlayer) - - var streamUrl: String? = null - if (partial) { - if (proxy == null) { - proxy = StreamProxy(object : Supplier() { - override fun get(): DownloadFile { - return currentPlaying!! - } - }) - proxy!!.start() - } - streamUrl = String.format( - Locale.getDefault(), "http://127.0.0.1:%d/%s", - proxy!!.port, URLEncoder.encode(file!!.path, Constants.UTF_8) - ) - Timber.i("Data Source: %s", streamUrl) - } else if (proxy != null) { - proxy?.stop() - proxy = null - } - - Timber.i("Preparing media player") - - if (streamUrl != null) { - Timber.v("LocalMediaPlayer doPlay dataSource: %s", streamUrl) - mediaPlayer.setDataSource(streamUrl) - } else { - Timber.v("LocalMediaPlayer doPlay Path: %s", file!!.path) - val descriptor = file.getDocumentFileDescriptor("r")!! - mediaPlayer.setDataSource(descriptor.fileDescriptor) - descriptor.close() - } - - setPlayerState(PlayerState.PREPARING, downloadFile) - - mediaPlayer.setOnBufferingUpdateListener { mp, percent -> - val song = downloadFile.track - - if (percent == 100) { - mp.setOnBufferingUpdateListener(null) - } - - // The secondary progress is an indicator of how far the song is cached. - if (song.transcodedContentType == null && Settings.maxBitRate == 0) { - val progress = (percent.toDouble() / 100.toDouble() * playerDuration).toInt() - secondaryProgress.postValue(progress) - } - } - - mediaPlayer.setOnPreparedListener { - Timber.i("Media player prepared") - setPlayerState(PlayerState.PREPARED, downloadFile) - - // Populate seek bar secondary progress if we have a complete file for consistency - if (downloadFile.isWorkDone) { - secondaryProgress.postValue(playerDuration) - } - - synchronized(this@LocalMediaPlayer) { - if (position != 0) { - Timber.i("Restarting player from position %d", position) - seekTo(position) - } - cachedPosition = position - if (start) { - mediaPlayer.start() - setPlayerState(PlayerState.STARTED, downloadFile) - } else { - setPlayerState(PlayerState.PAUSED, downloadFile) - } - } - - postRunnable { - onPrepared - } - } - - attachHandlersToPlayer(mediaPlayer, downloadFile, partial) - mediaPlayer.prepareAsync() - } catch (x: Exception) { - handleError(x) - } - } - - private fun setAudioAttributes(player: MediaPlayer) { - player.setAudioAttributes(AudioFocusHandler.getAudioAttributes()) - } - - @Suppress("ComplexCondition") - @Synchronized - private fun setupNext(downloadFile: DownloadFile) { - try { - val file = Storage.getFromPath(downloadFile.completeOrPartialFile) - - // Release the media player if it is not our active player - if (nextMediaPlayer != null && nextMediaPlayer != mediaPlayer) { - nextMediaPlayer!!.setOnCompletionListener(null) - nextMediaPlayer!!.release() - nextMediaPlayer = null - } - nextMediaPlayer = MediaPlayer() - nextMediaPlayer!!.setWakeMode(context, PARTIAL_WAKE_LOCK) - - setAudioAttributes(nextMediaPlayer!!) - - // This has nothing to do with the MediaSession, it is used to associate - // the equalizer or visualizer with the player - try { - nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId - } catch (ignored: Throwable) { - } - - Timber.v("LocalMediaPlayer setupNext Path: %s", file!!.path) - val descriptor = file.getDocumentFileDescriptor("r")!! - nextMediaPlayer!!.setDataSource(descriptor.fileDescriptor) - descriptor.close() - - setNextPlayerState(PlayerState.PREPARING) - nextMediaPlayer!!.setOnPreparedListener { - try { - setNextPlayerState(PlayerState.PREPARED) - if (Settings.gaplessPlayback && - (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) - ) { - mediaPlayer.setNextMediaPlayer(nextMediaPlayer) - nextSetup = true - } - } catch (x: Exception) { - handleErrorNext(x) - } - } - nextMediaPlayer!!.setOnErrorListener { _, what, extra -> - Timber.w("Error on playing next (%d, %d): %s", what, extra, downloadFile) - true - } - nextMediaPlayer!!.prepareAsync() - } catch (x: Exception) { - handleErrorNext(x) - } - } - - private fun attachHandlersToPlayer( - mediaPlayer: MediaPlayer, - downloadFile: DownloadFile, - isPartial: Boolean - ) { - mediaPlayer.setOnErrorListener { _, what, extra -> - Timber.w("Error on playing file (%d, %d): %s", what, extra, downloadFile) - val pos = cachedPosition - reset() - downloadFile.setPlaying(false) - doPlay(downloadFile, pos, true) - downloadFile.setPlaying(true) - true - } - - var duration = 0 - if (downloadFile.track.duration != null) { - duration = downloadFile.track.duration!! * 1000 - } - - mediaPlayer.setOnCompletionListener(object : OnCompletionListener { - override fun onCompletion(mediaPlayer: MediaPlayer) { - // Acquire a temporary wakelock, since when we return from - // this callback the MediaPlayer will release its wakelock - // and allow the device to go to sleep. - wakeLock.acquire(60000) - val pos = cachedPosition - Timber.i("Ending position %d of %d", pos, duration) - - if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) { - setPlayerState(PlayerState.COMPLETED, downloadFile) - if (Settings.gaplessPlayback && - nextPlaying != null && - nextPlayerState === PlayerState.PREPARED - ) { - if (nextSetup) { - nextSetup = false - } - playNext() - } else { - if (onSongCompleted != null) { - val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onSongCompleted!!(currentPlaying) } - mainHandler.post(myRunnable) - } - } - return - } - - synchronized(this) { - if (downloadFile.isWorkDone) { - // Complete was called early even though file is fully buffered - Timber.i("Requesting restart from %d of %d", pos, duration) - reset() - downloadFile.setPlaying(false) - doPlay(downloadFile, pos, true) - downloadFile.setPlaying(true) - } else { - Timber.i("Requesting restart from %d of %d", pos, duration) - reset() - bufferTask = BufferTask(downloadFile, pos) - bufferTask!!.start() - } - } - } - }) - } - - @Synchronized - fun reset() { - if (bufferTask != null) { - bufferTask!!.cancel() - } - - resetMediaPlayer() - - try { - setPlayerState(PlayerState.IDLE, currentPlaying) - mediaPlayer.setOnErrorListener(null) - mediaPlayer.setOnCompletionListener(null) - } catch (x: Exception) { - handleError(x) - } - } - - @Synchronized - fun resetMediaPlayer() { - try { - mediaPlayer.reset() - } catch (x: Exception) { - Timber.w(x, "MediaPlayer was released but LocalMediaPlayer was not destroyed") - - // Recreate MediaPlayer - mediaPlayer = MediaPlayer() - } - } - - private inner class BufferTask( - private val downloadFile: DownloadFile, - private val position: Int, - private val autoStart: Boolean = true - ) : CancellableTask() { - private val expectedFileSize: Long - private val partialFile: String = downloadFile.partialFile - - override fun execute() { - setPlayerState(PlayerState.DOWNLOADING, downloadFile) - while (!bufferComplete() && !isOffline()) { - Util.sleepQuietly(1000L) - if (isCancelled) { - return - } - } - - doPlay(downloadFile, position, autoStart) - } - - private fun bufferComplete(): Boolean { - val completeFileAvailable = downloadFile.isWorkDone - val size = Storage.getFromPath(partialFile)?.length ?: 0 - - Timber.i( - "Buffering %s (%d/%d, %s)", - partialFile, size, expectedFileSize, completeFileAvailable - ) - - return completeFileAvailable || size >= expectedFileSize - } - - override fun toString(): String { - return String.format("BufferTask (%s)", downloadFile) - } - - init { - var bufferLength = Settings.bufferLength.toLong() - if (bufferLength == 0L) { - // Set to seconds in a day, basically infinity - bufferLength = 86400L - } - - // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. - val bitRate = downloadFile.getBitRate() - val byteCount = max(100000, bitRate * 1024L / 8L * bufferLength) - - // Find out how large the file should grow before resuming playback. - Timber.i("Buffering from position %d and bitrate %d", position, bitRate) - expectedFileSize = position * bitRate / 8 + byteCount - } - } - - private inner class CheckCompletionTask(downloadFile: DownloadFile?) : CancellableTask() { - private val downloadFile: DownloadFile? - private val partialFile: String? - override fun execute() { - Thread.currentThread().name = "CheckCompletionTask" - if (downloadFile == null) { - return - } - - // Do an initial sleep so this prepare can't compete with main prepare - Util.sleepQuietly(5000L) - while (!bufferComplete()) { - Util.sleepQuietly(5000L) - if (isCancelled) { - return - } - } - - // Start the setup of the next media player - mediaPlayerHandler!!.post { setupNext(downloadFile) } - } - - private fun bufferComplete(): Boolean { - val completeFileAvailable = downloadFile!!.isWorkDone - val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) - - val length = if (partialFile == null) 0 - else Storage.getFromPath(partialFile)?.length ?: 0 - - Timber.i("Buffering next %s (%d)", partialFile, length) - - return completeFileAvailable && state - } - - override fun toString(): String { - return String.format("CheckCompletionTask (%s)", downloadFile) - } - - init { - setNextPlayerState(PlayerState.IDLE) - this.downloadFile = downloadFile - partialFile = downloadFile?.partialFile - } - } - - private inner class PositionCache : Runnable { - var isRunning = true - fun stop() { - isRunning = false - } - - override fun run() { - Thread.currentThread().name = "PositionCache" - - // Stop checking position before the song reaches completion - while (isRunning) { - try { - if (playerState === PlayerState.STARTED) { - synchronized(playerState) { - if (playerState === PlayerState.STARTED) { - cachedPosition = mediaPlayer.currentPosition - } - } - RxBus.playbackPositionPublisher.onNext(cachedPosition) - } - Util.sleepQuietly(100L) - } catch (e: Exception) { - Timber.w(e, "Crashed getting current position") - isRunning = false - positionCache = null - } - } - } - } - - private fun handleError(x: Exception) { - Timber.w(x, "Media player error") - try { - mediaPlayer.reset() - } catch (ex: Exception) { - Timber.w(ex, "Exception encountered when resetting media player") - } - } - - private fun handleErrorNext(x: Exception) { - Timber.w(x, "Next Media player error") - nextMediaPlayer!!.reset() - } - - private fun postRunnable(runnable: Runnable?) { - if (runnable != null) { - val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { runnable.run() } - mainHandler.post(myRunnable) - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 9e17fde5..2f89e21b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -6,20 +6,34 @@ */ package org.moire.ultrasonic.service +import android.content.ComponentName +import android.content.Context import android.content.Intent +import androidx.core.net.toUri +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.Player +import androidx.media3.common.Player.STATE_BUFFERING +import androidx.media3.common.Timeline +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.google.common.util.concurrent.MoreExecutors import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.domain.RepeatMode import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.MediaPlayerService.Companion.executeOnStartedMediaPlayerService -import org.moire.ultrasonic.service.MediaPlayerService.Companion.getInstance -import org.moire.ultrasonic.service.MediaPlayerService.Companion.runningInstance +import org.moire.ultrasonic.playback.LegacyPlaylistManager +import org.moire.ultrasonic.playback.PlaybackService +import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1 +import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2 +import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3 +import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4 +import org.moire.ultrasonic.service.DownloadService.Companion.getInstance import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.ShufflePlayBuffer import timber.log.Timber /** @@ -32,8 +46,8 @@ class MediaPlayerController( private val playbackStateSerializer: PlaybackStateSerializer, private val externalStorageMonitor: ExternalStorageMonitor, private val downloader: Downloader, - private val shufflePlayBuffer: ShufflePlayBuffer, - private val localMediaPlayer: LocalMediaPlayer + private val legacyPlaylistManager: LegacyPlaylistManager, + val context: Context ) : KoinComponent { private var created = false @@ -42,22 +56,142 @@ class MediaPlayerController( var showVisualization = false private var autoPlayStart = false + private val scrobbler = Scrobbler() + private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() private val activeServerProvider: ActiveServerProvider by inject() + private var sessionToken = + SessionToken(context, ComponentName(context, PlaybackService::class.java)) + + private var mediaControllerFuture = MediaController.Builder( + context, + sessionToken + ).buildAsync() + + var controller: MediaController? = null + fun onCreate() { if (created) return externalStorageMonitor.onCreate { reset() } isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault + + mediaControllerFuture.addListener({ + controller = mediaControllerFuture.get() + + controller?.addListener(object : Player.Listener { + /* + * This will be called everytime the playlist has changed. + */ + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + legacyPlaylistManager.rebuildPlaylist(controller!!) + } + + override fun onPlaybackStateChanged(playbackState: Int) { + translatePlaybackState(playbackState = playbackState) + playerStateChangedHandler() + publishPlaybackState() + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + translatePlaybackState(isPlaying = isPlaying) + playerStateChangedHandler() + publishPlaybackState() + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + legacyPlaylistManager.updateCurrentPlaying(mediaItem) + publishPlaybackState() + } + }) + + //controller?.play() + }, MoreExecutors.directExecutor()) + created = true Timber.i("MediaPlayerController created") } + @Suppress("DEPRECATION") + fun translatePlaybackState( + playbackState: Int = controller?.playbackState ?: 0, + isPlaying: Boolean = controller?.isPlaying ?: false + ) { + legacyPlayerState = when (playbackState) { + STATE_BUFFERING -> PlayerState.DOWNLOADING + Player.STATE_ENDED -> { + PlayerState.COMPLETED + } + Player.STATE_IDLE -> { + PlayerState.IDLE + } + Player.STATE_READY -> { + if (isPlaying) { + PlayerState.STARTED + } else { + PlayerState.PAUSED + } + } + else -> { + PlayerState.IDLE + } + } + } + + private fun playerStateChangedHandler() { + + val playerState = legacyPlayerState + val currentPlaying = legacyPlaylistManager.currentPlaying + + when { + playerState === PlayerState.PAUSED -> { + // TODO: Save playlist +// playbackStateSerializer.serialize( +// downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition +// ) + } + playerState === PlayerState.STARTED -> { + scrobbler.scrobble(currentPlaying, false) + } + playerState === PlayerState.COMPLETED -> { + scrobbler.scrobble(currentPlaying, true) + } + } + + //Update widget + if (currentPlaying != null) { + updateWidget(playerState, currentPlaying.track) + } + + Timber.d("Processed player state change") + } + + private fun publishPlaybackState() { + RxBus.playerStatePublisher.onNext( + RxBus.StateWithTrack( + state = legacyPlayerState, + track = legacyPlaylistManager.currentPlaying, + index = currentMediaItemIndex + ) + ) + } + + private fun updateWidget(playerState: PlayerState, song: Track?) { + val started = playerState === PlayerState.STARTED + val context = UApp.applicationContext() + + UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(context, song, started, false) + UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(context, song, started, true) + UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(context, song, started, false) + UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false) + } + fun onDestroy() { if (!created) return val context = UApp.applicationContext() externalStorageMonitor.onDestroy() - context.stopService(Intent(context, MediaPlayerService::class.java)) + context.stopService(Intent(context, DownloadService::class.java)) + legacyPlaylistManager.onDestroy() downloader.onDestroy() created = false Timber.i("MediaPlayerController destroyed") @@ -73,33 +207,30 @@ class MediaPlayerController( ) { addToPlaylist( songs, - save = false, + cachePermanently = false, autoPlay = false, playNext = false, shuffle = false, newPlaylist = newPlaylist ) + if (currentPlayingIndex != -1) { - executeOnStartedMediaPlayerService { mediaPlayerService: MediaPlayerService -> - mediaPlayerService.play(currentPlayingIndex, autoPlayStart) - if (localMediaPlayer.currentPlaying != null) { - if (autoPlay && jukeboxMediaPlayer.isEnabled) { - jukeboxMediaPlayer.skip( - downloader.currentPlayingIndex, - currentPlayingPosition / 1000 - ) - } else { - if (localMediaPlayer.currentPlaying!!.isCompleteFileAvailable) { - localMediaPlayer.play( - localMediaPlayer.currentPlaying, - currentPlayingPosition, - autoPlay - ) - } - } - } - autoPlayStart = false + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.skip( + currentPlayingIndex, + currentPlayingPosition / 1000 + ) + } else { + seekTo(currentPlayingIndex, currentPlayingPosition) } + + if (autoPlay) { + prepare() + play() + } + + autoPlayStart = false + } } @@ -110,93 +241,139 @@ class MediaPlayerController( @Synchronized fun play(index: Int) { - executeOnStartedMediaPlayerService { service: MediaPlayerService -> - service.play(index, true) - } + controller?.seekTo(index, 0L) + controller?.play() } @Synchronized fun play() { - executeOnStartedMediaPlayerService { service: MediaPlayerService -> - service.play() + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.start() + } else { + controller?.play() } } + @Synchronized + fun prepare() { + controller?.prepare() + } + @Synchronized fun resumeOrPlay() { - executeOnStartedMediaPlayerService { service: MediaPlayerService -> - service.resumeOrPlay() - } + controller?.play() } @Synchronized fun togglePlayPause() { - if (localMediaPlayer.playerState === PlayerState.IDLE) autoPlayStart = true - executeOnStartedMediaPlayerService { service: MediaPlayerService -> - service.togglePlayPause() + if (playbackState == Player.STATE_IDLE) autoPlayStart = true + if (controller?.isPlaying == false) { + controller?.pause() + } else { + controller?.play() } } - @Synchronized - fun start() { - executeOnStartedMediaPlayerService { service: MediaPlayerService -> - service.start() - } - } @Synchronized fun seekTo(position: Int) { - val mediaPlayerService = runningInstance - mediaPlayerService?.seekTo(position) + controller?.seekTo(position.toLong()) + } + + @Synchronized + fun seekTo(index: Int, position: Int) { + controller?.seekTo(index, position.toLong()) } @Synchronized fun pause() { - val mediaPlayerService = runningInstance - mediaPlayerService?.pause() + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.stop() + } else { + controller?.pause() + } } @Synchronized fun stop() { - val mediaPlayerService = runningInstance - mediaPlayerService?.stop() + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.stop() + } else { + controller?.stop() + } } + @Synchronized + @Deprecated("Use InsertionMode Syntax") @Suppress("LongParameterList") fun addToPlaylist( songs: List?, - save: Boolean, + cachePermanently: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, newPlaylist: Boolean ) { if (songs == null) return - val filteredSongs = songs.filterNotNull() - downloader.addToPlaylist(filteredSongs, save, autoPlay, playNext, newPlaylist) - jukeboxMediaPlayer.updatePlaylist() - if (shuffle) shuffle() - val isLastTrack = (downloader.getPlaylist().size - 1 == downloader.currentPlayingIndex) - if (!playNext && !autoPlay && isLastTrack) { - val mediaPlayerService = runningInstance - mediaPlayerService?.setNextPlaying() + val insertionMode = when { + newPlaylist -> InsertionMode.CLEAR + playNext -> InsertionMode.AFTER_CURRENT + else -> InsertionMode.APPEND } + val filteredSongs = songs.filterNotNull() + + addToPlaylist( + filteredSongs, cachePermanently, autoPlay, shuffle, insertionMode + ) + } + + @Synchronized + fun addToPlaylist( + songs: List, + cachePermanently: Boolean, + autoPlay: Boolean, + shuffle: Boolean, + insertionMode: InsertionMode + ) { + var insertAt = 0 + + if (insertionMode == InsertionMode.CLEAR) { + clear() + } + + when (insertionMode) { + InsertionMode.CLEAR -> clear() + InsertionMode.APPEND -> insertAt = mediaItemCount + InsertionMode.AFTER_CURRENT -> insertAt = currentMediaItemIndex + } + + val mediaItems: List = songs.map { + val downloadFile = downloader.getDownloadFileForSong(it) + if (cachePermanently) downloadFile.shouldSave = true + val result = it.toMediaItem() + legacyPlaylistManager.addToCache(result, downloader.getDownloadFileForSong(it)) + result + } + + controller?.addMediaItems(insertAt, mediaItems) + + jukeboxMediaPlayer.updatePlaylist() + + if (shuffle) isShufflePlayEnabled = true + if (autoPlay) { + prepare() play(0) } else { - if (localMediaPlayer.currentPlaying == null && downloader.getPlaylist().isNotEmpty()) { - localMediaPlayer.currentPlaying = downloader.getPlaylist()[0] - downloader.getPlaylist()[0].setPlaying(true) - } downloader.checkDownloads() } playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, + legacyPlaylistManager.playlist, + currentMediaItemIndex, playerPosition ) } @@ -206,77 +383,60 @@ class MediaPlayerController( if (songs == null) return val filteredSongs = songs.filterNotNull() downloader.downloadBackground(filteredSongs, save) + playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, + legacyPlaylistManager.playlist, + currentMediaItemIndex, playerPosition ) } - @Synchronized - fun setCurrentPlaying(index: Int) { - val mediaPlayerService = runningInstance - mediaPlayerService?.setCurrentPlaying(index) - } - fun stopJukeboxService() { jukeboxMediaPlayer.stopJukeboxService() } @set:Synchronized var isShufflePlayEnabled: Boolean - get() = shufflePlayBuffer.isEnabled + get() = controller?.shuffleModeEnabled == true set(enabled) { - shufflePlayBuffer.isEnabled = enabled + controller?.shuffleModeEnabled = enabled if (enabled) { - clear() downloader.checkDownloads() } } @Synchronized - fun shuffle() { - downloader.shuffle() - playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, - playerPosition - ) - jukeboxMediaPlayer.updatePlaylist() - val mediaPlayerService = runningInstance - mediaPlayerService?.setNextPlaying() + fun toggleShuffle() { + isShufflePlayEnabled = !isShufflePlayEnabled } + val bufferedPercentage: Int + get() = controller?.bufferedPercentage ?: 0 + @Synchronized fun moveItemInPlaylist(oldPos: Int, newPos: Int) { - downloader.moveItemInPlaylist(oldPos, newPos) + controller?.moveMediaItem(oldPos, newPos) } @set:Synchronized - var repeatMode: RepeatMode - get() = Settings.repeatMode - set(repeatMode) { - Settings.repeatMode = repeatMode - val mediaPlayerService = runningInstance - mediaPlayerService?.setNextPlaying() + var repeatMode: Int + get() = controller?.repeatMode ?: 0 + set(newMode) { + controller?.repeatMode = newMode } @Synchronized @JvmOverloads fun clear(serialize: Boolean = true) { - val mediaPlayerService = runningInstance - if (mediaPlayerService != null) { - mediaPlayerService.clear(serialize) - } else { - // If no MediaPlayerService is available, just empty the playlist - downloader.clearPlaylist() - if (serialize) { - playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, playerPosition - ) - } + + controller?.clearMediaItems() + + if (controller != null && serialize) { + playbackStateSerializer.serialize( + listOf(), -1, 0 + ) } + jukeboxMediaPlayer.updatePlaylist() } @@ -289,11 +449,12 @@ class MediaPlayerController( fun clearIncomplete() { reset() - downloader.clearIncomplete() + downloader.clearActiveDownloads() + downloader.clearBackground() playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, + legacyPlaylistManager.playlist, + currentMediaItemIndex, playerPosition ) @@ -301,26 +462,17 @@ class MediaPlayerController( } @Synchronized - // TODO: If a playlist contains an item twice, this call will wrongly remove all + // FIXME + // With the new API we can only remove by index!! fun removeFromPlaylist(downloadFile: DownloadFile) { - if (downloadFile == localMediaPlayer.currentPlaying) { - reset() - currentPlaying = null - } - downloader.removeFromPlaylist(downloadFile) playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, + legacyPlaylistManager.playlist, + legacyPlaylistManager.currentPlayingIndex, playerPosition ) jukeboxMediaPlayer.updatePlaylist() - - if (downloadFile == localMediaPlayer.nextPlaying) { - val mediaPlayerService = runningInstance - mediaPlayerService?.setNextPlaying() - } } @Synchronized @@ -341,80 +493,56 @@ class MediaPlayerController( @Synchronized fun previous() { - val index = downloader.currentPlayingIndex - if (index == -1) { - return - } - - // Restart song if played more than five seconds. - @Suppress("MagicNumber") - if (playerPosition > 5000 || index == 0) { - play(index) - } else { - play(index - 1) - } + controller?.seekToPrevious() } @Synchronized operator fun next() { - val index = downloader.currentPlayingIndex - if (index != -1) { - when (repeatMode) { - RepeatMode.SINGLE, RepeatMode.OFF -> { - // Play next if exists - if (index + 1 >= 0 && index + 1 < downloader.getPlaylist().size) { - play(index + 1) - } - } - RepeatMode.ALL -> { - play((index + 1) % downloader.getPlaylist().size) - } - else -> { - } - } - } + controller?.seekToNext() } @Synchronized fun reset() { - val mediaPlayerService = runningInstance - if (mediaPlayerService != null) localMediaPlayer.reset() + controller?.clearMediaItems() } @get:Synchronized val playerPosition: Int get() { - val mediaPlayerService = runningInstance ?: return 0 - return mediaPlayerService.playerPosition + return if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.positionSeconds * 1000 + } else { + controller?.currentPosition?.toInt() ?: 0 + } } @get:Synchronized val playerDuration: Int get() { - val mediaPlayerService = runningInstance ?: return 0 - return mediaPlayerService.playerDuration + return controller?.duration?.toInt() ?: return 0 } + @Deprecated("Use Controller.playbackState and Controller.isPlaying") @set:Synchronized - var playerState: PlayerState - get() = localMediaPlayer.playerState - set(state) { - val mediaPlayerService = runningInstance - if (mediaPlayerService != null) - localMediaPlayer.setPlayerState(state, localMediaPlayer.currentPlaying) - } + var legacyPlayerState: PlayerState = PlayerState.IDLE + + val playbackState: Int + get() = controller?.playbackState ?: 0 + + val isPlaying: Boolean + get() = controller?.isPlaying ?: false @set:Synchronized var isJukeboxEnabled: Boolean get() = jukeboxMediaPlayer.isEnabled set(jukeboxEnabled) { jukeboxMediaPlayer.isEnabled = jukeboxEnabled - playerState = PlayerState.IDLE + if (jukeboxEnabled) { jukeboxMediaPlayer.startJukeboxService() reset() - // Cancel current download, if necessary. + // Cancel current downloads downloader.clearActiveDownloads() } else { jukeboxMediaPlayer.stopJukeboxService() @@ -441,19 +569,12 @@ class MediaPlayerController( } fun setVolume(volume: Float) { - if (runningInstance != null) localMediaPlayer.setVolume(volume) - } - - private fun updateNotification() { - runningInstance?.updateNotification( - localMediaPlayer.playerState, - localMediaPlayer.currentPlaying - ) + controller?.volume = volume } fun toggleSongStarred() { - if (localMediaPlayer.currentPlaying == null) return - val song = localMediaPlayer.currentPlaying!!.track + if (legacyPlaylistManager.currentPlaying == null) return + val song = legacyPlaylistManager.currentPlaying!!.track Thread { val musicService = getMusicService() @@ -469,15 +590,16 @@ class MediaPlayerController( }.start() // Trigger an update - localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) + // TODO Update Metadata of MediaItem... + //localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) song.starred = !song.starred } @Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions fun setSongRating(rating: Int) { if (!Settings.useFiveStarRating) return - if (localMediaPlayer.currentPlaying == null) return - val song = localMediaPlayer.currentPlaying!!.track + if (legacyPlaylistManager.currentPlaying == null) return + val song = legacyPlaylistManager.currentPlaying!!.track song.userRating = rating Thread { try { @@ -487,27 +609,33 @@ class MediaPlayerController( } }.start() // TODO this would be better handled with a Rx command - updateNotification() + //updateNotification() } - @set:Synchronized - var currentPlaying: DownloadFile? - get() = localMediaPlayer.currentPlaying - set(currentPlaying) { - if (runningInstance != null) localMediaPlayer.setCurrentPlaying(currentPlaying) - } + val currentMediaItem: MediaItem? + get() = controller?.currentMediaItem + val currentMediaItemIndex: Int + get() = controller?.currentMediaItemIndex ?: -1 + + @Deprecated("Use currentMediaItem") + val currentPlayingLegacy: DownloadFile? + get() = legacyPlaylistManager.currentPlaying + + val mediaItemCount: Int + get() = controller?.mediaItemCount ?: 0 + + @Deprecated("Use mediaItemCount") val playlistSize: Int - get() = downloader.getPlaylist().size - - val currentPlayingNumberOnPlaylist: Int - get() = downloader.currentPlayingIndex + get() = legacyPlaylistManager.playlist.size + @Deprecated("Use native APIs") val playList: List - get() = downloader.getPlaylist() + get() = legacyPlaylistManager.playlist + @Deprecated("Use timeline") val playListDuration: Long - get() = downloader.downloadListDuration + get() = legacyPlaylistManager.playlistDuration fun getDownloadFileForSong(song: Track): DownloadFile { return downloader.getDownloadFileForSong(song) @@ -516,4 +644,30 @@ class MediaPlayerController( init { Timber.i("MediaPlayerController constructed") } + + enum class InsertionMode { + CLEAR, APPEND, AFTER_CURRENT + } } + + +fun Track.toMediaItem(): MediaItem { + + val filePath = FileUtil.getSongFile(this) + val bitrate = Settings.maxBitRate + val uri = "$id|$bitrate|$filePath" + + val metadata = MediaMetadata.Builder() + metadata.setTitle(title) + .setArtist(artist) + .setAlbumTitle(album) + .setMediaUri(uri.toUri()) + .setAlbumArtist(artist) + + val mediaItem = MediaItem.Builder() + .setUri(uri) + .setMediaId(id) + .setMediaMetadata(metadata.build()) + + return mediaItem.build() +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 87288df8..a12e83c0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -8,32 +8,23 @@ package org.moire.ultrasonic.service import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.media.AudioManager import android.view.KeyEvent import io.reactivex.rxjava3.disposables.Disposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp.Companion.applicationContext -import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.ifNotNull import timber.log.Timber /** * This class is responsible for handling received events for the Media Player implementation - * - * @author Sindre Mehus */ class MediaPlayerLifecycleSupport : KoinComponent { private val playbackStateSerializer by inject() private val mediaPlayerController by inject() - private val downloader by inject() private var created = false private var headsetEventReceiver: BroadcastReceiver? = null @@ -50,11 +41,6 @@ class MediaPlayerLifecycleSupport : KoinComponent { return } - mediaButtonEventSubscription = RxBus.mediaButtonEventObservable.subscribe { - handleKeyEvent(it) - } - - registerHeadsetReceiver() mediaPlayerController.onCreate() if (autoPlay) mediaPlayerController.preload() @@ -68,13 +54,6 @@ class MediaPlayerLifecycleSupport : KoinComponent { false ) - // Work-around: Serialize again, as the restore() method creates a - // serialization without current playing info. - playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, - mediaPlayerController.playerPosition - ) afterCreated?.run() } @@ -87,11 +66,12 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (!created) return - playbackStateSerializer.serializeNow( - downloader.getPlaylist(), - downloader.currentPlayingIndex, - mediaPlayerController.playerPosition - ) + // TODO +// playbackStateSerializer.serializeNow( +// downloader.getPlaylist(), +// downloader.currentPlayingIndex, +// mediaPlayerController.playerPosition +// ) mediaPlayerController.clear(false) mediaButtonEventSubscription?.dispose() @@ -121,73 +101,19 @@ class MediaPlayerLifecycleSupport : KoinComponent { } } - /** - * The Headset Intent Receiver is responsible for resuming playback when a headset is inserted - * and pausing it when it is removed. - * Unfortunately this Intent can't be registered in the AndroidManifest, so it works only - * while Ultrasonic is running. - */ - private fun registerHeadsetReceiver() { - - val sp = Settings.preferences - val context = applicationContext() - val spKey = context - .getString(R.string.settings_playback_resume_play_on_headphones_plug) - - headsetEventReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val extras = intent.extras ?: return - - Timber.i("Headset event for: %s", extras["name"]) - - val state = extras.getInt("state") - - if (state == 0) { - if (!mediaPlayerController.isJukeboxEnabled) { - mediaPlayerController.pause() - } - } else if (state == 1) { - if (!mediaPlayerController.isJukeboxEnabled && - sp.getBoolean( - spKey, - false - ) && mediaPlayerController.playerState === PlayerState.PAUSED - ) { - mediaPlayerController.start() - } - } - } - } - - val headsetIntentFilter = IntentFilter(AudioManager.ACTION_HEADSET_PLUG) - - applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter) - } - - @Suppress("MagicNumber", "ComplexMethod") + @Suppress("MagicNumber") private fun handleKeyEvent(event: KeyEvent) { if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return - val keyCode: Int - val receivedKeyCode = event.keyCode - - // Translate PLAY and PAUSE codes to PLAY_PAUSE to improve compatibility with old Bluetooth devices - keyCode = if (Settings.singleButtonPlayPause && ( - receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY || - receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE - ) - ) { - Timber.i("Single button Play/Pause is set, rewriting keyCode to PLAY_PAUSE") - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - } else receivedKeyCode + val keyCode: Int = event.keyCode val autoStart = keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || - keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || - keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || + keyCode == KeyEvent.KEYCODE_MEDIA_NEXT // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start onCreate(autoStart) { @@ -197,14 +123,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.previous() KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.next() KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() - - KeyEvent.KEYCODE_MEDIA_PLAY -> - if (mediaPlayerController.playerState === PlayerState.IDLE) { - mediaPlayerController.play() - } else if (mediaPlayerController.playerState !== PlayerState.STARTED) { - mediaPlayerController.start() - } - + KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1) KeyEvent.KEYCODE_2 -> mediaPlayerController.setSongRating(2) @@ -221,21 +140,15 @@ class MediaPlayerLifecycleSupport : KoinComponent { /** * This function processes the intent that could come from other applications. */ - @Suppress("ComplexMethod") private fun handleUltrasonicIntent(intentAction: String) { val isRunning = created // If Ultrasonic is not running, do nothing to stop or pause - if ( - !isRunning && ( - intentAction == Constants.CMD_PAUSE || - intentAction == Constants.CMD_STOP - ) - ) return + if (!isRunning && (intentAction == Constants.CMD_PAUSE || intentAction == Constants.CMD_STOP)) + return - val autoStart = - intentAction == Constants.CMD_PLAY || + val autoStart = intentAction == Constants.CMD_PLAY || intentAction == Constants.CMD_RESUME_OR_PLAY || intentAction == Constants.CMD_TOGGLEPAUSE || intentAction == Constants.CMD_PREVIOUS || @@ -253,12 +166,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { Constants.CMD_NEXT -> mediaPlayerController.next() Constants.CMD_PREVIOUS -> mediaPlayerController.previous() Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() - - Constants.CMD_STOP -> { - // TODO: There is a stop() function, shouldn't we use that? - mediaPlayerController.pause() - mediaPlayerController.seekTo(0) - } + Constants.CMD_STOP -> mediaPlayerController.stop() Constants.CMD_PAUSE -> mediaPlayerController.pause() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt deleted file mode 100644 index 55951d8e..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ /dev/null @@ -1,769 +0,0 @@ -/* - * MediaPlayerService.kt - * Copyright (C) 2009-2021 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.service - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Handler -import android.os.IBinder -import android.os.Looper -import android.support.v4.media.session.MediaSessionCompat -import android.view.KeyEvent -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import io.reactivex.rxjava3.disposables.CompositeDisposable -import kotlin.collections.ArrayList -import org.koin.android.ext.android.inject -import org.moire.ultrasonic.R -import org.moire.ultrasonic.activity.NavigationActivity -import org.moire.ultrasonic.app.UApp -import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.domain.RepeatMode -import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.imageloader.BitmapUtils -import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1 -import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2 -import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3 -import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4 -import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.MediaSessionHandler -import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.ShufflePlayBuffer -import org.moire.ultrasonic.util.SimpleServiceBinder -import org.moire.ultrasonic.util.Util -import timber.log.Timber - -/** - * Android Foreground Service for playing music - * while the rest of the Ultrasonic App is in the background. - * - * "A foreground service is a service that the user is - * actively aware of and isn’t a candidate for the system to kill when low on memory." - */ -@Suppress("LargeClass") -class MediaPlayerService : Service() { - private val binder: IBinder = SimpleServiceBinder(this) - private val scrobbler = Scrobbler() - - private val jukeboxMediaPlayer by inject() - private val playbackStateSerializer by inject() - private val shufflePlayBuffer by inject() - private val downloader by inject() - private val localMediaPlayer by inject() - private val mediaSessionHandler by inject() - - private var mediaSession: MediaSessionCompat? = null - private var mediaSessionToken: MediaSessionCompat.Token? = null - private var isInForeground = false - private var notificationBuilder: NotificationCompat.Builder? = null - private var rxBusSubscription: CompositeDisposable = CompositeDisposable() - - private var currentPlayerState: PlayerState? = null - private var currentTrack: DownloadFile? = null - - override fun onBind(intent: Intent): IBinder { - return binder - } - - override fun onCreate() { - super.onCreate() - - shufflePlayBuffer.onCreate() - localMediaPlayer.init() - - setupOnSongCompletedHandler() - - localMediaPlayer.onPrepared = { - playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, - playerPosition - ) - null - } - - localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() } - - // Create Notification Channel - createNotificationChannel() - - // Update notification early. It is better to show an empty one temporarily - // than waiting too long and letting Android kill the app - updateNotification(PlayerState.IDLE, null) - - // Subscribing should be after updateNotification to avoid concurrency - rxBusSubscription += RxBus.playerStateObservable.subscribe { - playerStateChangedHandler(it.state, it.track) - } - - rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe { - mediaSessionToken = it - } - - rxBusSubscription += RxBus.skipToQueueItemCommandObservable.subscribe { - play(it.toInt()) - } - - mediaSessionHandler.initialize() - - instance = this - Timber.i("MediaPlayerService created") - } - - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - return START_NOT_STICKY - } - - override fun onDestroy() { - super.onDestroy() - instance = null - try { - mediaSessionHandler.release() - rxBusSubscription.dispose() - - localMediaPlayer.release() - downloader.stop() - shufflePlayBuffer.onDestroy() - - mediaSession?.release() - mediaSession = null - } catch (ignored: Throwable) { - } - Timber.i("MediaPlayerService stopped") - } - - private fun stopIfIdle() { - synchronized(instanceLock) { - // currentPlaying could be changed from another thread in the meantime, - // so check again before stopping for good - if (localMediaPlayer.currentPlaying == null || - localMediaPlayer.playerState === PlayerState.STOPPED - ) { - stopSelf() - } - } - } - - fun notifyDownloaderStopped() { - // TODO It would be nice to know if the service really can be stopped instead of just - // checking if it is idle once... - val handler = Handler(Looper.getMainLooper()) - handler.postDelayed({ stopIfIdle() }, 1000) - } - - @Synchronized - fun seekTo(position: Int) { - if (jukeboxMediaPlayer.isEnabled) { - // TODO These APIs should be more aligned - val seconds = position / 1000 - jukeboxMediaPlayer.skip(downloader.currentPlayingIndex, seconds) - } else { - localMediaPlayer.seekTo(position) - } - } - - @get:Synchronized - val playerPosition: Int - get() { - if (localMediaPlayer.playerState === PlayerState.IDLE || - localMediaPlayer.playerState === PlayerState.DOWNLOADING || - localMediaPlayer.playerState === PlayerState.PREPARING - ) { - return 0 - } - return if (jukeboxMediaPlayer.isEnabled) { - jukeboxMediaPlayer.positionSeconds * 1000 - } else { - localMediaPlayer.playerPosition - } - } - - @get:Synchronized - val playerDuration: Int - get() = localMediaPlayer.playerDuration - - @Synchronized - fun setCurrentPlaying(currentPlayingIndex: Int) { - try { - localMediaPlayer.setCurrentPlaying(downloader.getPlaylist()[currentPlayingIndex]) - } catch (ignored: IndexOutOfBoundsException) { - } - } - - @Synchronized - fun setNextPlaying() { - // Download the next few songs if necessary - downloader.checkDownloads() - - if (!Settings.gaplessPlayback) { - localMediaPlayer.clearNextPlaying(true) - return - } - - var index = downloader.currentPlayingIndex - - if (index != -1) { - when (Settings.repeatMode) { - RepeatMode.OFF -> index += 1 - RepeatMode.ALL -> index = (index + 1) % downloader.getPlaylist().size - RepeatMode.SINGLE -> { - } - else -> { - } - } - } - - localMediaPlayer.clearNextPlaying(false) - if (index < downloader.getPlaylist().size && index != -1) { - localMediaPlayer.setNextPlaying(downloader.getPlaylist()[index]) - } else { - localMediaPlayer.clearNextPlaying(true) - } - } - - @Synchronized - fun togglePlayPause() { - if (localMediaPlayer.playerState === PlayerState.PAUSED || - localMediaPlayer.playerState === PlayerState.COMPLETED || - localMediaPlayer.playerState === PlayerState.STOPPED - ) { - start() - } else if (localMediaPlayer.playerState === PlayerState.IDLE) { - play() - } else if (localMediaPlayer.playerState === PlayerState.STARTED) { - pause() - } - } - - @Synchronized - fun resumeOrPlay() { - if (localMediaPlayer.playerState === PlayerState.PAUSED || - localMediaPlayer.playerState === PlayerState.COMPLETED || - localMediaPlayer.playerState === PlayerState.STOPPED - ) { - start() - } else if (localMediaPlayer.playerState === PlayerState.IDLE) { - play() - } - } - - /** - * Plays either the current song (resume) or the first/next one in queue. - */ - @Synchronized - fun play() { - val current = downloader.currentPlayingIndex - if (current == -1) { - play(0) - } else { - play(current) - } - } - - @Synchronized - fun play(index: Int) { - play(index, true) - } - - @Synchronized - fun play(index: Int, start: Boolean) { - Timber.v("play requested for %d", index) - if (index < 0 || index >= downloader.getPlaylist().size) { - resetPlayback() - } else { - setCurrentPlaying(index) - if (start) { - if (jukeboxMediaPlayer.isEnabled) { - jukeboxMediaPlayer.skip(index, 0) - } else { - localMediaPlayer.play(downloader.getPlaylist()[index]) - } - } - setNextPlaying() - } - } - - @Synchronized - private fun resetPlayback() { - localMediaPlayer.reset() - localMediaPlayer.setCurrentPlaying(null) - playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, playerPosition - ) - } - - @Synchronized - fun pause() { - if (localMediaPlayer.playerState === PlayerState.STARTED) { - if (jukeboxMediaPlayer.isEnabled) { - jukeboxMediaPlayer.stop() - } else { - localMediaPlayer.pause() - } - localMediaPlayer.setPlayerState(PlayerState.PAUSED, localMediaPlayer.currentPlaying) - } - } - - @Synchronized - fun stop() { - if (localMediaPlayer.playerState === PlayerState.STARTED) { - if (jukeboxMediaPlayer.isEnabled) { - jukeboxMediaPlayer.stop() - } else { - localMediaPlayer.pause() - } - } - localMediaPlayer.setPlayerState(PlayerState.STOPPED, null) - } - - @Synchronized - fun start() { - if (jukeboxMediaPlayer.isEnabled) { - jukeboxMediaPlayer.start() - } else { - localMediaPlayer.start() - } - localMediaPlayer.setPlayerState(PlayerState.STARTED, localMediaPlayer.currentPlaying) - } - - private fun updateWidget(playerState: PlayerState, song: Track?) { - val started = playerState === PlayerState.STARTED - val context = this@MediaPlayerService - - UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(context, song, started, false) - UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(context, song, started, true) - UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(context, song, started, false) - UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false) - } - - private fun playerStateChangedHandler( - playerState: PlayerState, - currentPlaying: DownloadFile? - ) { - val context = this@MediaPlayerService - // AVRCP handles these separately so we must differentiate between the cases - val isStateChanged = playerState != currentPlayerState - val isTrackChanged = currentPlaying != currentTrack - if (!isStateChanged && !isTrackChanged) return - - val showWhenPaused = playerState !== PlayerState.STOPPED && - Settings.isNotificationAlwaysEnabled - - val show = playerState === PlayerState.STARTED || showWhenPaused - val song = currentPlaying?.track - - if (isStateChanged) { - when { - playerState === PlayerState.PAUSED -> { - playbackStateSerializer.serialize( - downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition - ) - } - playerState === PlayerState.STARTED -> { - scrobbler.scrobble(currentPlaying, false) - } - playerState === PlayerState.COMPLETED -> { - scrobbler.scrobble(currentPlaying, true) - } - } - - Util.broadcastPlaybackStatusChange(context, playerState) - Util.broadcastA2dpPlayStatusChange( - context, playerState, song, - downloader.getPlaylist().size, - downloader.getPlaylist().indexOf(currentPlaying) + 1, playerPosition - ) - } else { - // State didn't change, only the track - Util.broadcastA2dpMetaDataChange( - this@MediaPlayerService, playerPosition, currentPlaying, - downloader.all.size, downloader.currentPlayingIndex + 1 - ) - } - - if (isTrackChanged) { - Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying?.track) - } - - // Update widget - updateWidget(playerState, song) - - if (show) { - // Only update notification if player state is one that will change the icon - if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) { - updateNotification(playerState, currentPlaying) - } - } else { - stopForeground(true) - isInForeground = false - stopIfIdle() - } - - currentPlayerState = playerState - currentTrack = currentPlaying - - Timber.d("Processed player state change") - } - - private fun setupOnSongCompletedHandler() { - localMediaPlayer.onSongCompleted = { currentPlaying: DownloadFile? -> - val index = downloader.currentPlayingIndex - - if (currentPlaying != null) { - val song = currentPlaying.track - if (song.bookmarkPosition > 0 && Settings.shouldClearBookmark) { - val musicService = getMusicService() - try { - musicService.deleteBookmark(song.id) - } catch (ignored: Exception) { - } - } - } - if (index != -1) { - when (Settings.repeatMode) { - RepeatMode.OFF -> { - if (index + 1 < 0 || index + 1 >= downloader.getPlaylist().size) { - if (Settings.shouldClearPlaylist) { - clear(true) - jukeboxMediaPlayer.updatePlaylist() - } - resetPlayback() - } else { - play(index + 1) - } - } - RepeatMode.ALL -> { - play((index + 1) % downloader.getPlaylist().size) - } - RepeatMode.SINGLE -> play(index) - else -> { - } - } - } - null - } - } - - @Synchronized - fun clear(serialize: Boolean) { - localMediaPlayer.reset() - downloader.clearPlaylist() - localMediaPlayer.setCurrentPlaying(null) - setNextPlaying() - if (serialize) { - playbackStateSerializer.serialize( - downloader.getPlaylist(), - downloader.currentPlayingIndex, playerPosition - ) - } - } - - private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - - // The suggested importance of a startForeground service notification is IMPORTANCE_LOW - val channel = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_NAME, - NotificationManager.IMPORTANCE_LOW - ) - - channel.lightColor = android.R.color.holo_blue_dark - channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC - channel.setShowBadge(false) - - val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - manager.createNotificationChannel(channel) - } - } - - fun updateNotification(playerState: PlayerState, currentPlaying: DownloadFile?) { - val notification = buildForegroundNotification(playerState, currentPlaying) - - if (Settings.isNotificationEnabled) { - if (isInForeground) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - manager.notify(NOTIFICATION_ID, notification) - } else { - val manager = NotificationManagerCompat.from(this) - manager.notify(NOTIFICATION_ID, notification) - } - Timber.v("Updated notification") - } else { - startForeground(NOTIFICATION_ID, notification) - isInForeground = true - Timber.v("Created Foreground notification") - } - } - } - - /** - * This method builds a notification, reusing the Notification Builder if possible - */ - @Suppress("SpreadOperator") - private fun buildForegroundNotification( - playerState: PlayerState, - currentPlaying: DownloadFile? - ): Notification { - - // Init - val context = applicationContext - val song = currentPlaying?.track - val stopIntent = Util.getPendingIntentForMediaAction( - context, - KeyEvent.KEYCODE_MEDIA_STOP, - 100 - ) - - // We should use a single notification builder, otherwise the notification may not be updated - if (notificationBuilder == null) { - notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - - // Set some values that never change - notificationBuilder!!.setSmallIcon(R.drawable.ic_stat_ultrasonic) - notificationBuilder!!.setAutoCancel(false) - notificationBuilder!!.setOngoing(true) - notificationBuilder!!.setOnlyAlertOnce(true) - notificationBuilder!!.setWhen(System.currentTimeMillis()) - notificationBuilder!!.setShowWhen(false) - notificationBuilder!!.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - notificationBuilder!!.priority = NotificationCompat.PRIORITY_LOW - - // Add content intent (when user taps on notification) - notificationBuilder!!.setContentIntent(getPendingIntentForContent()) - - // This intent is executed when the user closes the notification - notificationBuilder!!.setDeleteIntent(stopIntent) - } - - // Use the Media Style, to enable native Android support for playback notification - val style = androidx.media.app.NotificationCompat.MediaStyle() - - if (mediaSessionToken != null) { - style.setMediaSession(mediaSessionToken) - } - - // Clear old actions - notificationBuilder!!.clearActions() - - if (song != null) { - // Add actions - val compactActions = addActions(context, notificationBuilder!!, playerState, song) - // Configure shortcut actions - style.setShowActionsInCompactView(*compactActions) - notificationBuilder!!.setStyle(style) - - // Set song title, artist and cover - val iconSize = (256 * context.resources.displayMetrics.density).toInt() - val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(song, iconSize) - notificationBuilder!!.setContentTitle(song.title) - notificationBuilder!!.setContentText(song.artist) - notificationBuilder!!.setLargeIcon(bitmap) - notificationBuilder!!.setSubText(song.album) - } else if (downloader.started) { - // No song is playing, but Ultrasonic is downloading files - notificationBuilder!!.setContentTitle( - getString(R.string.notification_downloading_title) - ) - } - - return notificationBuilder!!.build() - } - - private fun addActions( - context: Context, - notificationBuilder: NotificationCompat.Builder, - playerState: PlayerState, - song: Track? - ): IntArray { - // Init - val compactActionList = ArrayList() - var numActions = 0 // we start and 0 and then increment by 1 for each call to generateAction - - // Star - if (song != null) { - notificationBuilder.addAction(generateStarAction(context, numActions, song.starred)) - } - numActions++ - - // Next - notificationBuilder.addAction(generateAction(context, numActions)) - compactActionList.add(numActions) - numActions++ - - // Play/Pause button - notificationBuilder.addAction(generatePlayPauseAction(context, numActions, playerState)) - compactActionList.add(numActions) - numActions++ - - // Previous - notificationBuilder.addAction(generateAction(context, numActions)) - compactActionList.add(numActions) - numActions++ - - // Close - notificationBuilder.addAction(generateAction(context, numActions)) - val actionArray = IntArray(compactActionList.size) - for (i in actionArray.indices) { - actionArray[i] = compactActionList[i] - } - return actionArray - // notificationBuilder.setShowActionsInCompactView()) - } - - private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? { - val keycode: Int - val icon: Int - val label: String - - when (requestCode) { - 1 -> { - keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS - label = getString(R.string.common_play_previous) - icon = R.drawable.media_backward_medium_dark - } - 2 -> // Is handled in generatePlayPauseAction() - return null - 3 -> { - keycode = KeyEvent.KEYCODE_MEDIA_NEXT - label = getString(R.string.common_play_next) - icon = R.drawable.media_forward_medium_dark - } - 4 -> { - keycode = KeyEvent.KEYCODE_MEDIA_STOP - label = getString(R.string.buttons_stop) - icon = R.drawable.ic_baseline_close - } - else -> return null - } - - val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode) - return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() - } - - private fun generatePlayPauseAction( - context: Context, - requestCode: Int, - playerState: PlayerState - ): NotificationCompat.Action { - val isPlaying = playerState === PlayerState.STARTED - val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode) - val label: String - val icon: Int - - if (isPlaying) { - label = getString(R.string.common_pause) - icon = R.drawable.media_pause_large_dark - } else { - label = getString(R.string.common_play) - icon = R.drawable.media_start_large_dark - } - - return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() - } - - private fun generateStarAction( - context: Context, - requestCode: Int, - isStarred: Boolean - ): NotificationCompat.Action { - - val label: String - val icon: Int - val keyCode: Int = KeyEvent.KEYCODE_STAR - - if (isStarred) { - label = getString(R.string.download_menu_star) - icon = R.drawable.ic_star_full_dark - } else { - label = getString(R.string.download_menu_star) - icon = R.drawable.ic_star_hollow_dark - } - - val pendingIntent = Util.getPendingIntentForMediaAction(context, keyCode, requestCode) - return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() - } - - private fun getPendingIntentForContent(): PendingIntent { - val intent = Intent(this, NavigationActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - val flags = PendingIntent.FLAG_UPDATE_CURRENT - intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) - return PendingIntent.getActivity(this, 0, intent, flags) - } - - @Suppress("MagicNumber") - companion object { - private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic" - private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service" - private const val NOTIFICATION_ID = 3033 - - @Volatile - private var instance: MediaPlayerService? = null - private val instanceLock = Any() - - @JvmStatic - fun getInstance(): MediaPlayerService? { - val context = UApp.applicationContext() - // Try for twenty times to retrieve a running service, - // sleep 100 millis between each try, - // and run the block that creates a service only synchronized. - for (i in 0..19) { - if (instance != null) return instance - synchronized(instanceLock) { - if (instance != null) return instance - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService( - Intent(context, MediaPlayerService::class.java) - ) - } else { - context.startService(Intent(context, MediaPlayerService::class.java)) - } - } - Util.sleepQuietly(100L) - } - return instance - } - - @JvmStatic - val runningInstance: MediaPlayerService? - get() { - synchronized(instanceLock) { return instance } - } - - @JvmStatic - fun executeOnStartedMediaPlayerService( - taskToExecute: (MediaPlayerService) -> Unit - ) { - - val t: Thread = object : Thread() { - override fun run() { - val instance = getInstance() - if (instance == null) { - Timber.e("ExecuteOnStarted.. failed to get a MediaPlayerService instance!") - return - } else { - taskToExecute(instance) - } - } - } - t.start() - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index 7115140a..edb1cea5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -53,7 +53,7 @@ class PlaybackStateSerializer : KoinComponent { } } - fun serializeNow( + private fun serializeNow( songs: Iterable, currentPlayingIndex: Int, currentPlayingPosition: Int diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index f5686372..469b50e9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -20,11 +20,6 @@ class RxBus { .replay(1) .autoConnect(0) - val mediaButtonEventPublisher: PublishSubject = - PublishSubject.create() - val mediaButtonEventObservable: Observable = - mediaButtonEventPublisher.observeOn(AndroidSchedulers.mainThread()) - val themeChangedEventPublisher: PublishSubject = PublishSubject.create() val themeChangedEventObservable: Observable = @@ -83,7 +78,7 @@ class RxBus { } } - data class StateWithTrack(val state: PlayerState, val track: DownloadFile?) + data class StateWithTrack(val state: PlayerState, val track: DownloadFile?, val index: Int = -1) } operator fun CompositeDisposable.plusAssign(disposable: Disposable) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 3dca314c..e02b134c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -34,20 +34,23 @@ class DownloadHandler( autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, - songs: List + songs: List, ) { val onValid = Runnable { - if (!append && !playNext) { - mediaPlayerController.clear() + // TODO: The logic here is different than in the controller... + val insertionMode = when { + append -> MediaPlayerController.InsertionMode.APPEND + playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT + else -> MediaPlayerController.InsertionMode.CLEAR } + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() mediaPlayerController.addToPlaylist( songs, save, autoPlay, - playNext, shuffle, - false + insertionMode ) val playlistName: String? = fragment.arguments?.getString( Constants.INTENT_PLAYLIST_NAME @@ -281,26 +284,28 @@ class DownloadHandler( } } + // Called when we have collected the tracks override fun done(songs: List) { if (Settings.shouldSortByDisc) { Collections.sort(songs, EntryByDiscAndTrackComparator()) } if (songs.isNotEmpty()) { - if (!append && !playNext && !unpin && !background) { - mediaPlayerController.clear() - } networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() if (!background) { if (unpin) { mediaPlayerController.unpin(songs) } else { + val insertionMode = when { + append -> MediaPlayerController.InsertionMode.APPEND + playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT + else -> MediaPlayerController.InsertionMode.CLEAR + } mediaPlayerController.addToPlaylist( songs, save, autoPlay, - playNext, shuffle, - false + insertionMode ) if ( !append && diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index 9990ca7c..e585ea66 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -233,7 +233,7 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) { ) { if (file.isFile && (isPartial(file) || isComplete(file))) { files.add(file) - } else { + } else if (file.isDirectory) { // Depth-first for (child in listFiles(file)) { findCandidatesForDeletion(child, files, dirs) @@ -257,7 +257,7 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) { for (downloadFile in downloader.value.all) { filesToNotDelete.add(downloadFile.partialFile) filesToNotDelete.add(downloadFile.completeFile) - filesToNotDelete.add(downloadFile.saveFile) + filesToNotDelete.add(downloadFile.pinnedFile) } filesToNotDelete.add(musicDirectory.path) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index 8511d9c1..8e7fe064 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -406,7 +406,7 @@ object FileUtil { return path.substringBeforeLast('/') } - fun getSaveFile(name: String): String { + fun getPinnedFile(name: String): String { val baseName = getBaseName(name) if (baseName.endsWith(".partial") || baseName.endsWith(".complete")) { return "${getBaseName(baseName)}.${getExtension(name)}" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt deleted file mode 100644 index ae3def28..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt +++ /dev/null @@ -1,332 +0,0 @@ -/* - * MediaSessionHandler.kt - * Copyright (C) 2009-2021 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.util - -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat -import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN -import android.view.KeyEvent -import io.reactivex.rxjava3.disposables.CompositeDisposable -import kotlin.Pair -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.imageloader.BitmapUtils -import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver -import org.moire.ultrasonic.service.DownloadFile -import org.moire.ultrasonic.service.RxBus -import org.moire.ultrasonic.service.plusAssign -import org.moire.ultrasonic.util.Util.ifNotNull -import timber.log.Timber - -private const val INTENT_CODE_MEDIA_BUTTON = 161 -/** - * Central place to handle the state of the MediaSession - */ -class MediaSessionHandler : KoinComponent { - - private var mediaSession: MediaSessionCompat? = null - private var playbackState: Int? = null - private var playbackActions: Long? = null - private var cachedPlayingIndex: Long? = null - - private val applicationContext by inject() - - private var referenceCount: Int = 0 - private var cachedPlaylist: List? = null - private var cachedPosition: Long = 0 - - private val rxBusSubscription: CompositeDisposable = CompositeDisposable() - - fun release() { - - if (referenceCount > 0) referenceCount-- - if (referenceCount > 0) return - - mediaSession?.isActive = false - RxBus.releaseMediaSessionToken() - rxBusSubscription.dispose() - mediaSession?.release() - mediaSession = null - - Timber.i("MediaSessionHandler.release Media Session released") - } - - fun initialize() { - - referenceCount++ - if (referenceCount > 1) return - - @Suppress("MagicNumber") - val keycode = 110 - - Timber.d("MediaSessionHandler.initialize Creating Media Session") - - mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService") - val mediaSessionToken = mediaSession?.sessionToken ?: return - RxBus.mediaSessionTokenPublisher.onNext(mediaSessionToken) - - updateMediaButtonReceiver() - - mediaSession?.setCallback(object : MediaSessionCompat.Callback() { - override fun onPlay() { - super.onPlay() - - Util.getPendingIntentForMediaAction( - applicationContext, - KeyEvent.KEYCODE_MEDIA_PLAY, - keycode - ).send() - - Timber.v("Media Session Callback: onPlay") - } - - override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { - super.onPlayFromMediaId(mediaId, extras) - - Timber.d("Media Session Callback: onPlayFromMediaId %s", mediaId) - RxBus.playFromMediaIdCommandPublisher.onNext(Pair(mediaId, extras)) - } - - override fun onPlayFromSearch(query: String?, extras: Bundle?) { - super.onPlayFromSearch(query, extras) - - Timber.d("Media Session Callback: onPlayFromSearch %s", query) - RxBus.playFromSearchCommandPublisher.onNext(Pair(query, extras)) - } - - override fun onPause() { - super.onPause() - Util.getPendingIntentForMediaAction( - applicationContext, - KeyEvent.KEYCODE_MEDIA_PAUSE, - keycode - ).send() - Timber.v("Media Session Callback: onPause") - } - - override fun onStop() { - super.onStop() - Util.getPendingIntentForMediaAction( - applicationContext, - KeyEvent.KEYCODE_MEDIA_STOP, - keycode - ).send() - Timber.v("Media Session Callback: onStop") - } - - override fun onSkipToNext() { - super.onSkipToNext() - Util.getPendingIntentForMediaAction( - applicationContext, - KeyEvent.KEYCODE_MEDIA_NEXT, - keycode - ).send() - Timber.v("Media Session Callback: onSkipToNext") - } - - override fun onSkipToPrevious() { - super.onSkipToPrevious() - Util.getPendingIntentForMediaAction( - applicationContext, - KeyEvent.KEYCODE_MEDIA_PREVIOUS, - keycode - ).send() - Timber.v("Media Session Callback: onSkipToPrevious") - } - - override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { - // This probably won't be necessary once we implement more - // of the modern media APIs, like the MediaController etc. - val event = mediaButtonEvent.extras!!["android.intent.extra.KEY_EVENT"] as KeyEvent? - event.ifNotNull { RxBus.mediaButtonEventPublisher.onNext(it) } - return true - } - - override fun onSkipToQueueItem(id: Long) { - super.onSkipToQueueItem(id) - RxBus.skipToQueueItemCommandPublisher.onNext(id) - } - } - ) - - // It seems to be the best practice to set this to true for the lifetime of the session - mediaSession?.isActive = true - rxBusSubscription += RxBus.playbackPositionObservable.subscribe { - updateMediaSessionPlaybackPosition(it) - } - rxBusSubscription += RxBus.playlistObservable.subscribe { - updateMediaSessionQueue(it) - } - rxBusSubscription += RxBus.playerStateObservable.subscribe { - updateMediaSession(it.state, it.track) - } - - Timber.i("MediaSessionHandler.initialize Media Session created") - } - - @Suppress("LongMethod", "ComplexMethod") - private fun updateMediaSession( - playerState: PlayerState, - currentPlaying: DownloadFile? - ) { - Timber.d("Updating the MediaSession") - - // Set Metadata - val metadata = MediaMetadataCompat.Builder() - if (currentPlaying != null) { - try { - val song = currentPlaying.track - val cover = BitmapUtils.getAlbumArtBitmapFromDisk( - song, Util.getMinDisplayMetric() - ) - val duration = song.duration?.times(1000) ?: -1 - metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration.toLong()) - metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist) - metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.artist) - metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album) - metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title) - metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover) - } catch (all: Exception) { - Timber.e(all, "Error setting the metadata") - } - } - - // Save the metadata - mediaSession?.setMetadata(metadata.build()) - - playbackActions = PlaybackStateCompat.ACTION_PLAY_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or - PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or - PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or - PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM - - // Map our playerState to native PlaybackState - // TODO: Synchronize these APIs - when (playerState) { - PlayerState.STARTED -> { - playbackState = PlaybackStateCompat.STATE_PLAYING - playbackActions = playbackActions!! or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_STOP - } - PlayerState.COMPLETED, - PlayerState.STOPPED -> { - playbackState = PlaybackStateCompat.STATE_STOPPED - cachedPosition = PLAYBACK_POSITION_UNKNOWN - } - PlayerState.IDLE -> { - // IDLE state usually just means the playback is stopped - // STATE_NONE means that there is no track to play (playlist is empty) - playbackState = if (currentPlaying == null) - PlaybackStateCompat.STATE_NONE - else - PlaybackStateCompat.STATE_STOPPED - playbackActions = 0L - cachedPosition = PLAYBACK_POSITION_UNKNOWN - } - PlayerState.PAUSED -> { - playbackState = PlaybackStateCompat.STATE_PAUSED - playbackActions = playbackActions!! or - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_STOP - } - else -> { - // These are the states PREPARING, PREPARED & DOWNLOADING - playbackState = PlaybackStateCompat.STATE_PAUSED - } - } - - val playbackStateBuilder = PlaybackStateCompat.Builder() - playbackStateBuilder.setState(playbackState!!, cachedPosition, 1.0f) - - // Set actions - playbackStateBuilder.setActions(playbackActions!!) - - val index = cachedPlaylist?.indexOf(currentPlaying) - cachedPlayingIndex = if (index == null || index < 0) null else index.toLong() - cachedPlaylist.ifNotNull { setMediaSessionQueue(it) } - - if (cachedPlaylist != null && !Settings.shouldDisableNowPlayingListSending) - cachedPlayingIndex.ifNotNull { playbackStateBuilder.setActiveQueueItemId(it) } - - // Save the playback state - mediaSession?.setPlaybackState(playbackStateBuilder.build()) - } - - private fun updateMediaSessionQueue(playlist: List) { - cachedPlaylist = playlist - setMediaSessionQueue(playlist) - } - - private fun setMediaSessionQueue(playlist: List) { - if (mediaSession == null) return - if (Settings.shouldDisableNowPlayingListSending) return - - val queue = playlist.mapIndexed { id, file -> - MediaSessionCompat.QueueItem( - Util.getMediaDescriptionForEntry(file.track), - id.toLong() - ) - } - mediaSession?.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing)) - mediaSession?.setQueue(queue) - } - - private fun updateMediaSessionPlaybackPosition(playbackPosition: Int) { - cachedPosition = playbackPosition.toLong() - if (playbackState == null || playbackActions == null) return - - val playbackStateBuilder = PlaybackStateCompat.Builder() - playbackStateBuilder.setState(playbackState!!, cachedPosition, 1.0f) - playbackStateBuilder.setActions(playbackActions!!) - - if (cachedPlaylist != null && !Settings.shouldDisableNowPlayingListSending) - cachedPlayingIndex.ifNotNull { playbackStateBuilder.setActiveQueueItemId(it) } - - mediaSession?.setPlaybackState(playbackStateBuilder.build()) - } - - fun updateMediaButtonReceiver() { - if (Settings.mediaButtonsEnabled) { - registerMediaButtonEventReceiver() - } else { - unregisterMediaButtonEventReceiver() - } - } - - private fun registerMediaButtonEventReceiver() { - val component = ComponentName( - applicationContext.packageName, - MediaButtonIntentReceiver::class.java.name - ) - val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) - mediaButtonIntent.component = component - - val pendingIntent = PendingIntent.getBroadcast( - applicationContext, - INTENT_CODE_MEDIA_BUTTON, - mediaButtonIntent, - PendingIntent.FLAG_CANCEL_CURRENT - ) - - mediaSession?.setMediaButtonReceiver(pendingIntent) - } - - private fun unregisterMediaButtonEventReceiver() { - mediaSession?.setMediaButtonReceiver(null) - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index c286bd69..2b29f506 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -9,13 +9,11 @@ package org.moire.ultrasonic.util import android.content.Context import android.content.SharedPreferences -import android.os.Build import androidx.preference.PreferenceManager import java.util.regex.Pattern import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider -import org.moire.ultrasonic.domain.RepeatMode /** * Contains convenience functions for reading and writing preferences @@ -23,43 +21,6 @@ import org.moire.ultrasonic.domain.RepeatMode object Settings { private val PATTERN = Pattern.compile(":") - var repeatMode: RepeatMode - get() { - val preferences = preferences - return RepeatMode.valueOf( - preferences.getString( - Constants.PREFERENCES_KEY_REPEAT_MODE, - RepeatMode.OFF.name - )!! - ) - } - set(repeatMode) { - val preferences = preferences - val editor = preferences.edit() - editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name) - editor.apply() - } - - // After API26 foreground services must be used for music playback, - // and they must have a notification - val isNotificationEnabled: Boolean - get() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) return true - val preferences = preferences - return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION, false) - } - - // After API26 foreground services must be used for music playback, - // and they must have a notification - val isNotificationAlwaysEnabled: Boolean - get() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) return true - val preferences = preferences - return preferences.getBoolean(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION, false) - } - - var isLockScreenEnabled by BooleanSetting(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS) - @JvmStatic var theme by StringSetting( Constants.PREFERENCES_KEY_THEME, diff --git a/ultrasonic/src/main/res/drawable/media3_notification_pause.xml b/ultrasonic/src/main/res/drawable/media3_notification_pause.xml new file mode 100644 index 00000000..64164940 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media3_notification_pause.xml @@ -0,0 +1,5 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media3_notification_play.xml b/ultrasonic/src/main/res/drawable/media3_notification_play.xml new file mode 100644 index 00000000..0ebbb01e --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media3_notification_play.xml @@ -0,0 +1,5 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media3_notification_seek_to_next.xml b/ultrasonic/src/main/res/drawable/media3_notification_seek_to_next.xml new file mode 100644 index 00000000..dc96d2dc --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media3_notification_seek_to_next.xml @@ -0,0 +1,5 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media3_notification_seek_to_previous.xml b/ultrasonic/src/main/res/drawable/media3_notification_seek_to_previous.xml new file mode 100644 index 00000000..79c6bfd3 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media3_notification_seek_to_previous.xml @@ -0,0 +1,5 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml b/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml new file mode 100644 index 00000000..e2943794 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml @@ -0,0 +1,9 @@ + + + From 1703f02aad2e7fe34dbc528e986cf00e21c92f92 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Apr 2022 00:01:26 +0200 Subject: [PATCH 03/67] Remove unused file --- .../ultrasonic/playback/UltrasonicCache.kt | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt deleted file mode 100644 index f3b59499..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/UltrasonicCache.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * UltrasonicCache.kt - * Copyright (C) 2009-2022 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.playback - -import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.cache.Cache -import androidx.media3.datasource.cache.CacheSpan -import androidx.media3.datasource.cache.ContentMetadata -import androidx.media3.datasource.cache.ContentMetadataMutations -import java.io.File -import java.util.NavigableSet - -@UnstableApi - -class UltrasonicCache : Cache { - override fun getUid(): Long { - TODO("Not yet implemented") - } - - override fun release() { - // R/O Cache Implementation - } - - override fun addListener(key: String, listener: Cache.Listener): NavigableSet { - // Not (yet?) implemented - return emptySet() as NavigableSet - } - - override fun removeListener(key: String, listener: Cache.Listener) { - // Not (yet?) implemented - } - - override fun getCachedSpans(key: String): NavigableSet { - TODO("Not yet implemented") - } - - override fun getKeys(): MutableSet { - TODO("Not yet implemented") - } - - override fun getCacheSpace(): Long { - TODO("Not yet implemented") - } - - override fun startReadWrite(key: String, position: Long, length: Long): CacheSpan { - TODO("Not yet implemented") - } - - override fun startReadWriteNonBlocking(key: String, position: Long, length: Long): CacheSpan? { - TODO("Not yet implemented") - } - - override fun startFile(key: String, position: Long, length: Long): File { - // R/O Cache Implementation - return File("NONE") - } - - override fun commitFile(file: File, length: Long) { - // R/O Cache Implementation - } - - override fun releaseHoleSpan(holeSpan: CacheSpan) { - TODO("Not yet implemented") - } - - override fun removeResource(key: String) { - // R/O Cache Implementation - } - - override fun removeSpan(span: CacheSpan) { - // R/O Cache Implementation - } - - override fun isCached(key: String, position: Long, length: Long): Boolean { - TODO("Not yet implemented") - } - - override fun getCachedLength(key: String, position: Long, length: Long): Long { - TODO("Not yet implemented") - } - - override fun getCachedBytes(key: String, position: Long, length: Long): Long { - TODO("Not yet implemented") - } - - override fun applyContentMetadataMutations(key: String, mutations: ContentMetadataMutations) { - TODO("Not yet implemented") - } - - override fun getContentMetadata(key: String): ContentMetadata { - TODO("Not yet implemented") - } - -} From 5966dd729964101148a3858a7d032728bbc99fdc Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Apr 2022 17:43:02 +0200 Subject: [PATCH 04/67] Remove gapless setting --- .../src/main/kotlin/org/moire/ultrasonic/util/Constants.kt | 1 - .../src/main/kotlin/org/moire/ultrasonic/util/Settings.kt | 4 ---- ultrasonic/src/main/res/values-cs/strings.xml | 2 -- ultrasonic/src/main/res/values-de/strings.xml | 2 -- ultrasonic/src/main/res/values-es/strings.xml | 2 -- ultrasonic/src/main/res/values-fr/strings.xml | 2 -- ultrasonic/src/main/res/values-hu/strings.xml | 2 -- ultrasonic/src/main/res/values-it/strings.xml | 2 -- ultrasonic/src/main/res/values-nl/strings.xml | 2 -- ultrasonic/src/main/res/values-pl/strings.xml | 2 -- ultrasonic/src/main/res/values-pt-rBR/strings.xml | 2 -- ultrasonic/src/main/res/values-pt/strings.xml | 2 -- ultrasonic/src/main/res/values-ru/strings.xml | 2 -- ultrasonic/src/main/res/values-zh-rCN/strings.xml | 2 -- ultrasonic/src/main/res/values/strings.xml | 2 -- ultrasonic/src/main/res/xml/settings.xml | 6 ------ 16 files changed, 37 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index 0c9538f5..0f22d0c4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -85,7 +85,6 @@ object Constants { const val PREFERENCES_KEY_DEFAULT_SONGS = "defaultSongs" const val PREFERENCES_KEY_DEFAULT_ARTISTS = "defaultArtists" const val PREFERENCES_KEY_SHOW_NOW_PLAYING = "showNowPlaying" - const val PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback" const val PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS = "playbackControlSettings" const val PREFERENCES_KEY_CLEAR_SEARCH_HISTORY = "clearSearchHistory" const val PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 2b29f506..e5a0c4d5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -140,10 +140,6 @@ object Settings { var showNowPlaying by BooleanSetting(Constants.PREFERENCES_KEY_SHOW_NOW_PLAYING, true) - @JvmStatic - var gaplessPlayback - by BooleanSetting(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, false) - @JvmStatic var shouldTransitionOnPlayback by BooleanSetting( Constants.PREFERENCES_KEY_DOWNLOAD_TRANSITION, diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index b2eb3819..197a16ad 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -195,8 +195,6 @@ Řadit seznam skladeb dle čísla CD a čísla skladby Připojovat jméno umělce, bitrate a příponu souboru Při spuštění přehrávání přepnout na aktivitu stahování - Přehrávání bez pauz - Zapnout přehrávání bez pauz Skrýt hudební soubory před ostatními aplikacemi. Skrýt před ostatními Nabyde účinnosti při příštím skenování hudby systému Android. diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index 3737b57c..7a597eb3 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -210,8 +210,6 @@ Bitrate und Dateiendung anzeigen Bitrate und Dateityp hinter der Künstler*in anzeigen Herunterladen zusammen mit der Wiedergabe starten. - Lückenlose Wiedergabe - Lückenlose Wiedergabe aktivieren Musikdateien vor anderen Apps verbergen Vor anderen verbergen Wird beim nächsten Durchsuchen nach Musik durch Android wirksam. diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index f53ea705..80b620cc 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -217,8 +217,6 @@ Añadir el nombre del artista con la tasa de bits y la extensión del archivo Mostrar descargas en reproducción Mostrar la actividad de descarga cuando comienza la reproducción - Reproducción sin pausas - Activa la reproducción sin pausas Oculta los archivos de música desde otras aplicaciones. Ocultar desde otras Tiene efecto la próxima vez que Android escanee la música de tu dispositivo. diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 34b0833e..5bf0ddb3 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -212,8 +212,6 @@ Ajouter le nom d\'artiste, bitrate et suffixe du fichier Afficher le téléchargement lors de la lecture Aller vers les téléchargements lorsque qu\'un titre est écouté - Lecture sans interruption - Activer la lecture sans interruption Masquer les fichiers musicaux et les couvertures d\'album aux autres applis (Galerie, Musique, etc.) Masquer aux autres Prendra effet la prochaine fois qu\'Android recensera les médias disponibles sur l\'appareil. diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 888ba13c..a8e758b3 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -204,8 +204,6 @@ Dalok rendezése albumsorszám és dalsorszám szerint. Bitráta és fájlkiterjesztés megjelenítése az előadónév mellett. Letöltési aktivitás megjelenítése a lejátszás indításakor. - Egybefüggő lejátszás - Kihagyás (dalszünet) nélküli egybefüggő lejátszás (Gapless). Zenefájlok elrejtése egyéb alkalmazások elől. Elrejtés A következő alkalomtól lép életbe, amikor az Android zenefájlokat keres a telefonon. diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 54122b09..fe7c0a1d 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -191,8 +191,6 @@ Ordina lista canzoni secondo il numero disco e traccia Aggiungi nome artista con bitrare ed estensione file Passa al download quando inizia riproduzione - Riproduzione Ininterrotta - Abilita riproduzione ininterrotta Nascondi file musicali di altre app Nascondi Da Altro Effettivo alla prossima scansione Android per file musicali sul telefono. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 6d65e679..e358892d 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -217,8 +217,6 @@ Bitsnelheid en bestandsextensie toevoegen aan artiestennaam Downloads tonen bij afspelen Overschakelen naar downloadactiviteit na starten van afspelen - Naadloze overgang - Naadloze overgang tussen nummers inschakelen Muziekbestanden verbergen voor andere apps. Verbergen voor andere apps Dit wordt toegepast bij de volgende keer dat Android je muziek doorzoekt. diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 9438ffff..285241f4 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -195,8 +195,6 @@ Sortuje listę utworów wg numeru dysku i numeru utworu Dołącza bitrate i typ pliku do nazwy artysty Wyświetla postęp pobierania podczas odtwarzania - Odtwarzanie bez przerw - Włącz odtwarzanie bez przerw między utworami Ukrywa pliki muzyczne przed innymi aplikacjami. Ukryj pliki Efekt widoczny będzie po następnym skanowaniu muzyki przez system Android diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index c23a305a..0aaf5b8b 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -212,8 +212,6 @@ Adicionar o nome do artista com a taxa de bits e sufixo do arquivo Mostrar Downloads na Reprodução Transição para atividade de download quando iniciar reprodução - Reprodução sem Interrupção - Ativar reprodução sem interrupção Esconder arquivos de músicas de outros aplicativos Esconder de Outros Será efetivado na próxima vez que o Android procurar por músicas em seu celular. diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index c8da511d..681f7630 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -195,8 +195,6 @@ Classificar músicas pelo número do álbum e faixas. Adiciona o nome do artista com a taxa de bits e sufixo do ficheiro Transição para atividade de download quando iniciar reprodução - Reprodução sem Interrupção - Habilita reprodução sem interrupção Esconder músicas de outros aplicativos. Esconder de Outros Será realizado na próxima vez que o Android procurar por músicas em seu telemóvel. diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 7935cc57..51a3a2bf 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -206,8 +206,6 @@ Сортировать список песен по номеру диска и треку Добавить имя исполнителя с битрейтом и суффиксом файла Переход к загрузке активности при запуске воспроизведения - Воспроизведение без промежутка - Включить воспроизведение без паузы Включить воспроизведение без паузы Скрыть от других Вступает в силу в следующий раз, Android сканирует ваш телефон на предмет музыки diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 2877b4d9..bdf1e475 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -212,8 +212,6 @@ 按光盘编号和曲目编号对歌曲列表进行排序 展示比特率和文件后缀 在艺术家姓名后追加比特率和文件后缀 - 无缝播放 - 启用无缝播放 隐藏来自其他应用的音乐 隐藏其他来源 在安卓系统下次扫描音乐时生效。 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 3228ef45..e17ed742 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -234,8 +234,6 @@ Append artist name with bitrate and file suffix Show Now Playing on Play Switch to Now Playing after starting playback in media view - Gapless Playback - Enable gapless playback Hide music files from other apps. Hide From Other Takes effect next time Android scans your phone for music. diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 99c9fc0a..8872733b 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -77,12 +77,6 @@ a:summary="@string/settings.download_transition_summary" a:title="@string/settings.download_transition" app:iconSpaceReserved="false"/> - Date: Mon, 4 Apr 2022 17:59:12 +0200 Subject: [PATCH 05/67] Split Cache into dedicated datasource --- detekt-baseline.xml | 25 +-- .../org/moire/ultrasonic/data/AppDatabase.kt | 6 +- .../ultrasonic/fragment/PlayerFragment.kt | 116 ++++++----- .../ultrasonic/fragment/SearchFragment.kt | 2 +- .../ultrasonic/playback/APIDataSource.kt | 106 ++-------- .../ultrasonic/playback/CachedDataSource.kt | 183 ++++++++++++++++++ .../playback/LegacyPlaylistManager.kt | 2 +- .../playback/MediaNotificationProvider.kt | 11 +- .../org/moire/ultrasonic/playback/Plan.md | 3 +- .../ultrasonic/playback/PlaybackService.kt | 19 +- .../service/AutoMediaBrowserService.kt | 1 - .../moire/ultrasonic/service/DownloadFile.kt | 7 +- .../ultrasonic/service/DownloadService.kt | 1 - .../moire/ultrasonic/service/Downloader.kt | 20 +- .../ultrasonic/service/JukeboxMediaPlayer.kt | 21 +- .../service/MediaPlayerController.kt | 14 +- .../service/MediaPlayerLifecycleSupport.kt | 24 +-- .../org/moire/ultrasonic/service/RxBus.kt | 1 - 18 files changed, 321 insertions(+), 241 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index bfcdeea0..535a4028 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -8,35 +8,20 @@ ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name) - ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile) - ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile) ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType) - LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192 - MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000 - MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000 - MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8 - MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L - MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L - MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L - MagicNumber:MediaPlayerService.kt$MediaPlayerService$3 - MagicNumber:MediaPlayerService.kt$MediaPlayerService$4 + MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f + MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50 MagicNumber:RESTMusicService.kt$RESTMusicService$206 - NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean ) - NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable - TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception - TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable - TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception - TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception - TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw RuntimeException( String.format(Locale.ROOT, "Download of '%s' was cancelled", track) ) - TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service + TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable + TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable + TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) ) TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt index 067aa079..da6932ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -10,9 +10,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase * This could be settings or data that are not specific to any remote music database */ @Database( - entities = [ServerSetting::class], - version = 5, - exportSchema = true + entities = [ServerSetting::class], + version = 5, + exportSchema = true ) abstract class AppDatabase : RoomDatabase() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index f5b266e2..143d0371 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -45,6 +45,16 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import io.reactivex.rxjava3.disposables.CompositeDisposable +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException +import kotlin.math.abs +import kotlin.math.max import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -76,16 +86,6 @@ import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.view.AutoRepeatButton import org.moire.ultrasonic.view.VisualizerView import timber.log.Timber -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit -import kotlin.coroutines.cancellation.CancellationException -import kotlin.math.abs -import kotlin.math.max /** * Contains the Music Player screen of Ultrasonic with playback controls and the playlist @@ -216,7 +216,6 @@ class PlayerFragment : swipeVelocity = swipeDistance gestureScanner = GestureDetector(context, this) - findViews(view) val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous) val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next) @@ -829,7 +828,6 @@ class PlayerFragment : scrollToCurrent() } - private fun initPlaylistDisplay() { // Create a View Manager viewManager = LinearLayoutManager(this.context) @@ -869,63 +867,63 @@ class PlayerFragment : ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { - val from = viewHolder.bindingAdapterPosition - val to = target.bindingAdapterPosition + val from = viewHolder.bindingAdapterPosition + val to = target.bindingAdapterPosition - // Move it in the data set - mediaPlayerController.moveItemInPlaylist(from, to) - viewAdapter.submitList(mediaPlayerController.playList) + // Move it in the data set + mediaPlayerController.moveItemInPlaylist(from, to) + viewAdapter.submitList(mediaPlayerController.playList) - return true - } + return true + } - // Swipe to delete from playlist - @SuppressLint("NotifyDataSetChanged") - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - val pos = viewHolder.bindingAdapterPosition - val file = mediaPlayerController.playList[pos] - mediaPlayerController.removeFromPlaylist(file) + // Swipe to delete from playlist + @SuppressLint("NotifyDataSetChanged") + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val pos = viewHolder.bindingAdapterPosition + val file = mediaPlayerController.playList[pos] + mediaPlayerController.removeFromPlaylist(file) - val songRemoved = String.format( - resources.getString(R.string.download_song_removed), - file.track.title - ) - Util.toast(context, songRemoved) + val songRemoved = String.format( + resources.getString(R.string.download_song_removed), + file.track.title + ) + Util.toast(context, songRemoved) - viewAdapter.submitList(mediaPlayerController.playList) - viewAdapter.notifyDataSetChanged() - } + viewAdapter.submitList(mediaPlayerController.playList) + viewAdapter.notifyDataSetChanged() + } - override fun onSelectedChanged( - viewHolder: RecyclerView.ViewHolder?, - actionState: Int - ) { - super.onSelectedChanged(viewHolder, actionState) + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) - if (actionState == ACTION_STATE_DRAG) { - viewHolder?.itemView?.alpha = 0.6f + if (actionState == ACTION_STATE_DRAG) { + viewHolder?.itemView?.alpha = 0.6f + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + + viewHolder.itemView.alpha = 1.0f + } + + override fun isLongPressDragEnabled(): Boolean { + return false } } - - override fun clearView( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ) { - super.clearView(recyclerView, viewHolder) - - viewHolder.itemView.alpha = 1.0f - } - - override fun isLongPressDragEnabled(): Boolean { - return false - } - } ) dragTouchHelper.attachToRecyclerView(playlistView) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 72baa959..6df86761 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -151,7 +151,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { val arguments = arguments val autoPlay = arguments != null && - arguments.getBoolean(Constants.INTENT_AUTOPLAY, false) + arguments.getBoolean(Constants.INTENT_AUTOPLAY, false) val query = arguments?.getString(Constants.INTENT_QUERY) // If started with a query, enter it to the searchView diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt index 92f3244a..f3deb4ba 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -10,7 +10,6 @@ package org.moire.ultrasonic.playback import android.net.Uri import androidx.core.net.toUri import androidx.media3.common.C -import androidx.media3.common.MediaLibraryInfo import androidx.media3.common.PlaybackException import androidx.media3.common.util.Assertions import androidx.media3.common.util.UnstableApi @@ -25,20 +24,16 @@ import androidx.media3.datasource.HttpDataSource.RequestProperties import androidx.media3.datasource.HttpUtil import androidx.media3.datasource.TransferListener import com.google.common.net.HttpHeaders -import okhttp3.CacheControl +import java.io.IOException +import java.io.InputStream +import java.io.InterruptedIOException import okhttp3.Call import okhttp3.ResponseBody import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.response.StreamResponse import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.api.subsonic.toStreamResponse -import org.moire.ultrasonic.util.AbstractFile -import org.moire.ultrasonic.util.FileUtil -import org.moire.ultrasonic.util.Storage import timber.log.Timber -import java.io.IOException -import java.io.InputStream -import java.io.InterruptedIOException /** * An [HttpDataSource] that delegates to Square's [Call.Factory]. @@ -49,39 +44,26 @@ import java.io.InterruptedIOException * construct the instance. */ @UnstableApi -open class OkHttpDataSource private constructor( - subsonicAPIClient: SubsonicAPIClient, - userAgent: String?, - cacheControl: CacheControl?, - defaultRequestProperties: RequestProperties? +open class APIDataSource private constructor( + subsonicAPIClient: SubsonicAPIClient ) : BaseDataSource(true), HttpDataSource { - companion object { - init { - MediaLibraryInfo.registerModule("media3.datasource.okhttp") - } - } - /** [DataSource.Factory] for [OkHttpDataSource] instances. */ + /** [DataSource.Factory] for [APIDataSource] instances. */ class Factory(private val subsonicAPIClient: SubsonicAPIClient) : HttpDataSource.Factory { private val defaultRequestProperties: RequestProperties = RequestProperties() - private var userAgent: String? = null private var transferListener: TransferListener? = null - private var cacheControl: CacheControl? = null - override fun setDefaultRequestProperties(defaultRequestProperties: Map): Factory { + override fun setDefaultRequestProperties( + defaultRequestProperties: Map + ): Factory { this.defaultRequestProperties.clearAndSet(defaultRequestProperties) return this } - /** * Sets the [TransferListener] that will be used. * - * - * The default is `null`. - * - * * See [DataSource.addTransferListener]. * * @param transferListener The listener that will be used. @@ -92,41 +74,29 @@ open class OkHttpDataSource private constructor( return this } - override fun createDataSource(): OkHttpDataSource { - val dataSource = OkHttpDataSource( - subsonicAPIClient, - userAgent, - cacheControl, - defaultRequestProperties + override fun createDataSource(): APIDataSource { + val dataSource = APIDataSource( + subsonicAPIClient ) if (transferListener != null) { dataSource.addTransferListener(transferListener!!) } return dataSource } - } - private val subsonicAPIClient: SubsonicAPIClient = Assertions.checkNotNull(subsonicAPIClient) - private val requestProperties: RequestProperties - private val userAgent: String? - private val cacheControl: CacheControl? - private val defaultRequestProperties: RequestProperties? + private val requestProperties: RequestProperties = RequestProperties() private var dataSpec: DataSpec? = null private var response: retrofit2.Response? = null private var responseByteStream: InputStream? = null private var openedNetwork = false - private var openedFile = false - private var cachePath: String? = null - private var cacheFile: AbstractFile? = null private var bytesToRead: Long = 0 private var bytesRead: Long = 0 override fun getUri(): Uri? { - return when { - cachePath != null -> cachePath!!.toUri() - response == null -> null + return when (response) { + null -> null else -> response!!.raw().request.url.toString().toUri() } } @@ -164,15 +134,6 @@ open class OkHttpDataSource private constructor( val components = dataSpec.uri.toString().split('|') val id = components[0] val bitrate = components[1].toInt() - val path = components[2] - - val cacheLength = checkCache(path) - - // We have found an item in the cache, return early - if (cacheLength > 0) { - bytesToRead = cacheLength - return bytesToRead - } Timber.i("DATASOURCE: %s", "Start") val request = subsonicAPIClient.api.stream(id, bitrate, offset = 0) @@ -216,7 +177,9 @@ open class OkHttpDataSource private constructor( val headers = response.headers().toMultimap() closeConnectionQuietly() val cause: IOException? = - if (responseCode == 416) DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) else null + if (responseCode == 416) DataSourceException( + PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE + ) else null throw InvalidResponseCodeException( responseCode, response.message(), cause, headers, dataSpec, errorResponseBody ) @@ -266,35 +229,9 @@ open class OkHttpDataSource private constructor( openedNetwork = false transferEnded() closeConnectionQuietly() - } else if (openedFile) { - openedFile = false - responseByteStream?.close() - responseByteStream = null } } - /** - * Checks our cache for a matching media file - */ - private fun checkCache(path: String): Long { - var filePath: String = path - var found = Storage.isPathExists(path) - - if (!found) { - filePath = FileUtil.getCompleteFile(path) - found = Storage.isPathExists(filePath) - } - - if (!found) return -1 - - cachePath = filePath - - cacheFile = Storage.getFromPath(filePath)!! - responseByteStream = cacheFile!!.getFileInputStream() - - return cacheFile!!.getDocumentFileDescriptor("r")!!.length - } - /** * Attempts to skip the specified number of bytes in full. * @@ -388,11 +325,4 @@ open class OkHttpDataSource private constructor( } responseByteStream = null } - - init { - this.userAgent = userAgent - this.cacheControl = cacheControl - this.defaultRequestProperties = defaultRequestProperties - requestProperties = RequestProperties() - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt new file mode 100644 index 00000000..8de2a107 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -0,0 +1,183 @@ +/* + * CachedDataSource.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.playback + +import android.net.Uri +import androidx.core.net.toUri +import androidx.media3.common.C +import androidx.media3.common.util.Util +import androidx.media3.datasource.BaseDataSource +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSpec +import androidx.media3.datasource.HttpDataSource +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.CacheDataSource.CacheIgnoredReason +import java.io.IOException +import java.io.InputStream +import org.moire.ultrasonic.util.AbstractFile +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.Storage + +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +class CachedDataSource( + private var upstreamDataSource: DataSource, + private var eventListener: EventListener? +) : BaseDataSource(false) { + + class Factory( + var upstreamDataSourceFactory: DataSource.Factory + ) : DataSource.Factory { + + private var eventListener: EventListener? = null + + /** + * Sets the {link EventListener} to which events are delivered. + * + * + * The default is `null`. + * + * @param eventListener The [EventListener]. + * @return This factory. + */ + fun setEventListener(eventListener: EventListener?): Factory { + this.eventListener = eventListener + return this + } + + override fun createDataSource(): CachedDataSource { + return createDataSourceInternal( + upstreamDataSourceFactory.createDataSource() + ) + } + + private fun createDataSourceInternal( + upstreamDataSource: DataSource + ): CachedDataSource { + return CachedDataSource( + upstreamDataSource, + eventListener + ) + } + } + + /** Listener of [CacheDataSource] events. */ + interface EventListener { + /** + * Called when bytes have been read from the cache. + * + * @param cacheSizeBytes Current cache size in bytes. + * @param cachedBytesRead Total bytes read from the cache since this method was last called. + */ + fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) + + /** + * Called when the current request ignores cache. + * + * @param reason Reason cache is bypassed. + */ + fun onCacheIgnored(reason: @CacheIgnoredReason Int) + } + + private var bytesToRead: Long = 0 + private var bytesRead: Long = 0 + private var dataSpec: DataSpec? = null + private var responseByteStream: InputStream? = null + private var openedFile = false + private var cachePath: String? = null + private var cacheFile: AbstractFile? = null + + override fun open(dataSpec: DataSpec): Long { + this.dataSpec = dataSpec + bytesRead = 0 + bytesToRead = 0 + + val components = dataSpec.uri.toString().split('|') + val path = components[2] + val cacheLength = checkCache(path) + + // We have found an item in the cache, return early + if (cacheLength > 0) { + transferInitializing(dataSpec) + bytesToRead = cacheLength + return bytesToRead + } + + // else forward the call to upstream + return upstreamDataSource.open(dataSpec) + } + + override fun read(buffer: ByteArray, offset: Int, length: Int): Int { + if (cachePath != null) { + try { + return readInternal(buffer, offset, length) + } catch (e: IOException) { + throw HttpDataSource.HttpDataSourceException.createForIOException( + e, Util.castNonNull(dataSpec), HttpDataSource.HttpDataSourceException.TYPE_READ + ) + } + } else { + return upstreamDataSource.read(buffer, offset, length) + } + } + + private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int { + var readLength = readLength + if (readLength == 0) { + return 0 + } + if (bytesToRead != C.LENGTH_UNSET.toLong()) { + val bytesRemaining = bytesToRead - bytesRead + if (bytesRemaining == 0L) { + return C.RESULT_END_OF_INPUT + } + readLength = readLength.toLong().coerceAtMost(bytesRemaining).toInt() + } + val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLength) + if (read == -1) { + return C.RESULT_END_OF_INPUT + } + bytesRead += read.toLong() + // TODO + // bytesTransferred(read) + return read + } + + override fun getUri(): Uri? { + return cachePath?.toUri() + } + + override fun close() { + if (openedFile) { + openedFile = false + responseByteStream?.close() + responseByteStream = null + } + } + + /** + * Checks our cache for a matching media file + */ + private fun checkCache(path: String): Long { + var filePath: String = path + var found = Storage.isPathExists(path) + + if (!found) { + filePath = FileUtil.getCompleteFile(path) + found = Storage.isPathExists(filePath) + } + + if (!found) return -1 + + cachePath = filePath + + cacheFile = Storage.getFromPath(filePath)!! + responseByteStream = cacheFile!!.getFileInputStream() + + return cacheFile!!.getDocumentFileDescriptor("r")!!.length + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index ebe17602..9ea9b832 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -124,4 +124,4 @@ class LegacyPlaylistManager : KoinComponent { if (save != null) this.shouldSave = save } } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 4afd316d..02b63b8e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -60,8 +60,8 @@ internal class MediaNotificationProvider(context: Context) : ActionFactory.COMMAND_SKIP_TO_PREVIOUS ) ) - if (mediaController.playbackState == Player.STATE_ENDED - || !mediaController.playWhenReady + if (mediaController.playbackState == Player.STATE_ENDED || + !mediaController.playWhenReady ) { // Play action. builder.addAction( @@ -133,8 +133,8 @@ internal class MediaNotificationProvider(context: Context) : } private fun ensureNotificationChannel() { - if (Util.SDK_INT < 26 - || notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null + if (Util.SDK_INT < 26 || + notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null ) { return } @@ -154,5 +154,4 @@ internal class MediaNotificationProvider(context: Context) : return R.drawable.ic_stat_ultrasonic } } - -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md index 7f9a4100..0019ee0b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/Plan.md @@ -10,9 +10,8 @@ UI: [] If wanted also hand them to Downloader.kt [x] Service plays MediaItem through OkHttp [x] UI needs to receive info from service -[] Create a Cache Layer +[x] Create a Cache Layer [] Translate AutoMediaBrowserService [] Add new shuffle icon.... -DownloadNotificationHelper convertToPlaybackStateCompatState() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index bd98fb84..3a102ec3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -43,7 +43,6 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.util.Constants - class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var player: ExoPlayer private lateinit var mediaLibrarySession: MediaLibrarySession @@ -154,7 +153,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent { .build() return item - } } @@ -180,26 +178,19 @@ class PlaybackService : MediaLibraryService(), KoinComponent { * * Could be refined to use WAKE_MODE_LOCAL when offline.... */ - setMediaNotificationProvider(MediaNotificationProvider(UApp.applicationContext())) - val subsonicAPIClient: SubsonicAPIClient by inject() // Create a MediaSource which passes calls through our OkHttp Stack - dataSourceFactory = OkHttpDataSource.Factory(subsonicAPIClient) - + dataSourceFactory = APIDataSource.Factory(subsonicAPIClient) // A download cache should not evict media, so should use a NoopCacheEvictor. // A download cache should not evict media, so should use a NoopCacheEvictor. // TODO: Add cache: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer // var cache = UltrasonicCache() // -// val cacheDataSourceFactory: DataSource.Factory = CacheDataSource.Factory() -// .setCache(cache) -// .setUpstreamDataSourceFactory(dataSourceFactory) -// .setCacheWriteDataSinkFactory(null) // Disable writing. - + val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(dataSourceFactory) // Create a renderer with HW rendering support val renderer = DefaultRenderersFactory(this) @@ -210,12 +201,12 @@ class PlaybackService : MediaLibraryService(), KoinComponent { .setAudioAttributes(getAudioAttributes(), true) .setWakeMode(C.WAKE_MODE_NETWORK) .setHandleAudioBecomingNoisy(true) - .setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory)) - //.setRenderersFactory(renderer) + .setMediaSourceFactory(DefaultMediaSourceFactory(cacheDataSourceFactory)) + // .setRenderersFactory(renderer) .build() // Enable audio offload - //player.experimentalSetOffloadSchedulingEnabled(true) + // player.experimentalSetOffloadSchedulingEnabled(true) MediaItemTree.initialize(assets) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index f700ee8a..a8b3a9ce 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -107,7 +107,6 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { playFromSearchCommand(it.first) } - val handler = Handler(Looper.getMainLooper()) handler.postDelayed( { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 95985926..e52d6a90 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -80,9 +80,9 @@ class DownloadFile( init { partialFile = FileUtil.getParentPath(pinnedFile) + "/" + - FileUtil.getPartialFile(FileUtil.getNameFromPath(pinnedFile)) + FileUtil.getPartialFile(FileUtil.getNameFromPath(pinnedFile)) completeFile = FileUtil.getParentPath(pinnedFile) + "/" + - FileUtil.getCompleteFile(FileUtil.getNameFromPath(pinnedFile)) + FileUtil.getCompleteFile(FileUtil.getNameFromPath(pinnedFile)) } /** @@ -99,7 +99,6 @@ class DownloadFile( downloadPrepared = true } - @Synchronized fun cancelDownload() { downloadTask?.cancel() @@ -122,7 +121,7 @@ class DownloadFile( @get:Synchronized val isWorkDone: Boolean get() = Storage.isPathExists(completeFile) && !shouldSave || - Storage.isPathExists(pinnedFile) || saveWhenDone || completeWhenDone + Storage.isPathExists(pinnedFile) || saveWhenDone || completeWhenDone @get:Synchronized val isDownloading: Boolean diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index 1f1dddda..46a216a8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -52,7 +52,6 @@ class DownloadService : Service() { override fun onCreate() { super.onCreate() - // Create Notification Channel createNotificationChannel() updateNotification() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 9679f03b..4348f933 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -6,6 +6,11 @@ import android.os.Looper import android.text.TextUtils import androidx.lifecycle.MutableLiveData import io.reactivex.rxjava3.disposables.CompositeDisposable +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.Locale +import java.util.PriorityQueue import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider @@ -22,11 +27,6 @@ import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.safeClose import timber.log.Timber -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.util.Locale -import java.util.PriorityQueue /** * This class is responsible for maintaining the playlist and downloading @@ -232,7 +232,6 @@ class Downloader( return (oldSize != activelyDownloading.size) } - @get:Synchronized val all: List get() { @@ -296,7 +295,6 @@ class Downloader( updateLiveData() } - @Synchronized fun downloadBackground(songs: List, save: Boolean) { @@ -339,7 +337,6 @@ class Downloader( return downloadFile } - companion object { const val PARALLEL_DOWNLOADS = 3 const val CHECK_INTERVAL = 5000L @@ -400,8 +397,11 @@ class Downloader( val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0 needsDownloading = ( - downloadFile.desiredBitRate == 0 || duration == null || duration == 0 || fileLength == 0L - ) + downloadFile.desiredBitRate == 0 || + duration == null || + duration == 0 || + fileLength == 0L + ) if (needsDownloading) { // Attempt partial HTTP GET, appending to the file if it exists. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index af14e15d..a6326cd3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -13,6 +13,13 @@ import android.view.LayoutInflater import android.view.View import android.widget.ProgressBar import android.widget.Toast +import java.util.concurrent.Executors +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import kotlin.math.roundToInt import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException @@ -24,13 +31,6 @@ import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Util.sleepQuietly import org.moire.ultrasonic.util.Util.toast import timber.log.Timber -import java.util.concurrent.Executors -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong -import kotlin.math.roundToInt /** * Provides an asynchronous interface to the remote jukebox on the Subsonic server. @@ -204,7 +204,10 @@ class JukeboxMediaPlayer(private val downloader: Downloader) { val positionSeconds: Int get() { - if (jukeboxStatus == null || jukeboxStatus!!.positionSeconds == null || timeOfLastUpdate.get() == 0L) { + if (jukeboxStatus == null || + jukeboxStatus!!.positionSeconds == null || + timeOfLastUpdate.get() == 0L + ) { return 0 } if (jukeboxStatus!!.isPlaying) { @@ -334,4 +337,4 @@ class JukeboxMediaPlayer(private val downloader: Downloader) { companion object { private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 2f89e21b..73450e89 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -105,7 +105,7 @@ class MediaPlayerController( } }) - //controller?.play() + // controller?.play() }, MoreExecutors.directExecutor()) created = true @@ -158,7 +158,7 @@ class MediaPlayerController( } } - //Update widget + // Update widget if (currentPlaying != null) { updateWidget(playerState, currentPlaying.track) } @@ -230,7 +230,6 @@ class MediaPlayerController( } autoPlayStart = false - } } @@ -274,7 +273,6 @@ class MediaPlayerController( } } - @Synchronized fun seekTo(position: Int) { controller?.seekTo(position.toLong()) @@ -303,7 +301,6 @@ class MediaPlayerController( } } - @Synchronized @Deprecated("Use InsertionMode Syntax") @Suppress("LongParameterList") @@ -591,7 +588,7 @@ class MediaPlayerController( // Trigger an update // TODO Update Metadata of MediaItem... - //localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) + // localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) song.starred = !song.starred } @@ -609,7 +606,7 @@ class MediaPlayerController( } }.start() // TODO this would be better handled with a Rx command - //updateNotification() + // updateNotification() } val currentMediaItem: MediaItem? @@ -650,7 +647,6 @@ class MediaPlayerController( } } - fun Track.toMediaItem(): MediaItem { val filePath = FileUtil.getSongFile(this) @@ -670,4 +666,4 @@ fun Track.toMediaItem(): MediaItem { .setMediaMetadata(metadata.build()) return mediaItem.build() -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index a12e83c0..f5f47d40 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -110,10 +110,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { val autoStart = keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || - keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || - keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || + keyCode == KeyEvent.KEYCODE_MEDIA_NEXT // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start onCreate(autoStart) { @@ -140,23 +140,23 @@ class MediaPlayerLifecycleSupport : KoinComponent { /** * This function processes the intent that could come from other applications. */ - private fun handleUltrasonicIntent(intentAction: String) { + private fun handleUltrasonicIntent(action: String) { val isRunning = created // If Ultrasonic is not running, do nothing to stop or pause - if (!isRunning && (intentAction == Constants.CMD_PAUSE || intentAction == Constants.CMD_STOP)) + if (!isRunning && (action == Constants.CMD_PAUSE || action == Constants.CMD_STOP)) return - val autoStart = intentAction == Constants.CMD_PLAY || - intentAction == Constants.CMD_RESUME_OR_PLAY || - intentAction == Constants.CMD_TOGGLEPAUSE || - intentAction == Constants.CMD_PREVIOUS || - intentAction == Constants.CMD_NEXT + val autoStart = action == Constants.CMD_PLAY || + action == Constants.CMD_RESUME_OR_PLAY || + action == Constants.CMD_TOGGLEPAUSE || + action == Constants.CMD_PREVIOUS || + action == Constants.CMD_NEXT // We can receive intents when everything is stopped, so we need to start onCreate(autoStart) { - when (intentAction) { + when (action) { Constants.CMD_PLAY -> mediaPlayerController.play() Constants.CMD_RESUME_OR_PLAY -> // If Ultrasonic wasn't running, the autoStart is enough to resume, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 469b50e9..1ffb89b7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -2,7 +2,6 @@ package org.moire.ultrasonic.service import android.os.Bundle import android.support.v4.media.session.MediaSessionCompat -import android.view.KeyEvent import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable From 5dc9fda7a458d399bdf834035b447e57718c32ea Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Apr 2022 18:18:52 +0200 Subject: [PATCH 06/67] Detekt fixes --- detekt-config.yml | 2 +- .../ultrasonic/fragment/PlayerFragment.kt | 1 + .../ultrasonic/playback/APIDataSource.kt | 5 +++- .../playback/LegacyPlaylistManager.kt | 5 ---- .../ultrasonic/playback/MediaItemTree.kt | 7 ----- .../playback/MediaNotificationProvider.kt | 4 ++- .../ultrasonic/service/DownloadService.kt | 30 ------------------- .../moire/ultrasonic/service/Downloader.kt | 2 ++ .../service/MediaPlayerController.kt | 24 ++++++++++++++- .../service/MediaPlayerLifecycleSupport.kt | 3 +- 10 files changed, 36 insertions(+), 47 deletions(-) diff --git a/detekt-config.yml b/detekt-config.yml index cdebb21a..b59e4ec3 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -70,7 +70,7 @@ style: excludeImportStatements: false MagicNumber: # 100 common in percentage, 1000 in milliseconds - ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024'] + ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024', '4096'] ignoreEnums: true ignorePropertyDeclaration: true UnnecessaryAbstractClass: diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 143d0371..d1e4d95b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -1017,6 +1017,7 @@ class PlayerFragment : } } + @Suppress("LongMethod") @Synchronized private fun onSliderProgressChanged() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt index f3deb4ba..6ab75ca3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -43,6 +43,7 @@ import timber.log.Timber * priority) the `dataSpec`, [.setRequestProperty] and the default parameters used to * construct the instance. */ +@Suppress("MagicNumber") @UnstableApi open class APIDataSource private constructor( subsonicAPIClient: SubsonicAPIClient @@ -124,6 +125,7 @@ open class APIDataSource private constructor( requestProperties.clear() } + @Suppress("LongMethod", "NestedBlockDepth") @Throws(HttpDataSourceException::class) override fun open(dataSpec: DataSpec): Long { this.dataSpec = dataSpec @@ -171,7 +173,7 @@ open class APIDataSource private constructor( } val errorResponseBody: ByteArray = try { Util.toByteArray(Assertions.checkNotNull(responseByteStream)) - } catch (e: IOException) { + } catch (ignore: IOException) { Util.EMPTY_BYTE_ARRAY } val headers = response.headers().toMultimap() @@ -241,6 +243,7 @@ open class APIDataSource private constructor( * occurs while reading from the source, or if the data ended before skipping the specified * number of bytes. */ + @Suppress("ThrowsCount") @Throws(HttpDataSourceException::class) private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) { var bytesToSkip = bytesToSkip diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 9ea9b832..86a0483a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -93,11 +93,6 @@ class LegacyPlaylistManager : KoinComponent { val playlist: List get() = _playlist - // FIXME: Returns wrong count if item is twice in queue - @get:Synchronized - val currentPlayingIndex: Int - get() = _playlist.indexOf(currentPlaying) - @get:Synchronized val playlistDuration: Long get() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt index f3232bb8..d2f76e5f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt @@ -22,7 +22,6 @@ import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS -import androidx.media3.common.util.Util import com.google.common.collect.ImmutableList import org.json.JSONObject @@ -89,12 +88,6 @@ object MediaItemTree { .build() } - @androidx.media3.common.util.UnstableApi - private fun loadJSONFromAsset(assets: AssetManager): String { - val buffer = assets.open("catalog.json").use { Util.toByteArray(it) } - return String(buffer, Charsets.UTF_8) - } - fun initialize(assets: AssetManager) { if (isInitialized) return isInitialized = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 02b63b8e..2eaaba9a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -12,6 +12,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.graphics.BitmapFactory +import android.os.Build import android.os.Bundle import androidx.core.app.NotificationCompat import androidx.core.graphics.drawable.IconCompat @@ -38,6 +39,7 @@ internal class MediaNotificationProvider(context: Context) : context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager ) + @Suppress("LongMethod") override fun createNotification( mediaController: MediaController, actionFactory: ActionFactory, @@ -133,7 +135,7 @@ internal class MediaNotificationProvider(context: Context) : } private fun ensureNotificationChannel() { - if (Util.SDK_INT < 26 || + if (Util.SDK_INT < Build.VERSION_CODES.O || notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null ) { return diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index 46a216a8..e9cc351d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -84,36 +84,6 @@ class DownloadService : Service() { stopSelf() } - private fun setupOnSongCompletedHandler() { -// localMediaPlayer.onSongCompleted = { currentPlaying: DownloadFile? -> -// val index = downloader.currentPlayingIndex -// -// if (currentPlaying != null) { -// val song = currentPlaying.track -// if (song.bookmarkPosition > 0 && Settings.shouldClearBookmark) { -// val musicService = getMusicService() -// try { -// musicService.deleteBookmark(song.id) -// } catch (ignored: Exception) { -// } -// } -// } -// if (index != -1) { -// -// if (index + 1 < 0 || index + 1 >= downloader.getPlaylist().size) { -// if (Settings.shouldClearPlaylist) { -// clear(true) -// jukeboxMediaPlayer.updatePlaylist() -// } -// resetPlayback() -// } else { -// play(index + 1) -// } -// } -// null -// } - } - private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 4348f933..3cb70bd6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -131,6 +131,7 @@ class Downloader( } } + @Suppress("ComplexMethod", "ComplexCondition") @Synchronized fun checkDownloadsInternal() { if (!Util.isExternalStoragePresent() || !storageMonitor.isExternalStorageAvailable) { @@ -355,6 +356,7 @@ class Downloader( private inner class DownloadTask(private val downloadFile: DownloadFile) : CancellableTask() { val musicService = MusicServiceFactory.getMusicService() + @Suppress("LongMethod", "ComplexMethod", "NestedBlockDepth") override fun execute() { downloadFile.downloadPrepared = false diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 73450e89..766da850 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -100,6 +100,7 @@ class MediaPlayerController( } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + onTrackCompleted(mediaItem) legacyPlaylistManager.updateCurrentPlaying(mediaItem) publishPlaybackState() } @@ -166,6 +167,27 @@ class MediaPlayerController( Timber.d("Processed player state change") } + private fun onTrackCompleted(mediaItem: MediaItem?) { + // This method is called before we update the currentPlaying, + // so in fact currentPlaying will refer to the track that has just finished. + if (legacyPlaylistManager.currentPlaying != null) { + val song = legacyPlaylistManager.currentPlaying!!.track + if (song.bookmarkPosition > 0 && Settings.shouldClearBookmark) { + val musicService = getMusicService() + try { + musicService.deleteBookmark(song.id) + } catch (ignored: Exception) { + } + } + } + + // Playback has ended... + if (mediaItem == null && Settings.shouldClearPlaylist) { + clear(true) + jukeboxMediaPlayer.updatePlaylist() + } + } + private fun publishPlaybackState() { RxBus.playerStatePublisher.onNext( RxBus.StateWithTrack( @@ -465,7 +487,7 @@ class MediaPlayerController( playbackStateSerializer.serialize( legacyPlaylistManager.playlist, - legacyPlaylistManager.currentPlayingIndex, + currentMediaItemIndex, playerPosition ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index f5f47d40..77b55348 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -101,7 +101,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { } } - @Suppress("MagicNumber") + @Suppress("MagicNumber", "ComplexMethod") private fun handleKeyEvent(event: KeyEvent) { if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return @@ -140,6 +140,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { /** * This function processes the intent that could come from other applications. */ + @Suppress("ComplexMethod") private fun handleUltrasonicIntent(action: String) { val isRunning = created From b1c2d020b57d956264d167569348c583ae1c39b6 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Apr 2022 18:27:59 +0200 Subject: [PATCH 07/67] Fix a bug in MetadataParser (unrelated to media3) --- .../kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 0034db0b..27d84aa9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -563,7 +563,7 @@ class OfflineMusicService : MusicService, KoinComponent { } catch (ignored: Exception) { } - artist = meta.artist ?: file.parent!!.parent!!.name + artist = meta.artist ?: file.parent!!.parent?.name ?: "" album = meta.album ?: file.parent!!.name title = meta.title ?: title isVideo = meta.hasVideo != null From 2f7f47783a755ce089197d26b9af50f50a2ea9a1 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Apr 2022 18:28:23 +0200 Subject: [PATCH 08/67] Enable HW playback for better performance --- .../org/moire/ultrasonic/playback/PlaybackService.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 3a102ec3..bfad720a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -184,12 +184,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // Create a MediaSource which passes calls through our OkHttp Stack dataSourceFactory = APIDataSource.Factory(subsonicAPIClient) - - // A download cache should not evict media, so should use a NoopCacheEvictor. - // A download cache should not evict media, so should use a NoopCacheEvictor. - // TODO: Add cache: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer -// var cache = UltrasonicCache() -// val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(dataSourceFactory) // Create a renderer with HW rendering support @@ -202,11 +196,11 @@ class PlaybackService : MediaLibraryService(), KoinComponent { .setWakeMode(C.WAKE_MODE_NETWORK) .setHandleAudioBecomingNoisy(true) .setMediaSourceFactory(DefaultMediaSourceFactory(cacheDataSourceFactory)) - // .setRenderersFactory(renderer) + .setRenderersFactory(renderer) .build() // Enable audio offload - // player.experimentalSetOffloadSchedulingEnabled(true) + player.experimentalSetOffloadSchedulingEnabled(true) MediaItemTree.initialize(assets) From dd65a12b53ea813f192553fc134913ec0b1f160a Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 4 Apr 2022 21:18:07 +0200 Subject: [PATCH 09/67] Migrate AutoMediaBrowser --- gradle/libs.versions.toml | 2 + ultrasonic/build.gradle | 1 + .../AutoMediaBrowserCallback.kt} | 804 +++++++++--------- .../ultrasonic/playback/CachedDataSource.kt | 8 +- .../playback/LegacyPlaylistManager.kt | 14 - .../ultrasonic/playback/MediaItemTree.kt | 247 ------ .../playback/MediaNotificationProvider.kt | 1 - .../ultrasonic/playback/PlaybackService.kt | 105 +-- .../moire/ultrasonic/service/Downloader.kt | 12 +- .../ultrasonic/service/JukeboxMediaPlayer.kt | 3 +- .../service/MediaPlayerLifecycleSupport.kt | 27 +- .../service/PlaybackStateSerializer.kt | 2 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 229 +---- 13 files changed, 465 insertions(+), 990 deletions(-) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{service/AutoMediaBrowserService.kt => playback/AutoMediaBrowserCallback.kt} (62%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea4b36e8..59ea094a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ multidex = "2.0.1" room = "2.4.0" kotlin = "1.6.10" kotlinxCoroutines = "1.6.0-native-mt" +kotlinxGuava = "1.6.0" viewModelKtx = "2.3.0" retrofit = "2.9.0" @@ -74,6 +75,7 @@ media3session = { module = "androidx.media3:media3-session", version.r kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxGuava"} retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" } diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 57836a3b..3cfcdeb6 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -112,6 +112,7 @@ dependencies { implementation libs.kotlinStdlib implementation libs.kotlinxCoroutines + implementation libs.kotlinxGuava implementation libs.koinAndroid implementation libs.okhttpLogging implementation libs.fastScroll diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt similarity index 62% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index a8b3a9ce..c18c3193 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -1,32 +1,47 @@ /* - * AutoMediaBrowserService.kt - * Copyright (C) 2009-2021 Ultrasonic developers + * CustomMediaLibrarySessionCallback.kt + * Copyright (C) 2009-2022 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.service +package org.moire.ultrasonic.playback +import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.MediaDescriptionCompat -import androidx.media.MediaBrowserServiceCompat -import androidx.media.utils.MediaConstants -import io.reactivex.rxjava3.disposables.CompositeDisposable +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS +import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES +import androidx.media3.common.Player +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +import androidx.media3.session.SessionResult +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.guava.future import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -66,13 +81,16 @@ private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM" private const val DISPLAY_LIMIT = 100 private const val SEARCH_LIMIT = 10 +private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" +private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" + /** * MediaBrowserService implementation for e.g. Android Auto */ @Suppress("TooManyFunctions", "LargeClass") -class AutoMediaBrowserService : MediaBrowserServiceCompat() { +class AutoMediaBrowserCallback(var player: Player) : + MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback, KoinComponent { - private val lifecycleSupport by inject() private val mediaPlayerController by inject() private val activeServerProvider: ActiveServerProvider by inject() private val musicService = MusicServiceFactory.getMusicService() @@ -89,40 +107,200 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { private val useId3Tags get() = Settings.shouldUseId3Tags private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId - private var rxBusSubscription: CompositeDisposable = CompositeDisposable() - @Suppress("MagicNumber") - override fun onCreate() { - super.onCreate() - - rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe { - if (sessionToken == null) sessionToken = it - } - - rxBusSubscription += RxBus.playFromMediaIdCommandObservable.subscribe { - playFromMediaId(it.first) - } - - rxBusSubscription += RxBus.playFromSearchCommandObservable.subscribe { - playFromSearchCommand(it.first) - } - - val handler = Handler(Looper.getMainLooper()) - handler.postDelayed( - { - // Ultrasonic may be started from Android Auto. This boots up the necessary components. - Timber.d( - "AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..." - ) - lifecycleSupport.onCreate() - DownloadService.getInstance() - }, - 100 + /** + * Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link + * MediaBrowser#getLibraryRoot(LibraryParams)}. + * + *

Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser + * asynchronously. You can also return a {@link LibraryResult} directly by using Guava's + * {@link Futures#immediateFuture(Object)}. + * + *

The {@link LibraryResult#params} may differ from the given {@link LibraryParams params} + * if the session can't provide a root that matches with the {@code params}. + * + *

To allow browsing the media library, return a {@link LibraryResult} with {@link + * LibraryResult#RESULT_SUCCESS} and a root {@link MediaItem} with a valid {@link + * MediaItem#mediaId}. The media id is required for the browser to get the children under the + * root. + * + *

Interoperability: If this callback is called because a legacy {@link + * android.support.v4.media.MediaBrowserCompat} has requested a {@link + * androidx.media.MediaBrowserServiceCompat.BrowserRoot}, then the main thread may be blocked + * until the returned future is done. If your service may be queried by a legacy {@link + * android.support.v4.media.MediaBrowserCompat}, you should ensure that the future completes + * quickly to avoid blocking the main thread for a long period of time. + * + * @param session The session for this event. + * @param browser The browser information. + * @param params The optional parameters passed by the browser. + * @return A pending result that will be resolved with a root media item. + * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT + */ + override fun onGetLibraryRoot( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return Futures.immediateFuture( + LibraryResult.ofItem( + buildMediaItem( + "Root Folder", + MEDIA_ROOT_ID, + isPlayable = false, + folderType = FOLDER_TYPE_MIXED + ), + params + ) ) - - Timber.i("AutoMediaBrowserService onCreate finished") } + override fun onGetItem( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + mediaId: String + ): ListenableFuture> { + playFromMediaId(mediaId) + + // TODO: Later + return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + } + + override fun onGetChildren( + session: MediaLibraryService.MediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + // TODO: params??? + return onLoadChildren(parentId) + } + + private fun setMediaItemFromSearchQuery(query: String) { + // Only accept query with pattern "play [Title]" or "[Title]" + // Where [Title]: must be exactly matched + // If no media with exact name found, play a random media instead + val mediaTitle = + if (query.startsWith("play ", ignoreCase = true)) { + query.drop(5) + } else { + query + } + + playFromMediaId(mediaTitle) + } + + override fun onSetMediaUri( + session: MediaSession, + controller: MediaSession.ControllerInfo, + uri: Uri, + extras: Bundle + ): Int { + + if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || + uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) + ) { + val searchQuery = + uri.getQueryParameter("query") + ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED + setMediaItemFromSearchQuery(searchQuery) + + return SessionResult.RESULT_SUCCESS + } else { + return SessionResult.RESULT_ERROR_NOT_SUPPORTED + } + } + + + @Suppress("ReturnCount", "ComplexMethod") + fun onLoadChildren( + parentId: String, + ): ListenableFuture>> { + Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId) + + val parentIdParts = parentId.split('|') + + when (parentIdParts.first()) { + MEDIA_ROOT_ID -> return getRootItems() + MEDIA_LIBRARY_ID -> return getLibrary() + MEDIA_ARTIST_ID -> return getArtists() + MEDIA_ARTIST_SECTION -> return getArtists(parentIdParts[1]) + MEDIA_ALBUM_ID -> return getAlbums(AlbumListType.SORTED_BY_NAME) + MEDIA_ALBUM_PAGE_ID -> return getAlbums( + AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt() + ) + MEDIA_PLAYLIST_ID -> return getPlaylists() + MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT) + MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST) + MEDIA_ALBUM_RECENT_ID -> return getAlbums(AlbumListType.RECENT) + MEDIA_ALBUM_RANDOM_ID -> return getAlbums(AlbumListType.RANDOM) + MEDIA_ALBUM_STARRED_ID -> return getAlbums(AlbumListType.STARRED) + MEDIA_SONG_RANDOM_ID -> return getRandomSongs() + MEDIA_SONG_STARRED_ID -> return getStarredSongs() + MEDIA_SHARE_ID -> return getShares() + MEDIA_BOOKMARK_ID -> return getBookmarks() + MEDIA_PODCAST_ID -> return getPodcasts() + MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2]) + MEDIA_ARTIST_ITEM -> return getAlbumsForArtist( + parentIdParts[1], parentIdParts[2] + ) + MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2]) + MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1]) + MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1]) + else -> return Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null)) + } + } + + fun onSearch( + query: String, + extras: Bundle?, + ): ListenableFuture>> { + Timber.d("AutoMediaBrowserService onSearch query: %s", query) + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { + val criteria = SearchCriteria(query, SEARCH_LIMIT, SEARCH_LIMIT, SEARCH_LIMIT) + val searchResult = callWithErrorHandling { musicService.search(criteria) } + + // TODO Add More... button to categories + if (searchResult != null) { + searchResult.artists.map { artist -> + mediaItems.add( + artist.name ?: "", + listOf(MEDIA_ARTIST_ITEM, artist.id, artist.name).joinToString("|"), + FOLDER_TYPE_ARTISTS + ) + } + + searchResult.albums.map { album -> + mediaItems.add( + album.title ?: "", + listOf(MEDIA_ALBUM_ITEM, album.id, album.name) + .joinToString("|"), + FOLDER_TYPE_ALBUMS + ) + } + + searchSongsCache = searchResult.songs + searchResult.songs.map { song -> + mediaItems.add( + buildMediaItemFromTrack( + song, + listOf(MEDIA_SEARCH_SONG_ITEM, song.id).joinToString("|"), + isPlayable = true + ) + ) + } + } + return@future LibraryResult.ofItemList(mediaItems, null) + } + } + + @Suppress("MagicNumber", "ComplexMethod") private fun playFromMediaId(mediaId: String?) { Timber.d( @@ -180,132 +358,6 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - override fun onDestroy() { - super.onDestroy() - rxBusSubscription.dispose() - serviceJob.cancel() - - Timber.i("AutoMediaBrowserService onDestroy finished") - } - - override fun onGetRoot( - clientPackageName: String, - clientUid: Int, - rootHints: Bundle? - ): BrowserRoot { - Timber.d( - "AutoMediaBrowserService onGetRoot called. clientPackageName: %s; clientUid: %d", - clientPackageName, clientUid - ) - - val extras = Bundle() - extras.putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM - ) - extras.putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM - ) - extras.putBoolean( - MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true - ) - - return BrowserRoot(MEDIA_ROOT_ID, extras) - } - - @Suppress("ReturnCount", "ComplexMethod") - override fun onLoadChildren( - parentId: String, - result: Result> - ) { - Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId) - - val parentIdParts = parentId.split('|') - - when (parentIdParts.first()) { - MEDIA_ROOT_ID -> return getRootItems(result) - MEDIA_LIBRARY_ID -> return getLibrary(result) - MEDIA_ARTIST_ID -> return getArtists(result) - MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1]) - MEDIA_ALBUM_ID -> return getAlbums(result, AlbumListType.SORTED_BY_NAME) - MEDIA_ALBUM_PAGE_ID -> return getAlbums( - result, AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt() - ) - MEDIA_PLAYLIST_ID -> return getPlaylists(result) - MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(result, AlbumListType.FREQUENT) - MEDIA_ALBUM_NEWEST_ID -> return getAlbums(result, AlbumListType.NEWEST) - MEDIA_ALBUM_RECENT_ID -> return getAlbums(result, AlbumListType.RECENT) - MEDIA_ALBUM_RANDOM_ID -> return getAlbums(result, AlbumListType.RANDOM) - MEDIA_ALBUM_STARRED_ID -> return getAlbums(result, AlbumListType.STARRED) - MEDIA_SONG_RANDOM_ID -> return getRandomSongs(result) - MEDIA_SONG_STARRED_ID -> return getStarredSongs(result) - MEDIA_SHARE_ID -> return getShares(result) - MEDIA_BOOKMARK_ID -> return getBookmarks(result) - MEDIA_PODCAST_ID -> return getPodcasts(result) - MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2], result) - MEDIA_ARTIST_ITEM -> return getAlbumsForArtist( - result, parentIdParts[1], parentIdParts[2] - ) - MEDIA_ALBUM_ITEM -> return getSongsForAlbum(result, parentIdParts[1], parentIdParts[2]) - MEDIA_SHARE_ITEM -> return getSongsForShare(result, parentIdParts[1]) - MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(result, parentIdParts[1]) - else -> result.sendResult(mutableListOf()) - } - } - - override fun onSearch( - query: String, - extras: Bundle?, - result: Result> - ) { - Timber.d("AutoMediaBrowserService onSearch query: %s", query) - val mediaItems: MutableList = ArrayList() - result.detach() - - serviceScope.launch { - val criteria = SearchCriteria(query, SEARCH_LIMIT, SEARCH_LIMIT, SEARCH_LIMIT) - val searchResult = callWithErrorHandling { musicService.search(criteria) } - - // TODO Add More... button to categories - if (searchResult != null) { - searchResult.artists.map { artist -> - mediaItems.add( - artist.name ?: "", - listOf(MEDIA_ARTIST_ITEM, artist.id, artist.name).joinToString("|"), - null, - R.string.search_artists - ) - } - - searchResult.albums.map { album -> - mediaItems.add( - album.title ?: "", - listOf(MEDIA_ALBUM_ITEM, album.id, album.name) - .joinToString("|"), - null, - R.string.search_albums - ) - } - - searchSongsCache = searchResult.songs - searchResult.songs.map { song -> - mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SEARCH_SONG_ITEM, song.id).joinToString("|"), - R.string.search_songs - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE - ) - ) - } - } - result.sendResult(mediaItems) - } - } - private fun playSearch(id: String) { serviceScope.launch { // If there is no cache, we can't play the selected song. @@ -316,112 +368,108 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getRootItems(result: Result>) { - val mediaItems: MutableList = ArrayList() + private fun getRootItems(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() if (!isOffline) mediaItems.add( R.string.music_library_label, MEDIA_LIBRARY_ID, - R.drawable.ic_library, null ) mediaItems.add( R.string.main_artists_title, MEDIA_ARTIST_ID, - R.drawable.ic_artist, - null + null, + folderType = FOLDER_TYPE_ARTISTS ) if (!isOffline) mediaItems.add( R.string.main_albums_title, MEDIA_ALBUM_ID, - R.drawable.ic_menu_browse_dark, - null + null, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.playlist_label, MEDIA_PLAYLIST_ID, - R.drawable.ic_menu_playlists_dark, - null + null, + folderType = FOLDER_TYPE_PLAYLISTS ) - result.sendResult(mediaItems) + return Futures.immediateFuture(LibraryResult.ofItemList(mediaItems, null)) } - private fun getLibrary(result: Result>) { - val mediaItems: MutableList = ArrayList() + private fun getLibrary(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() // Songs mediaItems.add( R.string.main_songs_random, MEDIA_SONG_RANDOM_ID, - null, - R.string.main_songs_title + R.string.main_songs_title, + folderType = FOLDER_TYPE_TITLES ) mediaItems.add( R.string.main_songs_starred, MEDIA_SONG_STARRED_ID, - null, - R.string.main_songs_title + R.string.main_songs_title, + folderType = FOLDER_TYPE_TITLES ) // Albums mediaItems.add( R.string.main_albums_newest, MEDIA_ALBUM_NEWEST_ID, - null, R.string.main_albums_title ) mediaItems.add( R.string.main_albums_recent, MEDIA_ALBUM_RECENT_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.main_albums_frequent, MEDIA_ALBUM_FREQUENT_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.main_albums_random, MEDIA_ALBUM_RANDOM_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) mediaItems.add( R.string.main_albums_starred, MEDIA_ALBUM_STARRED_ID, - null, - R.string.main_albums_title + R.string.main_albums_title, + folderType = FOLDER_TYPE_ALBUMS ) // Other - mediaItems.add(R.string.button_bar_shares, MEDIA_SHARE_ID, null, null) - mediaItems.add(R.string.button_bar_bookmarks, MEDIA_BOOKMARK_ID, null, null) - mediaItems.add(R.string.button_bar_podcasts, MEDIA_PODCAST_ID, null, null) + mediaItems.add(R.string.button_bar_shares, MEDIA_SHARE_ID, null) + mediaItems.add(R.string.button_bar_bookmarks, MEDIA_BOOKMARK_ID, null) + mediaItems.add(R.string.button_bar_podcasts, MEDIA_PODCAST_ID, null) - result.sendResult(mediaItems) + return Futures.immediateFuture(LibraryResult.ofItemList(mediaItems, null)) } private fun getArtists( - result: Result>, section: String? = null - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val childMediaId: String var artists = if (!isOffline && useId3Tags) { childMediaId = MEDIA_ARTIST_ITEM @@ -452,7 +500,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.add( currentSection, listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"), - null + FOLDER_TYPE_ARTISTS ) } } @@ -461,23 +509,22 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.add( artist.name ?: "", listOf(childMediaId, artist.id, artist.name).joinToString("|"), - null + FOLDER_TYPE_ARTISTS ) } } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getAlbumsForArtist( - result: Result>, id: String, name: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { val albums = if (!isOffline && useId3Tags) { callWithErrorHandling { musicService.getArtist(id, name, false) } } else { @@ -491,22 +538,20 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) .joinToString("|"), - null + FOLDER_TYPE_ALBUMS ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getSongsForAlbum( - result: Result>, id: String, name: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val songs = listSongsInMusicService(id, name) if (songs != null) { @@ -520,43 +565,36 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { items.map { item -> if (item.isDirectory) mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - item, - listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE - ) + item.title ?: "", + listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|"), + FOLDER_TYPE_TITLES ) else mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - item, - listOf( - MEDIA_ALBUM_SONG_ITEM, - id, - name, - item.id - ).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + item, + listOf( + MEDIA_ALBUM_SONG_ITEM, + id, + name, + item.id + ).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getAlbums( - result: Result>, type: AlbumListType, page: Int? = null - ) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { val offset = (page ?: 0) * DISPLAY_LIMIT val albums = if (useId3Tags) { callWithErrorHandling { @@ -577,7 +615,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) .joinToString("|"), - null + FOLDER_TYPE_ALBUMS ) } @@ -585,41 +623,37 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { mediaItems.add( R.string.search_more, listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"), - R.drawable.ic_menu_forward_dark, null ) - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } - private fun getPlaylists(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getPlaylists(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val playlists = callWithErrorHandling { musicService.getPlaylists(true) } playlists?.map { playlist -> mediaItems.add( playlist.name, listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name) .joinToString("|"), - null + FOLDER_TYPE_PLAYLISTS ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getPlaylist( id: String, name: String, - result: Result> - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val content = callWithErrorHandling { musicService.getPlaylist(id, name) } if (content != null) { @@ -632,22 +666,20 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { playlistCache = content.getTracks() playlistCache!!.take(DISPLAY_LIMIT).map { item -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - item, - listOf( - MEDIA_PLAYLIST_SONG_ITEM, - id, - name, - item.id - ).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + item, + listOf( + MEDIA_PLAYLIST_SONG_ITEM, + id, + name, + item.id + ).joinToString("|"), + isPlayable = true ) ) } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -689,30 +721,28 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getPodcasts(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + private fun getPodcasts(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + + return serviceScope.future { val podcasts = callWithErrorHandling { musicService.getPodcastsChannels(false) } podcasts?.map { podcast -> mediaItems.add( podcast.title ?: "", listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"), - null + FOLDER_TYPE_MIXED ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getPodcastEpisodes( - result: Result>, id: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + return serviceScope.future { val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) } if (episodes != null) { @@ -721,18 +751,16 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { episodes.getTracks().map { episode -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - episode, - listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id) - .joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + episode, + listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id) + .joinToString("|"), + isPlayable = true ) ) } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -757,27 +785,24 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getBookmarks(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() - serviceScope.launch { + private fun getBookmarks(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() + return serviceScope.future { val bookmarks = callWithErrorHandling { musicService.getBookmarks() } if (bookmarks != null) { val songs = Util.getSongsFromBookmarks(bookmarks) songs.getTracks().map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|"), + isPlayable = true ) ) } - result.sendResult(mediaItems) } + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -792,11 +817,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getShares(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getShares(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val shares = callWithErrorHandling { musicService.getShares(false) } shares?.map { share -> @@ -804,21 +828,19 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { share.name ?: "", listOf(MEDIA_SHARE_ITEM, share.id) .joinToString("|"), - null + FOLDER_TYPE_MIXED ) } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } private fun getSongsForShare( - result: Result>, id: String - ) { - val mediaItems: MutableList = ArrayList() - result.detach() + ): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val shares = callWithErrorHandling { musicService.getShares(false) } val selectedShare = shares?.firstOrNull { share -> share.id == id } @@ -829,17 +851,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { selectedShare.getEntries().map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -864,11 +884,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getStarredSongs(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getStarredSongs(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val songs = listStarredSongsInMusicService() if (songs != null) { @@ -880,17 +899,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { starredSongsCache = items items.map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SONG_STARRED_ITEM, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_SONG_STARRED_ITEM, song.id).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -917,11 +934,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun getRandomSongs(result: Result>) { - val mediaItems: MutableList = ArrayList() - result.detach() + private fun getRandomSongs(): ListenableFuture>> { + val mediaItems: MutableList = ArrayList() - serviceScope.launch { + return serviceScope.future { val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) } if (songs != null) { @@ -933,17 +949,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { randomSongsCache = items items.map { song -> mediaItems.add( - MediaBrowserCompat.MediaItem( - Util.getMediaDescriptionForEntry( - song, - listOf(MEDIA_SONG_RANDOM_ITEM, song.id).joinToString("|") - ), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + buildMediaItemFromTrack( + song, + listOf(MEDIA_SONG_RANDOM_ITEM, song.id).joinToString("|"), + isPlayable = true ) ) } } - result.sendResult(mediaItems) + return@future LibraryResult.ofItemList(mediaItems, null) } } @@ -985,77 +999,47 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } } - private fun MutableList.add( + private fun MutableList.add( title: String, mediaId: String, - icon: Int?, - groupNameId: Int? = null + folderType: Int ) { - val builder = MediaDescriptionCompat.Builder() - builder.setTitle(title) - builder.setMediaId(mediaId) - if (icon != null) - builder.setIconUri(Util.getUriToDrawable(applicationContext, icon)) - - if (groupNameId != null) - builder.setExtras( - Bundle().apply { - putString( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - getString(groupNameId) - ) - } - ) - - val mediaItem = MediaBrowserCompat.MediaItem( - builder.build(), - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + val mediaItem = buildMediaItem( + title, + mediaId, + isPlayable = false, + folderType = folderType ) this.add(mediaItem) } - private fun MutableList.add( + private fun MutableList.add( resId: Int, mediaId: String, - icon: Int?, groupNameId: Int?, - browsable: Boolean = true + browsable: Boolean = true, + folderType: Int = FOLDER_TYPE_MIXED ) { - val builder = MediaDescriptionCompat.Builder() - builder.setTitle(getString(resId)) - builder.setMediaId(mediaId) + val applicationContext = UApp.applicationContext() - if (icon != null) - builder.setIconUri(Util.getUriToDrawable(applicationContext, icon)) - - if (groupNameId != null) - builder.setExtras( - Bundle().apply { - putString( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - getString(groupNameId) - ) - } - ) - - val mediaItem = MediaBrowserCompat.MediaItem( - builder.build(), - if (browsable) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE - else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + val mediaItem = buildMediaItem( + applicationContext.getString(resId), + mediaId, + isPlayable = false, + folderType = folderType ) this.add(mediaItem) } - private fun MutableList.addPlayAllItem( + private fun MutableList.addPlayAllItem( mediaId: String, ) { this.add( R.string.select_album_play_all, mediaId, - R.drawable.ic_stat_play_dark, null, false ) @@ -1098,4 +1082,52 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { null } } -} + + + private fun buildMediaItemFromTrack( + track: Track, + mediaId: String, + isPlayable: Boolean + ): MediaItem { + + return buildMediaItem( + title = track.title ?: "", + mediaId = mediaId, + isPlayable = isPlayable, + folderType = FOLDER_TYPE_NONE, + album = track.album, + artist = track.artist, + genre = track.genre, + ) + } + + @Suppress("LongParameterList") + private fun buildMediaItem( + title: String, + mediaId: String, + isPlayable: Boolean, + @MediaMetadata.FolderType folderType: Int, + album: String? = null, + artist: String? = null, + genre: String? = null, + sourceUri: Uri? = null, + imageUri: Uri? = null, + ): MediaItem { + val metadata = + MediaMetadata.Builder() + .setAlbumTitle(album) + .setTitle(title) + .setArtist(artist) + .setGenre(genre) + .setFolderType(folderType) + .setIsPlayable(isPlayable) + .setArtworkUri(imageUri) + .build() + return MediaItem.Builder() + .setMediaId(mediaId) + .setMediaMetadata(metadata) + .setUri(sourceUri) + .build() + } + +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index 8de2a107..4c42a441 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -30,7 +30,7 @@ class CachedDataSource( ) : BaseDataSource(false) { class Factory( - var upstreamDataSourceFactory: DataSource.Factory + private var upstreamDataSourceFactory: DataSource.Factory ) : DataSource.Factory { private var eventListener: EventListener? = null @@ -112,16 +112,16 @@ class CachedDataSource( } override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - if (cachePath != null) { + return if (cachePath != null) { try { - return readInternal(buffer, offset, length) + readInternal(buffer, offset, length) } catch (e: IOException) { throw HttpDataSource.HttpDataSourceException.createForIOException( e, Util.castNonNull(dataSpec), HttpDataSource.HttpDataSourceException.TYPE_READ ) } } else { - return upstreamDataSource.read(buffer, offset, length) + upstreamDataSource.read(buffer, offset, length) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 86a0483a..0afaccd2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -64,20 +64,6 @@ class LegacyPlaylistManager : KoinComponent { currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()] } - @Synchronized - fun clearIncomplete() { - val iterator = _playlist.iterator() - var changedPlaylist = false - while (iterator.hasNext()) { - val downloadFile = iterator.next() - if (!downloadFile.isCompleteFileAvailable) { - iterator.remove() - changedPlaylist = true - } - } - if (changedPlaylist) playlistUpdateRevision++ - } - @Synchronized fun clearPlaylist() { _playlist.clear() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt deleted file mode 100644 index d2f76e5f..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaItemTree.kt +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.moire.ultrasonic.playback - -import android.content.res.AssetManager -import android.net.Uri -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS -import com.google.common.collect.ImmutableList -import org.json.JSONObject - -/** - * A sample media catalog that represents media items as a tree. - * - * It fetched the data from {@code catalog.json}. The root's children are folders containing media - * items from the same album/artist/genre. - * - * Each app should have their own way of representing the tree. MediaItemTree is used for - * demonstration purpose only. - */ -object MediaItemTree { - private var treeNodes: MutableMap = mutableMapOf() - private var titleMap: MutableMap = mutableMapOf() - private var isInitialized = false - private const val ROOT_ID = "[rootID]" - private const val ALBUM_ID = "[albumID]" - private const val GENRE_ID = "[genreID]" - private const val ARTIST_ID = "[artistID]" - private const val ALBUM_PREFIX = "[album]" - private const val GENRE_PREFIX = "[genre]" - private const val ARTIST_PREFIX = "[artist]" - private const val ITEM_PREFIX = "[item]" - - private class MediaItemNode(val item: MediaItem) { - private val children: MutableList = ArrayList() - - fun addChild(childID: String) { - this.children.add(treeNodes[childID]!!.item) - } - - fun getChildren(): List { - return ImmutableList.copyOf(children) - } - } - - private fun buildMediaItem( - title: String, - mediaId: String, - isPlayable: Boolean, - @MediaMetadata.FolderType folderType: Int, - album: String? = null, - artist: String? = null, - genre: String? = null, - sourceUri: Uri? = null, - imageUri: Uri? = null, - ): MediaItem { - // TODO(b/194280027): add artwork - val metadata = - MediaMetadata.Builder() - .setAlbumTitle(album) - .setTitle(title) - .setArtist(artist) - .setGenre(genre) - .setFolderType(folderType) - .setIsPlayable(isPlayable) - .setArtworkUri(imageUri) - .build() - return MediaItem.Builder() - .setMediaId(mediaId) - .setMediaMetadata(metadata) - .setUri(sourceUri) - .build() - } - - fun initialize(assets: AssetManager) { - if (isInitialized) return - isInitialized = true - // create root and folders for album/artist/genre. - treeNodes[ROOT_ID] = - MediaItemNode( - buildMediaItem( - title = "Root Folder", - mediaId = ROOT_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[ALBUM_ID] = - MediaItemNode( - buildMediaItem( - title = "Album Folder", - mediaId = ALBUM_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[ARTIST_ID] = - MediaItemNode( - buildMediaItem( - title = "Artist Folder", - mediaId = ARTIST_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[GENRE_ID] = - MediaItemNode( - buildMediaItem( - title = "Genre Folder", - mediaId = GENRE_ID, - isPlayable = false, - folderType = FOLDER_TYPE_MIXED - ) - ) - treeNodes[ROOT_ID]!!.addChild(ALBUM_ID) - treeNodes[ROOT_ID]!!.addChild(ARTIST_ID) - treeNodes[ROOT_ID]!!.addChild(GENRE_ID) - - // Here, parse the json file in asset for media list. - // We use a file in asset for demo purpose -// val jsonObject = JSONObject(loadJSONFromAsset(assets)) -// val mediaList = jsonObject.getJSONArray("media") -// -// // create subfolder with same artist, album, etc. -// for (i in 0 until mediaList.length()) { -// addNodeToTree(mediaList.getJSONObject(i)) -// } - } - - private fun addNodeToTree(mediaObject: JSONObject) { - - val id = mediaObject.getString("id") - val album = mediaObject.getString("album") - val title = mediaObject.getString("title") - val artist = mediaObject.getString("artist") - val genre = mediaObject.getString("genre") - val sourceUri = Uri.parse(mediaObject.getString("source")) - val imageUri = Uri.parse(mediaObject.getString("image")) - // key of such items in tree - val idInTree = ITEM_PREFIX + id - val albumFolderIdInTree = ALBUM_PREFIX + album - val artistFolderIdInTree = ARTIST_PREFIX + artist - val genreFolderIdInTree = GENRE_PREFIX + genre - - treeNodes[idInTree] = - MediaItemNode( - buildMediaItem( - title = title, - mediaId = idInTree, - isPlayable = true, - album = album, - artist = artist, - genre = genre, - sourceUri = sourceUri, - imageUri = imageUri, - folderType = FOLDER_TYPE_NONE - ) - ) - - titleMap[title.lowercase()] = treeNodes[idInTree]!! - - if (!treeNodes.containsKey(albumFolderIdInTree)) { - treeNodes[albumFolderIdInTree] = - MediaItemNode( - buildMediaItem( - title = album, - mediaId = albumFolderIdInTree, - isPlayable = true, - folderType = FOLDER_TYPE_PLAYLISTS - ) - ) - treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree) - } - treeNodes[albumFolderIdInTree]!!.addChild(idInTree) - - // add into artist folder - if (!treeNodes.containsKey(artistFolderIdInTree)) { - treeNodes[artistFolderIdInTree] = - MediaItemNode( - buildMediaItem( - title = artist, - mediaId = artistFolderIdInTree, - isPlayable = true, - folderType = FOLDER_TYPE_PLAYLISTS - ) - ) - treeNodes[ARTIST_ID]!!.addChild(artistFolderIdInTree) - } - treeNodes[artistFolderIdInTree]!!.addChild(idInTree) - - // add into genre folder - if (!treeNodes.containsKey(genreFolderIdInTree)) { - treeNodes[genreFolderIdInTree] = - MediaItemNode( - buildMediaItem( - title = genre, - mediaId = genreFolderIdInTree, - isPlayable = true, - folderType = FOLDER_TYPE_PLAYLISTS - ) - ) - treeNodes[GENRE_ID]!!.addChild(genreFolderIdInTree) - } - treeNodes[genreFolderIdInTree]!!.addChild(idInTree) - } - - fun getItem(id: String): MediaItem? { - return treeNodes[id]?.item - } - - fun getRootItem(): MediaItem { - return treeNodes[ROOT_ID]!!.item - } - - fun getChildren(id: String): List? { - return treeNodes[id]?.getChildren() - } - - fun getRandomItem(): MediaItem { - var curRoot = getRootItem() - while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) { - val children = getChildren(curRoot.mediaId)!! - curRoot = children.random() - } - return curRoot - } - - fun getItemFromTitle(title: String): MediaItem? { - return titleMap[title]?.item - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 2eaaba9a..8ef86f09 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -50,7 +50,6 @@ internal class MediaNotificationProvider(context: Context) : context, NOTIFICATION_CHANNEL_ID ) - // TODO(b/193193926): Filter actions depending on the player's available commands. // Skip to previous action. builder.addAction( actionFactory.createMediaAction( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index bfad720a..2b3ad224 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -18,8 +18,6 @@ package org.moire.ultrasonic.playback import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent -import android.net.Uri -import android.os.Bundle import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.C.CONTENT_TYPE_MUSIC @@ -29,13 +27,8 @@ import androidx.media3.datasource.DataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession -import androidx.media3.session.SessionResult -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.activity.NavigationActivity @@ -48,94 +41,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var dataSourceFactory: DataSource.Factory - private val librarySessionCallback = CustomMediaLibrarySessionCallback() - - companion object { - private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" - private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" - } - - private inner class CustomMediaLibrarySessionCallback : - MediaLibrarySession.MediaLibrarySessionCallback { - override fun onGetLibraryRoot( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - params: LibraryParams? - ): ListenableFuture> { - return Futures.immediateFuture( - LibraryResult.ofItem( - MediaItemTree.getRootItem(), - params - ) - ) - } - - override fun onGetItem( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - mediaId: String - ): ListenableFuture> { - val item = - MediaItemTree.getItem(mediaId) - ?: return Futures.immediateFuture( - LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) - ) - return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) - } - - override fun onGetChildren( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - parentId: String, - page: Int, - pageSize: Int, - params: LibraryParams? - ): ListenableFuture>> { - val children = - MediaItemTree.getChildren(parentId) - ?: return Futures.immediateFuture( - LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) - ) - - return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) - } - - private fun setMediaItemFromSearchQuery(query: String) { - // Only accept query with pattern "play [Title]" or "[Title]" - // Where [Title]: must be exactly matched - // If no media with exact name found, play a random media instead - val mediaTitle = - if (query.startsWith("play ", ignoreCase = true)) { - query.drop(5) - } else { - query - } - - val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() - player.setMediaItem(item) - } - - override fun onSetMediaUri( - session: MediaSession, - controller: MediaSession.ControllerInfo, - uri: Uri, - extras: Bundle - ): Int { - - if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || - uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) - ) { - val searchQuery = - uri.getQueryParameter("query") - ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED - setMediaItemFromSearchQuery(searchQuery) - - return SessionResult.RESULT_SUCCESS - } else { - return SessionResult.RESULT_ERROR_NOT_SUPPORTED - } - } - } + private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback /* * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, @@ -148,11 +54,9 @@ class PlaybackService : MediaLibraryService(), KoinComponent { mediaItem: MediaItem ): MediaItem { // Again, set the Uri, so that it will get a LocalConfiguration - val item = mediaItem.buildUpon() + return mediaItem.buildUpon() .setUri(mediaItem.mediaMetadata.mediaUri) .build() - - return item } } @@ -202,9 +106,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // Enable audio offload player.experimentalSetOffloadSchedulingEnabled(true) - MediaItemTree.initialize(assets) + // Create browser interface + librarySessionCallback = AutoMediaBrowserCallback(player) - // THIS Will need to use the AutoCalls + // This will need to use the AutoCalls mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) .setMediaItemFiller(CustomMediaItemFiller()) .setSessionActivity(getPendingIntentForContent()) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 3cb70bd6..4cb69b04 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -64,7 +64,7 @@ class Downloader( private val rxBusSubscription: CompositeDisposable = CompositeDisposable() - var downloadChecker = object : Runnable { + private var downloadChecker = object : Runnable { override fun run() { try { Timber.w("Checking Downloads") @@ -399,11 +399,11 @@ class Downloader( val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0 needsDownloading = ( - downloadFile.desiredBitRate == 0 || - duration == null || - duration == 0 || - fileLength == 0L - ) + downloadFile.desiredBitRate == 0 || + duration == null || + duration == 0 || + fileLength == 0L + ) if (needsDownloading) { // Attempt partial HTTP GET, appending to the file if it exists. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index a6326cd3..75c926a1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -8,6 +8,7 @@ package org.moire.ultrasonic.service import android.content.Context import android.os.Handler +import android.os.Looper import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -145,7 +146,7 @@ class JukeboxMediaPlayer(private val downloader: Downloader) { private fun disableJukeboxOnError(x: Throwable, resourceId: Int) { Timber.w(x.toString()) val context = applicationContext() - Handler().post { toast(context, resourceId, false) } + Handler(Looper.getMainLooper()).post { toast(context, resourceId, false) } mediaPlayerControllerLazy.value.isJukeboxEnabled = false } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 77b55348..c41d6c85 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -66,12 +66,11 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (!created) return - // TODO -// playbackStateSerializer.serializeNow( -// downloader.getPlaylist(), -// downloader.currentPlayingIndex, -// mediaPlayerController.playerPosition -// ) + playbackStateSerializer.serializeNow( + mediaPlayerController.playList, + mediaPlayerController.currentMediaItemIndex, + mediaPlayerController.playerPosition + ) mediaPlayerController.clear(false) mediaButtonEventSubscription?.dispose() @@ -110,10 +109,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { val autoStart = keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || - keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || - keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || + keyCode == KeyEvent.KEYCODE_MEDIA_NEXT // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start onCreate(autoStart) { @@ -150,10 +149,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { return val autoStart = action == Constants.CMD_PLAY || - action == Constants.CMD_RESUME_OR_PLAY || - action == Constants.CMD_TOGGLEPAUSE || - action == Constants.CMD_PREVIOUS || - action == Constants.CMD_NEXT + action == Constants.CMD_RESUME_OR_PLAY || + action == Constants.CMD_TOGGLEPAUSE || + action == Constants.CMD_PREVIOUS || + action == Constants.CMD_NEXT // We can receive intents when everything is stopped, so we need to start onCreate(autoStart) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index edb1cea5..7115140a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -53,7 +53,7 @@ class PlaybackStateSerializer : KoinComponent { } } - private fun serializeNow( + fun serializeNow( songs: Iterable, currentPlayingIndex: Int, currentPlayingPosition: Int diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index febda154..109b8163 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -9,10 +9,8 @@ package org.moire.ultrasonic.util import android.annotation.SuppressLint import android.app.Activity -import android.app.PendingIntent import android.content.ContentResolver import android.content.Context -import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -28,18 +26,20 @@ import android.net.Uri import android.net.wifi.WifiManager import android.net.wifi.WifiManager.WifiLock import android.os.Build -import android.os.Bundle import android.os.Environment -import android.os.Parcelable -import android.support.v4.media.MediaDescriptionCompat import android.text.TextUtils import android.util.TypedValue import android.view.Gravity -import android.view.KeyEvent import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.annotation.AnyRes -import androidx.media.utils.MediaConstants +import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp.Companion.applicationContext +import org.moire.ultrasonic.domain.Bookmark +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.domain.Track +import timber.log.Timber import java.io.Closeable import java.io.UnsupportedEncodingException import java.security.MessageDigest @@ -49,15 +49,6 @@ import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt -import org.moire.ultrasonic.R -import org.moire.ultrasonic.app.UApp.Companion.applicationContext -import org.moire.ultrasonic.domain.Bookmark -import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.domain.PlayerState -import org.moire.ultrasonic.domain.SearchResult -import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.DownloadFile -import timber.log.Timber private const val LINE_LENGTH = 60 private const val DEGRADE_PRECISION_AFTER = 10 @@ -77,11 +68,6 @@ object Util { private var MEGA_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null private var KILO_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null private var BYTE_LOCALIZED_FORMAT: DecimalFormat? = null - private const val EVENT_META_CHANGED = "org.moire.ultrasonic.EVENT_META_CHANGED" - private const val EVENT_PLAYSTATE_CHANGED = "org.moire.ultrasonic.EVENT_PLAYSTATE_CHANGED" - private const val CM_AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged" - private const val CM_AVRCP_PLAYBACK_COMPLETE = "com.android.music.playbackcomplete" - private const val CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged" // Used by hexEncode() private val HEX_DIGITS = @@ -448,150 +434,6 @@ object Util { return musicDirectory } - /** - * Broadcasts the given song info as the new song being played. - */ - fun broadcastNewTrackInfo(context: Context, song: Track?) { - val intent = Intent(EVENT_META_CHANGED) - if (song != null) { - intent.putExtra("title", song.title) - intent.putExtra("artist", song.artist) - intent.putExtra("album", song.album) - val albumArtFile = FileUtil.getAlbumArtFile(song) - intent.putExtra("coverart", albumArtFile) - } else { - intent.putExtra("title", "") - intent.putExtra("artist", "") - intent.putExtra("album", "") - intent.putExtra("coverart", "") - } - context.sendBroadcast(intent) - } - - fun broadcastA2dpMetaDataChange( - context: Context, - playerPosition: Int, - currentPlaying: DownloadFile?, - listSize: Int, - id: Int - ) { - if (!Settings.shouldSendBluetoothNotifications) return - - var song: Track? = null - val avrcpIntent = Intent(CM_AVRCP_METADATA_CHANGED) - if (currentPlaying != null) song = currentPlaying.track - - fillIntent(avrcpIntent, song, playerPosition, id, listSize) - - context.sendBroadcast(avrcpIntent) - } - - @Suppress("LongParameterList") - fun broadcastA2dpPlayStatusChange( - context: Context, - state: PlayerState?, - newSong: Track?, - listSize: Int, - id: Int, - playerPosition: Int - ) { - if (!Settings.shouldSendBluetoothNotifications) return - - if (newSong != null) { - - val avrcpIntent = Intent( - if (state == PlayerState.COMPLETED) CM_AVRCP_PLAYBACK_COMPLETE - else CM_AVRCP_PLAYSTATE_CHANGED - ) - - fillIntent(avrcpIntent, newSong, playerPosition, id, listSize) - - if (state != PlayerState.COMPLETED) { - when (state) { - PlayerState.STARTED -> avrcpIntent.putExtra("playing", true) - PlayerState.STOPPED, - PlayerState.PAUSED -> avrcpIntent.putExtra("playing", false) - else -> return // No need to broadcast. - } - } - - context.sendBroadcast(avrcpIntent) - } - } - - private fun fillIntent( - intent: Intent, - song: Track?, - playerPosition: Int, - id: Int, - listSize: Int - ) { - if (song == null) { - intent.putExtra("track", "") - intent.putExtra("track_name", "") - intent.putExtra("artist", "") - intent.putExtra("artist_name", "") - intent.putExtra("album", "") - intent.putExtra("album_name", "") - intent.putExtra("album_artist", "") - intent.putExtra("album_artist_name", "") - - if (Settings.shouldSendBluetoothAlbumArt) { - intent.putExtra("coverart", null as Parcelable?) - intent.putExtra("cover", null as Parcelable?) - } - - intent.putExtra("ListSize", 0.toLong()) - intent.putExtra("id", 0.toLong()) - intent.putExtra("duration", 0.toLong()) - intent.putExtra("position", 0.toLong()) - } else { - val title = song.title - val artist = song.artist - val album = song.album - val duration = song.duration - - intent.putExtra("track", title) - intent.putExtra("track_name", title) - intent.putExtra("artist", artist) - intent.putExtra("artist_name", artist) - intent.putExtra("album", album) - intent.putExtra("album_name", album) - intent.putExtra("album_artist", artist) - intent.putExtra("album_artist_name", artist) - - if (Settings.shouldSendBluetoothAlbumArt) { - val albumArtFile = FileUtil.getAlbumArtFile(song) - intent.putExtra("coverart", albumArtFile) - intent.putExtra("cover", albumArtFile) - } - - intent.putExtra("position", playerPosition.toLong()) - intent.putExtra("id", id.toLong()) - intent.putExtra("ListSize", listSize.toLong()) - - if (duration != null) { - intent.putExtra("duration", duration.toLong()) - } - } - } - - /** - * - * Broadcasts the given player state as the one being set. - */ - fun broadcastPlaybackStatusChange(context: Context, state: PlayerState?) { - val intent = Intent(EVENT_PLAYSTATE_CHANGED) - when (state) { - PlayerState.STARTED -> intent.putExtra("state", "play") - PlayerState.STOPPED -> intent.putExtra("state", "stop") - PlayerState.PAUSED -> intent.putExtra("state", "pause") - PlayerState.COMPLETED -> intent.putExtra("state", "complete") - else -> return // No need to broadcast. - } - context.sendBroadcast(intent) - } - @JvmStatic @Suppress("MagicNumber") fun getNotificationImageSize(context: Context): Int { @@ -667,7 +509,7 @@ object Util { val hours = TimeUnit.MILLISECONDS.toHours(millis) val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours) val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) - - TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes) + TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes) return when { hours >= DEGRADE_PRECISION_AFTER -> { @@ -761,9 +603,9 @@ object Util { fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri { return Uri.parse( ContentResolver.SCHEME_ANDROID_RESOURCE + - "://" + context.resources.getResourcePackageName(drawableId) + - '/' + context.resources.getResourceTypeName(drawableId) + - '/' + context.resources.getResourceEntryName(drawableId) + "://" + context.resources.getResourcePackageName(drawableId) + + '/' + context.resources.getResourceTypeName(drawableId) + + '/' + context.resources.getResourceEntryName(drawableId) ) } @@ -776,39 +618,6 @@ object Util { var fileFormat: String?, ) - fun getMediaDescriptionForEntry( - song: Track, - mediaId: String? = null, - groupNameId: Int? = null - ): MediaDescriptionCompat { - - val descriptionBuilder = MediaDescriptionCompat.Builder() - val desc = readableEntryDescription(song) - val title: String - - if (groupNameId != null) - descriptionBuilder.setExtras( - Bundle().apply { - putString( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - appContext().getString(groupNameId) - ) - } - ) - - if (desc.trackNumber.isNotEmpty()) { - title = "${desc.trackNumber} - ${desc.title}" - } else { - title = desc.title - } - - descriptionBuilder.setTitle(title) - descriptionBuilder.setSubtitle(desc.artist) - descriptionBuilder.setMediaId(mediaId) - - return descriptionBuilder.build() - } - @Suppress("ComplexMethod", "LongMethod") fun readableEntryDescription(song: Track): ReadableEntryDescription { val artist = StringBuilder(LINE_LENGTH) @@ -834,8 +643,8 @@ object Util { if (artistName != null) { if (Settings.shouldDisplayBitrateWithArtist && ( - !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() - ) + !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() + ) ) { artist.append(artistName).append(" (").append( String.format( @@ -880,18 +689,6 @@ object Util { ) } - fun getPendingIntentForMediaAction( - context: Context, - keycode: Int, - requestCode: Int - ): PendingIntent { - val intent = Intent(Constants.CMD_PROCESS_KEYCODE) - val flags = PendingIntent.FLAG_UPDATE_CURRENT - intent.setPackage(context.packageName) - intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode)) - return PendingIntent.getBroadcast(context, requestCode, intent, flags) - } - fun getConnectivityManager(): ConnectivityManager { val context = appContext() return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager From 46fb7664c38306267eca7424e5e4736d5b21ee79 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 10:10:24 +0200 Subject: [PATCH 10/67] Fix insertAfterCurrent, Fix getUri --- .../ultrasonic/playback/APIDataSource.kt | 2 ++ .../playback/AutoMediaBrowserCallback.kt | 11 +++----- .../ultrasonic/playback/CachedDataSource.kt | 7 ++++- .../moire/ultrasonic/service/Downloader.kt | 10 +++---- .../service/MediaPlayerController.kt | 2 +- .../service/MediaPlayerLifecycleSupport.kt | 16 ++++++------ .../ultrasonic/subsonic/DownloadHandler.kt | 2 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 26 +++++++++---------- 8 files changed, 40 insertions(+), 36 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt index 6ab75ca3..a2747301 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -138,6 +138,8 @@ open class APIDataSource private constructor( val bitrate = components[1].toInt() Timber.i("DATASOURCE: %s", "Start") + // FIXME + // WRONG API CLIENT val request = subsonicAPIClient.api.stream(id, bitrate, offset = 0) val response: retrofit2.Response? val streamResponse: StreamResponse diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index c18c3193..2dc81682 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -107,7 +107,6 @@ class AutoMediaBrowserCallback(var player: Player) : private val useId3Tags get() = Settings.shouldUseId3Tags private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId - /** * Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link * MediaBrowser#getLibraryRoot(LibraryParams)}. @@ -162,7 +161,9 @@ class AutoMediaBrowserCallback(var player: Player) : ): ListenableFuture> { playFromMediaId(mediaId) - // TODO: Later + // FIXME: + // Create LRU Cache of MediaItems, fill it in the other calls + // and retrieve it here. return Futures.immediateFuture( LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) ) @@ -215,7 +216,6 @@ class AutoMediaBrowserCallback(var player: Player) : } } - @Suppress("ReturnCount", "ComplexMethod") fun onLoadChildren( parentId: String, @@ -300,7 +300,6 @@ class AutoMediaBrowserCallback(var player: Player) : } } - @Suppress("MagicNumber", "ComplexMethod") private fun playFromMediaId(mediaId: String?) { Timber.d( @@ -1083,7 +1082,6 @@ class AutoMediaBrowserCallback(var player: Player) : } } - private fun buildMediaItemFromTrack( track: Track, mediaId: String, @@ -1129,5 +1127,4 @@ class AutoMediaBrowserCallback(var player: Player) : .setUri(sourceUri) .build() } - -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index 4c42a441..e79e66dd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -147,8 +147,12 @@ class CachedDataSource( return read } + /* + * This method is called by StatsDataSource to verify that the loading succeeded, + * so its important that we return the correct value here.. + */ override fun getUri(): Uri? { - return cachePath?.toUri() + return cachePath?.toUri() ?: upstreamDataSource.uri } override fun close() { @@ -174,6 +178,7 @@ class CachedDataSource( if (!found) return -1 cachePath = filePath + openedFile = true cacheFile = Storage.getFromPath(filePath)!! responseByteStream = cacheFile!!.getFileInputStream() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 4cb69b04..30e75bf9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -399,11 +399,11 @@ class Downloader( val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0 needsDownloading = ( - downloadFile.desiredBitRate == 0 || - duration == null || - duration == 0 || - fileLength == 0L - ) + downloadFile.desiredBitRate == 0 || + duration == null || + duration == 0 || + fileLength == 0L + ) if (needsDownloading) { // Attempt partial HTTP GET, appending to the file if it exists. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 766da850..758b78c4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -366,7 +366,7 @@ class MediaPlayerController( when (insertionMode) { InsertionMode.CLEAR -> clear() InsertionMode.APPEND -> insertAt = mediaItemCount - InsertionMode.AFTER_CURRENT -> insertAt = currentMediaItemIndex + InsertionMode.AFTER_CURRENT -> insertAt = currentMediaItemIndex + 1 } val mediaItems: List = songs.map { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index c41d6c85..586762ce 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -109,10 +109,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { val autoStart = keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || - keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || - keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || + keyCode == KeyEvent.KEYCODE_MEDIA_NEXT // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start onCreate(autoStart) { @@ -149,10 +149,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { return val autoStart = action == Constants.CMD_PLAY || - action == Constants.CMD_RESUME_OR_PLAY || - action == Constants.CMD_TOGGLEPAUSE || - action == Constants.CMD_PREVIOUS || - action == Constants.CMD_NEXT + action == Constants.CMD_RESUME_OR_PLAY || + action == Constants.CMD_TOGGLEPAUSE || + action == Constants.CMD_PREVIOUS || + action == Constants.CMD_NEXT // We can receive intents when everything is stopped, so we need to start onCreate(autoStart) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index e02b134c..3165b58b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -39,8 +39,8 @@ class DownloadHandler( val onValid = Runnable { // TODO: The logic here is different than in the controller... val insertionMode = when { - append -> MediaPlayerController.InsertionMode.APPEND playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT + append -> MediaPlayerController.InsertionMode.APPEND else -> MediaPlayerController.InsertionMode.CLEAR } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 109b8163..f3e16d44 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -33,13 +33,6 @@ import android.view.Gravity import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.annotation.AnyRes -import org.moire.ultrasonic.R -import org.moire.ultrasonic.app.UApp.Companion.applicationContext -import org.moire.ultrasonic.domain.Bookmark -import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.domain.SearchResult -import org.moire.ultrasonic.domain.Track -import timber.log.Timber import java.io.Closeable import java.io.UnsupportedEncodingException import java.security.MessageDigest @@ -49,6 +42,13 @@ import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt +import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp.Companion.applicationContext +import org.moire.ultrasonic.domain.Bookmark +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.domain.Track +import timber.log.Timber private const val LINE_LENGTH = 60 private const val DEGRADE_PRECISION_AFTER = 10 @@ -509,7 +509,7 @@ object Util { val hours = TimeUnit.MILLISECONDS.toHours(millis) val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours) val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) - - TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes) + TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes) return when { hours >= DEGRADE_PRECISION_AFTER -> { @@ -603,9 +603,9 @@ object Util { fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri { return Uri.parse( ContentResolver.SCHEME_ANDROID_RESOURCE + - "://" + context.resources.getResourcePackageName(drawableId) + - '/' + context.resources.getResourceTypeName(drawableId) + - '/' + context.resources.getResourceEntryName(drawableId) + "://" + context.resources.getResourcePackageName(drawableId) + + '/' + context.resources.getResourceTypeName(drawableId) + + '/' + context.resources.getResourceEntryName(drawableId) ) } @@ -643,8 +643,8 @@ object Util { if (artistName != null) { if (Settings.shouldDisplayBitrateWithArtist && ( - !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() - ) + !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() + ) ) { artist.append(artistName).append(" (").append( String.format( From 1a69507e3415e8df1d7db4f701b639bb2748c8a3 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 10:21:46 +0200 Subject: [PATCH 11/67] Remove a bunch of now unused prefs --- ultrasonic/src/main/AndroidManifest.xml | 13 --- .../receiver/BluetoothIntentReceiver.java | 100 ------------------ .../ultrasonic/fragment/SettingsFragment.kt | 73 ------------- .../service/MediaPlayerController.kt | 6 -- .../org/moire/ultrasonic/util/Constants.kt | 6 -- .../org/moire/ultrasonic/util/Settings.kt | 38 ------- ultrasonic/src/main/res/xml/settings.xml | 26 ----- 7 files changed, 262 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 757d335f..7e4b433e 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -71,11 +71,6 @@ - - - - - @@ -88,14 +83,6 @@ - - - - - - - - diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java deleted file mode 100644 index cf7844e7..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - 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 . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.receiver; - -import android.annotation.SuppressLint; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.Settings; - -import timber.log.Timber; - -/** - * Resume or pause playback on Bluetooth A2DP connect/disconnect. - * - * @author Sindre Mehus - */ -@SuppressLint("MissingPermission") -public class BluetoothIntentReceiver extends BroadcastReceiver -{ - @Override - public void onReceive(Context context, Intent intent) - { - int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - String action = intent.getAction(); - String name = device != null ? device.getName() : "Unknown"; - String address = device != null ? device.getAddress() : "Unknown"; - - Timber.d("A2DP State: %d; Action: %s; Device: %s; Address: %s", state, action, name, address); - - boolean actionBluetoothDeviceConnected = false; - boolean actionBluetoothDeviceDisconnected = false; - boolean actionA2dpConnected = false; - boolean actionA2dpDisconnected = false; - - if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) - { - actionBluetoothDeviceConnected = true; - } - else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action) || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED.equals(action)) - { - actionBluetoothDeviceDisconnected = true; - } - - if (state == android.bluetooth.BluetoothA2dp.STATE_CONNECTED) actionA2dpConnected = true; - else if (state == android.bluetooth.BluetoothA2dp.STATE_DISCONNECTED) actionA2dpDisconnected = true; - - boolean resume = false; - boolean pause = false; - - switch (Settings.getResumeOnBluetoothDevice()) - { - case Constants.PREFERENCE_VALUE_ALL: resume = actionA2dpConnected || actionBluetoothDeviceConnected; - break; - case Constants.PREFERENCE_VALUE_A2DP: resume = actionA2dpConnected; - break; - } - - switch (Settings.getPauseOnBluetoothDevice()) - { - case Constants.PREFERENCE_VALUE_ALL: pause = actionA2dpDisconnected || actionBluetoothDeviceDisconnected; - break; - case Constants.PREFERENCE_VALUE_A2DP: pause = actionA2dpDisconnected; - break; - } - - if (resume) - { - Timber.i("Connected to Bluetooth device %s address %s, resuming playback.", name, address); - context.sendBroadcast(new Intent(Constants.CMD_RESUME_OR_PLAY).setPackage(context.getPackageName())); - } - - if (pause) - { - Timber.i("Disconnected from Bluetooth device %s address %s, requesting pause.", name, address); - context.sendBroadcast(new Intent(Constants.CMD_PAUSE).setPackage(context.getPackageName())); - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 061d6f13..d7043da0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -12,7 +12,6 @@ import android.os.Bundle import android.provider.DocumentsContract import android.provider.SearchRecentSuggestions import android.view.View -import androidx.annotation.StringRes import androidx.fragment.app.DialogFragment import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference @@ -83,8 +82,6 @@ class SettingsFragment : private var sharingDefaultDescription: EditTextPreference? = null private var sharingDefaultGreeting: EditTextPreference? = null private var sharingDefaultExpiration: TimeSpanPreference? = null - private var resumeOnBluetoothDevice: Preference? = null - private var pauseOnBluetoothDevice: Preference? = null private var debugLogToFile: CheckBoxPreference? = null private var customCacheLocation: CheckBoxPreference? = null @@ -124,9 +121,6 @@ class SettingsFragment : sharingDefaultGreeting = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING) sharingDefaultExpiration = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION) - resumeOnBluetoothDevice = - findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE) - pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE) debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE) showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE) customCacheLocation = findPreference(Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION) @@ -134,7 +128,6 @@ class SettingsFragment : sharingDefaultGreeting!!.text = shareGreeting setupClearSearchPreference() setupCacheLocationPreference() - setupBluetoothDevicePreferences() // After API26 foreground services must be used for music playback, and they must have a notification if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -286,72 +279,6 @@ class SettingsFragment : startActivityForResult(intent, SELECT_CACHE_ACTIVITY) } - private fun setupBluetoothDevicePreferences() { - val resumeSetting = Settings.resumeOnBluetoothDevice - val pauseSetting = Settings.pauseOnBluetoothDevice - resumeOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(resumeSetting) - pauseOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(pauseSetting) - resumeOnBluetoothDevice!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - showBluetoothDevicePreferenceDialog( - R.string.settings_playback_resume_on_bluetooth_device, - Settings.resumeOnBluetoothDevice - ) { choice: Int -> - val editor = resumeOnBluetoothDevice!!.sharedPreferences.edit() - editor.putInt(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE, choice) - editor.apply() - resumeOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(choice) - } - true - } - pauseOnBluetoothDevice!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - showBluetoothDevicePreferenceDialog( - R.string.settings_playback_pause_on_bluetooth_device, - Settings.pauseOnBluetoothDevice - ) { choice: Int -> - Settings.pauseOnBluetoothDevice = choice - pauseOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(choice) - } - true - } - } - - private fun showBluetoothDevicePreferenceDialog( - @StringRes title: Int, - defaultChoice: Int, - onChosen: (Int) -> Unit - ) { - val choice = intArrayOf(defaultChoice) - AlertDialog.Builder(activity).setTitle(title) - .setSingleChoiceItems( - R.array.bluetoothDeviceSettingNames, defaultChoice - ) { _: DialogInterface?, i: Int -> choice[0] = i } - .setNegativeButton(R.string.common_cancel) { dialogInterface: DialogInterface, _: Int -> - dialogInterface.cancel() - } - .setPositiveButton(R.string.common_ok) { dialogInterface: DialogInterface, _: Int -> - onChosen(choice[0]) - dialogInterface.dismiss() - } - .create().show() - } - - private fun bluetoothDevicePreferenceToString(preferenceValue: Int): String { - return when (preferenceValue) { - Constants.PREFERENCE_VALUE_ALL -> { - getString(R.string.settings_playback_bluetooth_all) - } - Constants.PREFERENCE_VALUE_A2DP -> { - getString(R.string.settings_playback_bluetooth_a2dp) - } - Constants.PREFERENCE_VALUE_DISABLED -> { - getString(R.string.settings_playback_bluetooth_disabled) - } - else -> "" - } - } - private fun setupClearSearchPreference() { val clearSearchPreference = findPreference(Constants.PREFERENCES_KEY_CLEAR_SEARCH_HISTORY) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 758b78c4..66f36cc4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -180,12 +180,6 @@ class MediaPlayerController( } } } - - // Playback has ended... - if (mediaItem == null && Settings.shouldClearPlaylist) { - clear(true) - jukeboxMediaPlayer.updatePlaylist() - } } private fun publishPlaybackState() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index 0f22d0c4..8556eae4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -95,13 +95,10 @@ object Constants { const val PREFERENCES_KEY_TEMP_LOSS = "tempLoss" const val PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval" const val PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime" - const val PREFERENCES_KEY_CLEAR_PLAYLIST = "clearPlaylist" const val PREFERENCES_KEY_CLEAR_BOOKMARK = "clearBookmark" const val PREFERENCES_KEY_DISC_SORT = "discAndTrackSort" const val PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS = "sendBluetoothNotifications" const val PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART = "sendBluetoothAlbumArt" - const val PREFERENCES_KEY_DISABLE_SEND_NOW_PLAYING_LIST = "disableNowPlayingListSending" - const val PREFERENCES_KEY_VIEW_REFRESH = "viewRefresh" const val PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS = "sharingAlwaysAskForDetails" const val PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION = "sharingDefaultDescription" const val PREFERENCES_KEY_DEFAULT_SHARE_GREETING = "sharingDefaultGreeting" @@ -110,9 +107,6 @@ object Constants { const val PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating" const val PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory" const val PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted" - const val PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE = "resumeOnBluetoothDevice" - const val PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE = "pauseOnBluetoothDevice" - const val PREFERENCES_KEY_SINGLE_BUTTON_PLAY_PAUSE = "singleButtonPlayPause" const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile" const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage" const val PREFERENCE_VALUE_ALL = 0 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index e5a0c4d5..90bba48e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -124,10 +124,6 @@ object Settings { var defaultArtists by StringIntSetting(Constants.PREFERENCES_KEY_DEFAULT_ARTISTS, "3") - @JvmStatic - var bufferLength - by StringIntSetting(Constants.PREFERENCES_KEY_BUFFER_LENGTH, "5") - @JvmStatic var incrementTime by StringIntSetting(Constants.PREFERENCES_KEY_INCREMENT_TIME, "5") @@ -154,9 +150,6 @@ object Settings { var shouldUseId3Tags by BooleanSetting(Constants.PREFERENCES_KEY_ID3_TAGS, false) - @JvmStatic - var tempLoss by StringIntSetting(Constants.PREFERENCES_KEY_TEMP_LOSS, "1") - var activeServer by IntSetting(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1) var serverScaling by BooleanSetting(Constants.PREFERENCES_KEY_SERVER_SCALING, false) @@ -184,37 +177,18 @@ object Settings { "300" ) - var shouldClearPlaylist - by BooleanSetting(Constants.PREFERENCES_KEY_CLEAR_PLAYLIST, false) - var shouldSortByDisc by BooleanSetting(Constants.PREFERENCES_KEY_DISC_SORT, false) var shouldClearBookmark by BooleanSetting(Constants.PREFERENCES_KEY_CLEAR_BOOKMARK, false) - var singleButtonPlayPause - by BooleanSetting( - Constants.PREFERENCES_KEY_SINGLE_BUTTON_PLAY_PAUSE, - false - ) - // Inverted for readability var shouldSendBluetoothNotifications by BooleanSetting( Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS, true ) - var shouldSendBluetoothAlbumArt - by BooleanSetting(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART, true) - - var shouldDisableNowPlayingListSending - by BooleanSetting(Constants.PREFERENCES_KEY_DISABLE_SEND_NOW_PLAYING_LIST, false) - - @JvmStatic - var viewRefreshInterval - by StringIntSetting(Constants.PREFERENCES_KEY_VIEW_REFRESH, "1000") - var shouldAskForShareDetails by BooleanSetting(Constants.PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS, true) @@ -257,18 +231,6 @@ object Settings { return 0 } - @JvmStatic - var resumeOnBluetoothDevice by IntSetting( - Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE, - Constants.PREFERENCE_VALUE_DISABLED - ) - - @JvmStatic - var pauseOnBluetoothDevice by IntSetting( - Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE, - Constants.PREFERENCE_VALUE_A2DP - ) - @JvmStatic var debugLogToFile by BooleanSetting(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE, false) diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 8872733b..dfd7bb98 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -77,12 +77,6 @@ a:summary="@string/settings.download_transition_summary" a:title="@string/settings.download_transition" app:iconSpaceReserved="false"/> - - - - - Date: Tue, 5 Apr 2022 20:58:11 +0200 Subject: [PATCH 12/67] Add pref switch for HW offload --- .../org/moire/ultrasonic/playback/PlaybackService.kt | 7 +++++-- .../src/main/kotlin/org/moire/ultrasonic/util/Constants.kt | 5 +---- .../src/main/kotlin/org/moire/ultrasonic/util/Settings.kt | 2 ++ ultrasonic/src/main/res/values/strings.xml | 4 ++++ ultrasonic/src/main/res/xml/settings.xml | 6 ++++++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 2b3ad224..03f556c5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -35,6 +35,7 @@ import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var player: ExoPlayer @@ -92,7 +93,8 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // Create a renderer with HW rendering support val renderer = DefaultRenderersFactory(this) - renderer.setEnableAudioOffload(true) + + if (Settings.useHwOffload) renderer.setEnableAudioOffload(true) // Create the player player = ExoPlayer.Builder(this) @@ -104,7 +106,8 @@ class PlaybackService : MediaLibraryService(), KoinComponent { .build() // Enable audio offload - player.experimentalSetOffloadSchedulingEnabled(true) + if (Settings.useHwOffload) + player.experimentalSetOffloadSchedulingEnabled(true) // Create browser interface librarySessionCallback = AutoMediaBrowserCallback(player) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index 8556eae4..6fb9efeb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -71,7 +71,6 @@ object Constants { const val PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons" const val PREFERENCES_KEY_SCROBBLE = "scrobble" const val PREFERENCES_KEY_SERVER_SCALING = "serverScaling" - const val PREFERENCES_KEY_REPEAT_MODE = "repeatMode" const val PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload" const val PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength" const val PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout" @@ -105,13 +104,11 @@ object Constants { const val PREFERENCES_KEY_SHARE_ON_SERVER = "sharingCreateOnServer" const val PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION = "sharingDefaultExpiration" const val PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating" + const val PREFERENCES_KEY_HARDWARE_OFFLOAD = "use_hw_offload" const val PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory" const val PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted" const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile" const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage" - const val PREFERENCE_VALUE_ALL = 0 - const val PREFERENCE_VALUE_A2DP = 1 - const val PREFERENCE_VALUE_DISABLED = 2 const val FILENAME_PLAYLIST_SER = "downloadstate.ser" const val ALBUM_ART_FILE = "folder.jpeg" const val STARRED = "starred" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 90bba48e..58458f9e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -243,6 +243,8 @@ object Settings { var useFiveStarRating by BooleanSetting(Constants.PREFERENCES_KEY_USE_FIVE_STAR_RATING, false) + var useHwOffload by BooleanSetting(Constants.PREFERENCES_KEY_HARDWARE_OFFLOAD, false) + // TODO: Remove in December 2022 fun migrateFeatureStorage() { val sp = appContext.getSharedPreferences("feature_flags", Context.MODE_PRIVATE) diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index e17ed742..b1b76090 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -484,4 +484,8 @@ Use five star rating for songs Use five star rating system for songs instead of simply starring/unstarring items. + Use hardware playback (experimental) + Try to play the media using the media decoder chip on your phone. This can improve battery usage. + + diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index dfd7bb98..c03bdc62 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -96,6 +96,12 @@ a:summary="@string/settings.five_star_rating_description" a:title="@string/settings.five_star_rating_title" app:iconSpaceReserved="false" /> + Date: Tue, 5 Apr 2022 21:41:27 +0200 Subject: [PATCH 13/67] Use grayed out shuffle and repeat buttons. Fix removing and resorting playlist. --- .../ultrasonic/fragment/PlayerFragment.kt | 176 ++++++++++-------- .../service/MediaPlayerController.kt | 9 +- ultrasonic/src/main/res/values/strings.xml | 2 + 3 files changed, 110 insertions(+), 77 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index d1e4d95b..3d3254b3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -149,6 +149,7 @@ class PlayerFragment : private lateinit var pauseButton: View private lateinit var stopButton: View private lateinit var playButton: View + private lateinit var shuffleButton: View private lateinit var repeatButton: ImageView private lateinit var hollowStar: Drawable private lateinit var fullStar: Drawable @@ -219,7 +220,10 @@ class PlayerFragment : findViews(view) val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous) val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next) - val shuffleButton = view.findViewById(R.id.button_shuffle) + shuffleButton = view.findViewById(R.id.button_shuffle) + updateShuffleButtonState(mediaPlayerController.isShufflePlayEnabled) + updateRepeatButtonState(mediaPlayerController.repeatMode) + val ratingLinearLayout = view.findViewById(R.id.song_rating) if (!useFiveStarRating) ratingLinearLayout.isVisible = false hollowStar = Util.getDrawableFromAttribute(view.context, R.attr.star_hollow) @@ -293,8 +297,7 @@ class PlayerFragment : } shuffleButton.setOnClickListener { - mediaPlayerController.toggleShuffle() - Util.toast(activity, R.string.download_menu_shuffle_notification) + toggleShuffle() } repeatButton.setOnClickListener { @@ -417,6 +420,57 @@ class PlayerFragment : view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) } } + private fun updateShuffleButtonState(isEnabled: Boolean) { + if (isEnabled) { + shuffleButton.alpha = 1f + } else { + shuffleButton.alpha = 0.6f + } + } + + private fun updateRepeatButtonState(repeatMode: Int) { + when (repeatMode) { + 0 -> { + repeatButton.setImageDrawable( + Util.getDrawableFromAttribute( + requireContext(), R.attr.media_repeat_off + ) + ) + shuffleButton.alpha = 0.6f + } + 1 -> { + repeatButton.setImageDrawable( + Util.getDrawableFromAttribute( + requireContext(), R.attr.media_repeat_single + ) + ) + shuffleButton.alpha = 1f + } + 2 -> { + repeatButton.setImageDrawable( + Util.getDrawableFromAttribute( + requireContext(), R.attr.media_repeat_all + ) + ) + shuffleButton.alpha = 1f + } + else -> { + } + } + } + + private fun toggleShuffle() { + val isEnabled = mediaPlayerController.toggleShuffle() + + if (isEnabled) { + Util.toast(activity, R.string.download_menu_shuffle_on) + } else { + Util.toast(activity, R.string.download_menu_shuffle_off) + } + + updateShuffleButtonState(isEnabled) + } + override fun onResume() { super.onResume() if (mediaPlayerController.currentPlayingLegacy == null) { @@ -621,7 +675,6 @@ class PlayerFragment : return true } R.id.menu_remove -> { - mediaPlayerController.removeFromPlaylist(song!!) onPlaylistChanged() return true } @@ -637,8 +690,7 @@ class PlayerFragment : return true } R.id.menu_shuffle -> { - mediaPlayerController.toggleShuffle() - Util.toast(context, R.string.download_menu_shuffle_notification) + toggleShuffle() return true } R.id.menu_item_equalizer -> { @@ -867,63 +919,59 @@ class PlayerFragment : ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { - val from = viewHolder.bindingAdapterPosition - val to = target.bindingAdapterPosition + val from = viewHolder.bindingAdapterPosition + val to = target.bindingAdapterPosition - // Move it in the data set - mediaPlayerController.moveItemInPlaylist(from, to) - viewAdapter.submitList(mediaPlayerController.playList) + // Move it in the data set + mediaPlayerController.moveItemInPlaylist(from, to) + return true + } - return true - } + // Swipe to delete from playlist + @SuppressLint("NotifyDataSetChanged") + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val pos = viewHolder.bindingAdapterPosition + val item = mediaPlayerController.controller?.getMediaItemAt(pos) + mediaPlayerController.removeFromPlaylist(pos) - // Swipe to delete from playlist - @SuppressLint("NotifyDataSetChanged") - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - val pos = viewHolder.bindingAdapterPosition - val file = mediaPlayerController.playList[pos] - mediaPlayerController.removeFromPlaylist(file) + val songRemoved = String.format( + resources.getString(R.string.download_song_removed), + item?.mediaMetadata?.title + ) - val songRemoved = String.format( - resources.getString(R.string.download_song_removed), - file.track.title - ) - Util.toast(context, songRemoved) + Util.toast(context, songRemoved) + } - viewAdapter.submitList(mediaPlayerController.playList) - viewAdapter.notifyDataSetChanged() - } + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) - override fun onSelectedChanged( - viewHolder: RecyclerView.ViewHolder?, - actionState: Int - ) { - super.onSelectedChanged(viewHolder, actionState) - - if (actionState == ACTION_STATE_DRAG) { - viewHolder?.itemView?.alpha = 0.6f - } - } - - override fun clearView( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ) { - super.clearView(recyclerView, viewHolder) - - viewHolder.itemView.alpha = 1.0f - } - - override fun isLongPressDragEnabled(): Boolean { - return false + if (actionState == ACTION_STATE_DRAG) { + viewHolder?.itemView?.alpha = 0.6f } } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + + viewHolder.itemView.alpha = 1.0f + } + + override fun isLongPressDragEnabled(): Boolean { + return false + } + } ) dragTouchHelper.attachToRecyclerView(playlistView) @@ -938,25 +986,7 @@ class PlayerFragment : emptyTextView.isVisible = list.isEmpty() - when (mediaPlayerController.repeatMode) { - 0 -> repeatButton.setImageDrawable( - Util.getDrawableFromAttribute( - requireContext(), R.attr.media_repeat_off - ) - ) - 1 -> repeatButton.setImageDrawable( - Util.getDrawableFromAttribute( - requireContext(), R.attr.media_repeat_single - ) - ) - 2 -> repeatButton.setImageDrawable( - Util.getDrawableFromAttribute( - requireContext(), R.attr.media_repeat_all - ) - ) - else -> { - } - } + updateRepeatButtonState(mediaPlayerController.repeatMode) } private fun onCurrentChanged() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 66f36cc4..8a863ff6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -419,8 +419,9 @@ class MediaPlayerController( } @Synchronized - fun toggleShuffle() { + fun toggleShuffle(): Boolean { isShufflePlayEnabled = !isShufflePlayEnabled + return isShufflePlayEnabled } val bufferedPercentage: Int @@ -475,9 +476,9 @@ class MediaPlayerController( } @Synchronized - // FIXME - // With the new API we can only remove by index!! - fun removeFromPlaylist(downloadFile: DownloadFile) { + fun removeFromPlaylist(position: Int) { + + controller?.removeMediaItem(position) playbackStateSerializer.serialize( legacyPlaylistManager.playlist, diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index b1b76090..3025eb13 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -75,6 +75,8 @@ Screen On Show Album Shuffle + Shuffle mode enabled + Shuffle mode disabled Playlist was shuffled Visualizer Buffering From 4c22c8b41b9fbc1ed905b3802f38b34ba2c9afbb Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 21:56:13 +0200 Subject: [PATCH 14/67] Formating / detekt --- .../org/moire/ultrasonic/fragment/PlayerFragment.kt | 6 +++--- .../ultrasonic/playback/AutoMediaBrowserCallback.kt | 4 ++-- .../ultrasonic/service/MediaPlayerController.kt | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 3d3254b3..08b4fb1a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -914,11 +914,10 @@ class PlayerFragment : } ) - dragTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( + val callback = object : ItemTouchHelper.SimpleCallback( ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ) { - override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, @@ -972,7 +971,8 @@ class PlayerFragment : return false } } - ) + + dragTouchHelper = ItemTouchHelper(callback) dragTouchHelper.attachToRecyclerView(playlistView) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index 2dc81682..e2abb257 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -87,7 +87,7 @@ private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" /** * MediaBrowserService implementation for e.g. Android Auto */ -@Suppress("TooManyFunctions", "LargeClass") +@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") class AutoMediaBrowserCallback(var player: Player) : MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback, KoinComponent { @@ -161,7 +161,7 @@ class AutoMediaBrowserCallback(var player: Player) : ): ListenableFuture> { playFromMediaId(mediaId) - // FIXME: + // TODO: // Create LRU Cache of MediaItems, fill it in the other calls // and retrieve it here. return Futures.immediateFuture( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 8a863ff6..ec866abb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -100,7 +100,7 @@ class MediaPlayerController( } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - onTrackCompleted(mediaItem) + onTrackCompleted() legacyPlaylistManager.updateCurrentPlaying(mediaItem) publishPlaybackState() } @@ -167,7 +167,7 @@ class MediaPlayerController( Timber.d("Processed player state change") } - private fun onTrackCompleted(mediaItem: MediaItem?) { + private fun onTrackCompleted() { // This method is called before we update the currentPlaying, // so in fact currentPlaying will refer to the track that has just finished. if (legacyPlaylistManager.currentPlaying != null) { @@ -196,10 +196,10 @@ class MediaPlayerController( val started = playerState === PlayerState.STARTED val context = UApp.applicationContext() - UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(context, song, started, false) - UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(context, song, started, true) - UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(context, song, started, false) - UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false) + UltrasonicAppWidgetProvider4X1.instance?.notifyChange(context, song, started, false) + UltrasonicAppWidgetProvider4X2.instance?.notifyChange(context, song, started, true) + UltrasonicAppWidgetProvider4X3.instance?.notifyChange(context, song, started, false) + UltrasonicAppWidgetProvider4X4.instance?.notifyChange(context, song, started, false) } fun onDestroy() { From d0959ffcb5d4ababb104858165e051d865eeb0fc Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 21:56:25 +0200 Subject: [PATCH 15/67] AppWidget to Kotlin --- .../provider/UltrasonicAppWidgetProvider.java | 221 ------------------ .../UltrasonicAppWidgetProvider4X1.java | 42 ---- .../UltrasonicAppWidgetProvider4X2.java | 42 ---- .../UltrasonicAppWidgetProvider4X3.java | 42 ---- .../UltrasonicAppWidgetProvider4X4.java | 42 ---- .../provider/UltrasonicAppWidgetProvider.kt | 207 ++++++++++++++++ .../UltrasonicAppWidgetProvider4X1.kt | 28 +++ .../UltrasonicAppWidgetProvider4X2.kt | 28 +++ .../UltrasonicAppWidgetProvider4X3.kt | 28 +++ .../UltrasonicAppWidgetProvider4X4.kt | 28 +++ 10 files changed, 319 insertions(+), 389 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.java delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java deleted file mode 100644 index 29cd9bcc..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java +++ /dev/null @@ -1,221 +0,0 @@ -package org.moire.ultrasonic.provider; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.os.Environment; -import android.view.KeyEvent; -import android.widget.RemoteViews; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.activity.NavigationActivity; -import org.moire.ultrasonic.domain.Track; -import org.moire.ultrasonic.imageloader.BitmapUtils; -import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; -import org.moire.ultrasonic.service.MediaPlayerController; -import org.moire.ultrasonic.util.Constants; - -import timber.log.Timber; - -/** - * Widget Provider for the Ultrasonic Widgets - */ -public class UltrasonicAppWidgetProvider extends AppWidgetProvider -{ - protected int layoutId; - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) - { - defaultAppWidget(context, appWidgetIds); - } - - /** - * Initialize given widgets to default state, where we launch Ultrasonic on default click - * and hide actions if service not running. - */ - private void defaultAppWidget(Context context, int[] appWidgetIds) - { - final Resources res = context.getResources(); - final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId); - - views.setTextViewText(R.id.title, null); - views.setTextViewText(R.id.album, null); - views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text)); - - linkButtons(context, views, false); - pushUpdate(context, appWidgetIds, views); - } - - private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views) - { - // Update specific list of appWidgetIds if given, otherwise default to all - final AppWidgetManager manager = AppWidgetManager.getInstance(context); - - if (manager != null) - { - if (appWidgetIds != null) - { - manager.updateAppWidget(appWidgetIds, views); - } - else - { - manager.updateAppWidget(new ComponentName(context, this.getClass()), views); - } - } - } - - /** - * Handle a change notification coming over from {@link MediaPlayerController} - */ - public void notifyChange(Context context, Track currentSong, boolean playing, boolean setAlbum) - { - if (hasInstances(context)) - { - performUpdate(context, currentSong, playing, setAlbum); - } - } - - /** - * Check against {@link AppWidgetManager} if there are any instances of this widget. - */ - private boolean hasInstances(Context context) - { - AppWidgetManager manager = AppWidgetManager.getInstance(context); - - if (manager != null) - { - int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass())); - return (appWidgetIds.length > 0); - } - - return false; - } - - /** - * Update all active widget instances by pushing changes - */ - private void performUpdate(Context context, Track currentSong, boolean playing, boolean setAlbum) - { - final Resources res = context.getResources(); - final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId); - - String title = currentSong == null ? null : currentSong.getTitle(); - String artist = currentSong == null ? null : currentSong.getArtist(); - String album = currentSong == null ? null : currentSong.getAlbum(); - CharSequence errorState = null; - - // Show error message? - String status = Environment.getExternalStorageState(); - if (status.equals(Environment.MEDIA_SHARED) || status.equals(Environment.MEDIA_UNMOUNTED)) - { - errorState = res.getText(R.string.widget_sdcard_busy); - } - else if (status.equals(Environment.MEDIA_REMOVED)) - { - errorState = res.getText(R.string.widget_sdcard_missing); - } - else if (currentSong == null) - { - errorState = res.getText(R.string.widget_initial_text); - } - - if (errorState != null) - { - // Show error state to user - views.setTextViewText(R.id.title, null); - views.setTextViewText(R.id.artist, errorState); - if (setAlbum) - { - views.setTextViewText(R.id.album, null); - } - views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album); - } - else - { - // No error, so show normal titles - views.setTextViewText(R.id.title, title); - views.setTextViewText(R.id.artist, artist); - if (setAlbum) - { - views.setTextViewText(R.id.album, album); - } - } - - // Set correct drawable for pause state - if (playing) - { - views.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); - } - else - { - views.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); - } - - // Set the cover art - try - { - Bitmap bitmap = currentSong == null ? null : BitmapUtils.Companion.getAlbumArtBitmapFromDisk(currentSong, 240); - - if (bitmap == null) - { - // Set default cover art - views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album); - } - else - { - views.setImageViewBitmap(R.id.appwidget_coverart, bitmap); - } - } - catch (Exception x) - { - Timber.e(x, "Failed to load cover art"); - views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album); - } - - // Link actions buttons to intents - linkButtons(context, views, currentSong != null); - - pushUpdate(context, null, views); - } - - /** - * Link up various button actions using {@link PendingIntent}. - */ - private static void linkButtons(Context context, RemoteViews views, boolean playerActive) - { - Intent intent = new Intent(context, NavigationActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - if (playerActive) - intent.putExtra(Constants.INTENT_SHOW_PLAYER, true); - - intent.setAction("android.intent.action.MAIN"); - intent.addCategory("android.intent.category.LAUNCHER"); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT); - views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); - views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); - - // Emulate media button clicks. - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0); - views.setOnClickPendingIntent(R.id.control_play, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); - pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0); - views.setOnClickPendingIntent(R.id.control_next, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); - pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0); - views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.java deleted file mode 100644 index 0c8a8ca8..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - 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 . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.provider; - -import org.moire.ultrasonic.R; - -public class UltrasonicAppWidgetProvider4X1 extends UltrasonicAppWidgetProvider -{ - - public UltrasonicAppWidgetProvider4X1() - { - super(); - this.layoutId = R.layout.appwidget4x1; - } - - private static UltrasonicAppWidgetProvider4X1 instance; - - public static synchronized UltrasonicAppWidgetProvider4X1 getInstance() - { - if (instance == null) - { - instance = new UltrasonicAppWidgetProvider4X1(); - } - return instance; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.java deleted file mode 100644 index 3ba12ae6..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - 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 . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.provider; - -import org.moire.ultrasonic.R; - -public class UltrasonicAppWidgetProvider4X2 extends UltrasonicAppWidgetProvider -{ - - public UltrasonicAppWidgetProvider4X2() - { - super(); - this.layoutId = R.layout.appwidget4x2; - } - - private static UltrasonicAppWidgetProvider4X2 instance; - - public static synchronized UltrasonicAppWidgetProvider4X2 getInstance() - { - if (instance == null) - { - instance = new UltrasonicAppWidgetProvider4X2(); - } - return instance; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.java deleted file mode 100644 index 15b2a561..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - 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 . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.provider; - -import org.moire.ultrasonic.R; - -public class UltrasonicAppWidgetProvider4X3 extends UltrasonicAppWidgetProvider -{ - - public UltrasonicAppWidgetProvider4X3() - { - super(); - this.layoutId = R.layout.appwidget4x3; - } - - private static UltrasonicAppWidgetProvider4X3 instance; - - public static synchronized UltrasonicAppWidgetProvider4X3 getInstance() - { - if (instance == null) - { - instance = new UltrasonicAppWidgetProvider4X3(); - } - return instance; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.java deleted file mode 100644 index c28c55ab..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - 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 . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.provider; - -import org.moire.ultrasonic.R; - -public class UltrasonicAppWidgetProvider4X4 extends UltrasonicAppWidgetProvider -{ - - public UltrasonicAppWidgetProvider4X4() - { - super(); - this.layoutId = R.layout.appwidget4x4; - } - - private static UltrasonicAppWidgetProvider4X4 instance; - - public static synchronized UltrasonicAppWidgetProvider4X4 getInstance() - { - if (instance == null) - { - instance = new UltrasonicAppWidgetProvider4X4(); - } - return instance; - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt new file mode 100644 index 00000000..46acf216 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt @@ -0,0 +1,207 @@ +/* + * UltrasonicAppWidgetProvider.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.provider + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Environment +import android.view.KeyEvent +import android.widget.RemoteViews +import org.moire.ultrasonic.R +import org.moire.ultrasonic.activity.NavigationActivity +import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.imageloader.BitmapUtils +import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver +import org.moire.ultrasonic.util.Constants +import timber.log.Timber +import java.lang.Exception + +/** + * Widget Provider for the Ultrasonic Widgets + */ +open class UltrasonicAppWidgetProvider : AppWidgetProvider() { + @JvmField + protected var layoutId = 0 + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + defaultAppWidget(context, appWidgetIds) + } + + /** + * Initialize given widgets to default state, where we launch Ultrasonic on default click + * and hide actions if service not running. + */ + private fun defaultAppWidget(context: Context, appWidgetIds: IntArray) { + val res = context.resources + val views = RemoteViews(context.packageName, layoutId) + views.setTextViewText(R.id.title, null) + views.setTextViewText(R.id.album, null) + views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text)) + linkButtons(context, views, false) + pushUpdate(context, appWidgetIds, views) + } + + private fun pushUpdate(context: Context, appWidgetIds: IntArray?, views: RemoteViews) { + // Update specific list of appWidgetIds if given, otherwise default to all + val manager = AppWidgetManager.getInstance(context) + if (manager != null) { + if (appWidgetIds != null) { + manager.updateAppWidget(appWidgetIds, views) + } else { + manager.updateAppWidget(ComponentName(context, this.javaClass), views) + } + } + } + + /** + * Handle a change notification coming over from [MediaPlayerController] + */ + fun notifyChange(context: Context, currentSong: Track?, playing: Boolean, setAlbum: Boolean) { + if (hasInstances(context)) { + performUpdate(context, currentSong, playing, setAlbum) + } + } + + /** + * Check against [AppWidgetManager] if there are any instances of this widget. + */ + private fun hasInstances(context: Context): Boolean { + val manager = AppWidgetManager.getInstance(context) + if (manager != null) { + val appWidgetIds = manager.getAppWidgetIds(ComponentName(context, javaClass)) + return appWidgetIds.isNotEmpty() + } + return false + } + + /** + * Update all active widget instances by pushing changes + */ + private fun performUpdate( + context: Context, + currentSong: Track?, + playing: Boolean, + setAlbum: Boolean + ) { + val res = context.resources + val views = RemoteViews(context.packageName, layoutId) + val title = currentSong?.title + val artist = currentSong?.artist + val album = currentSong?.album + var errorState: CharSequence? = null + + // Show error message? + val status = Environment.getExternalStorageState() + if (status == Environment.MEDIA_SHARED || status == Environment.MEDIA_UNMOUNTED) { + errorState = res.getText(R.string.widget_sdcard_busy) + } else if (status == Environment.MEDIA_REMOVED) { + errorState = res.getText(R.string.widget_sdcard_missing) + } else if (currentSong == null) { + errorState = res.getText(R.string.widget_initial_text) + } + if (errorState != null) { + // Show error state to user + views.setTextViewText(R.id.title, null) + views.setTextViewText(R.id.artist, errorState) + if (setAlbum) { + views.setTextViewText(R.id.album, null) + } + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album) + } else { + // No error, so show normal titles + views.setTextViewText(R.id.title, title) + views.setTextViewText(R.id.artist, artist) + if (setAlbum) { + views.setTextViewText(R.id.album, album) + } + } + + // Set correct drawable for pause state + if (playing) { + views.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark) + } else { + views.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark) + } + + // Set the cover art + try { + val bitmap = + if (currentSong == null) null else BitmapUtils.getAlbumArtBitmapFromDisk( + currentSong, + 240 + ) + if (bitmap == null) { + // Set default cover art + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album) + } else { + views.setImageViewBitmap(R.id.appwidget_coverart, bitmap) + } + } catch (x: Exception) { + Timber.e(x, "Failed to load cover art") + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album) + } + + // Link actions buttons to intents + linkButtons(context, views, currentSong != null) + pushUpdate(context, null, views) + } + + companion object { + /** + * Link up various button actions using [PendingIntent]. + */ + @SuppressLint("UnspecifiedImmutableFlag") + private fun linkButtons(context: Context, views: RemoteViews, playerActive: Boolean) { + var intent = Intent( + context, + NavigationActivity::class.java + ).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + if (playerActive) intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) + intent.action = "android.intent.action.MAIN" + intent.addCategory("android.intent.category.LAUNCHER") + var pendingIntent = + PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT) + views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent) + views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent) + + // Emulate media button clicks. + intent = Intent(Constants.CMD_PROCESS_KEYCODE) + intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) + intent.putExtra( + Intent.EXTRA_KEY_EVENT, + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) + ) + pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0) + views.setOnClickPendingIntent(R.id.control_play, pendingIntent) + intent = Intent(Constants.CMD_PROCESS_KEYCODE) + intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) + intent.putExtra( + Intent.EXTRA_KEY_EVENT, + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT) + ) + pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0) + views.setOnClickPendingIntent(R.id.control_next, pendingIntent) + intent = Intent(Constants.CMD_PROCESS_KEYCODE) + intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) + intent.putExtra( + Intent.EXTRA_KEY_EVENT, + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS) + ) + pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0) + views.setOnClickPendingIntent(R.id.control_previous, pendingIntent) + } + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt new file mode 100644 index 00000000..97c96027 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt @@ -0,0 +1,28 @@ +/* + * UltrasonicAppWidgetProvider4X1.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.provider + +import org.moire.ultrasonic.R + +class UltrasonicAppWidgetProvider4X1 : UltrasonicAppWidgetProvider() { + companion object { + @get:Synchronized + var instance: UltrasonicAppWidgetProvider4X1? = null + get() { + if (field == null) { + field = UltrasonicAppWidgetProvider4X1() + } + return field + } + private set + } + + init { + layoutId = R.layout.appwidget4x1 + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt new file mode 100644 index 00000000..f30c6efe --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt @@ -0,0 +1,28 @@ +/* + * UltrasonicAppWidgetProvider4X2.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.provider + +import org.moire.ultrasonic.R + +class UltrasonicAppWidgetProvider4X2 : UltrasonicAppWidgetProvider() { + companion object { + @get:Synchronized + var instance: UltrasonicAppWidgetProvider4X2? = null + get() { + if (field == null) { + field = UltrasonicAppWidgetProvider4X2() + } + return field + } + private set + } + + init { + layoutId = R.layout.appwidget4x2 + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt new file mode 100644 index 00000000..324efcee --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt @@ -0,0 +1,28 @@ +/* + * UltrasonicAppWidgetProvider4X3.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.provider + +import org.moire.ultrasonic.R + +class UltrasonicAppWidgetProvider4X3 : UltrasonicAppWidgetProvider() { + companion object { + @get:Synchronized + var instance: UltrasonicAppWidgetProvider4X3? = null + get() { + if (field == null) { + field = UltrasonicAppWidgetProvider4X3() + } + return field + } + private set + } + + init { + layoutId = R.layout.appwidget4x3 + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt new file mode 100644 index 00000000..4d03970a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt @@ -0,0 +1,28 @@ +/* + * UltrasonicAppWidgetProvider4X4.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.provider + +import org.moire.ultrasonic.R + +class UltrasonicAppWidgetProvider4X4 : UltrasonicAppWidgetProvider() { + companion object { + @get:Synchronized + var instance: UltrasonicAppWidgetProvider4X4? = null + get() { + if (field == null) { + field = UltrasonicAppWidgetProvider4X4() + } + return field + } + private set + } + + init { + layoutId = R.layout.appwidget4x4 + } +} \ No newline at end of file From e1f4ee15d536de71ae2dcc1df357793d781f4e1f Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 22:24:06 +0200 Subject: [PATCH 16/67] Fix Widget --- ultrasonic/src/main/AndroidManifest.xml | 6 +- .../receiver/MediaButtonIntentReceiver.java | 83 ------------------- .../receiver/MediaButtonIntentReceiver.kt | 52 ++++++++++++ .../service/MediaPlayerController.kt | 7 +- 4 files changed, 60 insertions(+), 88 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 7e4b433e..27946252 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -127,7 +127,11 @@ android:name="android.appwidget.provider" android:resource="@xml/appwidget_info_4x4"/> - + + + + + diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java deleted file mode 100644 index 229da68d..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - 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 . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Parcelable; -import timber.log.Timber; - -import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.Settings; -import org.moire.ultrasonic.util.Util; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -/** - * @author Sindre Mehus - */ -public class MediaButtonIntentReceiver extends BroadcastReceiver -{ - private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); - - @Override - public void onReceive(Context context, Intent intent) - { - String intentAction = intent.getAction(); - - // If media button are turned off and we received a media button, exit - if (!Settings.getMediaButtonsEnabled() && Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) - return; - - // Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets - if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction) && - !Constants.CMD_PROCESS_KEYCODE.equals(intentAction)) return; - - Bundle extras = intent.getExtras(); - - if (extras == null) - { - return; - } - - Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT); - Timber.i("Got MEDIA_BUTTON key event: %s", event); - - try - { - Intent serviceIntent = new Intent(Constants.CMD_PROCESS_KEYCODE); - serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - lifecycleSupport.getValue().receiveIntent(serviceIntent); - - if (isOrderedBroadcast()) - { - abortBroadcast(); - } - } - catch (Exception x) - { - // Ignored. - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt new file mode 100644 index 00000000..48f6a135 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt @@ -0,0 +1,52 @@ +/* + * MediaButtonIntentReceiver.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Parcelable +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings +import timber.log.Timber +import java.lang.Exception + +/** + * This class is used to receive commands from the widget + */ +class MediaButtonIntentReceiver : BroadcastReceiver(), KoinComponent { + private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() + + override fun onReceive(context: Context, intent: Intent) { + val intentAction = intent.action + + // If media button are turned off and we received a media button, exit + if (!Settings.mediaButtonsEnabled && Intent.ACTION_MEDIA_BUTTON == intentAction) return + + // Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets + if (Intent.ACTION_MEDIA_BUTTON != intentAction && + Constants.CMD_PROCESS_KEYCODE != intentAction + ) return + val extras = intent.extras ?: return + val event = extras[Intent.EXTRA_KEY_EVENT] as Parcelable? + Timber.i("Got MEDIA_BUTTON key event: %s", event) + try { + val serviceIntent = Intent(Constants.CMD_PROCESS_KEYCODE) + serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event) + lifecycleSupport.receiveIntent(serviceIntent) + if (isOrderedBroadcast) { + abortBroadcast() + } + } catch (x: Exception) { + // Ignored. + } + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index ec866abb..d591d630 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -146,10 +146,9 @@ class MediaPlayerController( when { playerState === PlayerState.PAUSED -> { - // TODO: Save playlist -// playbackStateSerializer.serialize( -// downloader.getPlaylist(), downloader.currentPlayingIndex, playerPosition -// ) + playbackStateSerializer.serialize( + playList, currentMediaItemIndex, playerPosition + ) } playerState === PlayerState.STARTED -> { scrobbler.scrobble(currentPlaying, false) From bb77216eff01810ab606e5d03ea2a1177ba78911 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 22:33:04 +0200 Subject: [PATCH 17/67] Format --- .../ultrasonic/provider/UltrasonicAppWidgetProvider.kt | 9 +++++---- .../provider/UltrasonicAppWidgetProvider4X1.kt | 2 +- .../provider/UltrasonicAppWidgetProvider4X2.kt | 2 +- .../provider/UltrasonicAppWidgetProvider4X3.kt | 2 +- .../provider/UltrasonicAppWidgetProvider4X4.kt | 2 +- .../ultrasonic/receiver/MediaButtonIntentReceiver.kt | 6 +++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt index 46acf216..7a12fdfe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt @@ -17,6 +17,7 @@ import android.content.Intent import android.os.Environment import android.view.KeyEvent import android.widget.RemoteViews +import java.lang.Exception import org.moire.ultrasonic.R import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.domain.Track @@ -24,11 +25,11 @@ import org.moire.ultrasonic.imageloader.BitmapUtils import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.util.Constants import timber.log.Timber -import java.lang.Exception /** * Widget Provider for the Ultrasonic Widgets */ +@Suppress("MagicNumber") open class UltrasonicAppWidgetProvider : AppWidgetProvider() { @JvmField protected var layoutId = 0 @@ -149,8 +150,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { } else { views.setImageViewBitmap(R.id.appwidget_coverart, bitmap) } - } catch (x: Exception) { - Timber.e(x, "Failed to load cover art") + } catch (all: Exception) { + Timber.e(all, "Failed to load cover art") views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album) } @@ -204,4 +205,4 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { views.setOnClickPendingIntent(R.id.control_previous, pendingIntent) } } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt index 97c96027..5efd3ca8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X1.kt @@ -25,4 +25,4 @@ class UltrasonicAppWidgetProvider4X1 : UltrasonicAppWidgetProvider() { init { layoutId = R.layout.appwidget4x1 } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt index f30c6efe..7235a998 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X2.kt @@ -25,4 +25,4 @@ class UltrasonicAppWidgetProvider4X2 : UltrasonicAppWidgetProvider() { init { layoutId = R.layout.appwidget4x2 } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt index 324efcee..7b9187cf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X3.kt @@ -25,4 +25,4 @@ class UltrasonicAppWidgetProvider4X3 : UltrasonicAppWidgetProvider() { init { layoutId = R.layout.appwidget4x3 } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt index 4d03970a..d641ff4a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider4X4.kt @@ -25,4 +25,4 @@ class UltrasonicAppWidgetProvider4X4 : UltrasonicAppWidgetProvider() { init { layoutId = R.layout.appwidget4x4 } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt index 48f6a135..ec2dd114 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt @@ -11,13 +11,13 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Parcelable +import java.lang.Exception import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import timber.log.Timber -import java.lang.Exception /** * This class is used to receive commands from the widget @@ -45,8 +45,8 @@ class MediaButtonIntentReceiver : BroadcastReceiver(), KoinComponent { if (isOrderedBroadcast) { abortBroadcast() } - } catch (x: Exception) { + } catch (ignored: Exception) { // Ignored. } } -} \ No newline at end of file +} From fd34199c27ae03b617dd39f840786e9ef5c0b4e1 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 22:45:51 +0200 Subject: [PATCH 18/67] Remove unused resources --- .../src/main/res/drawable/ic_artist.xml | 9 ------ .../src/main/res/drawable/ic_library.xml | 9 ------ .../drawable/media_backward_medium_dark.xml | 5 --- .../drawable/media_forward_medium_dark.xml | 5 --- .../res/drawable/media_pause_large_dark.xml | 5 --- .../res/drawable/media_start_large_dark.xml | 5 --- .../src/main/res/drawable/menu_arrow.xml | 9 ------ ultrasonic/src/main/res/values-cs/strings.xml | 30 ----------------- ultrasonic/src/main/res/values-de/strings.xml | 23 ------------- ultrasonic/src/main/res/values-es/strings.xml | 29 ----------------- ultrasonic/src/main/res/values-fr/strings.xml | 29 ----------------- ultrasonic/src/main/res/values-hu/strings.xml | 28 ---------------- ultrasonic/src/main/res/values-it/strings.xml | 18 ----------- ultrasonic/src/main/res/values-nl/strings.xml | 29 ----------------- ultrasonic/src/main/res/values-pl/strings.xml | 29 ----------------- .../src/main/res/values-pt-rBR/strings.xml | 28 ---------------- ultrasonic/src/main/res/values-pt/strings.xml | 27 ---------------- ultrasonic/src/main/res/values-ru/strings.xml | 30 ----------------- .../src/main/res/values-zh-rCN/strings.xml | 28 ---------------- .../src/main/res/values-zh-rTW/strings.xml | 1 - ultrasonic/src/main/res/values/arrays.xml | 5 --- .../res/values/playback_preferences_keys.xml | 4 --- ultrasonic/src/main/res/values/strings.xml | 32 ------------------- 23 files changed, 417 deletions(-) delete mode 100644 ultrasonic/src/main/res/drawable/ic_artist.xml delete mode 100644 ultrasonic/src/main/res/drawable/ic_library.xml delete mode 100644 ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml delete mode 100644 ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml delete mode 100644 ultrasonic/src/main/res/drawable/media_pause_large_dark.xml delete mode 100644 ultrasonic/src/main/res/drawable/media_start_large_dark.xml delete mode 100644 ultrasonic/src/main/res/drawable/menu_arrow.xml delete mode 100644 ultrasonic/src/main/res/values/playback_preferences_keys.xml diff --git a/ultrasonic/src/main/res/drawable/ic_artist.xml b/ultrasonic/src/main/res/drawable/ic_artist.xml deleted file mode 100644 index c3daf609..00000000 --- a/ultrasonic/src/main/res/drawable/ic_artist.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_library.xml b/ultrasonic/src/main/res/drawable/ic_library.xml deleted file mode 100644 index 6981f924..00000000 --- a/ultrasonic/src/main/res/drawable/ic_library.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml b/ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml deleted file mode 100644 index 79c6bfd3..00000000 --- a/ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml b/ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml deleted file mode 100644 index dc96d2dc..00000000 --- a/ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/media_pause_large_dark.xml b/ultrasonic/src/main/res/drawable/media_pause_large_dark.xml deleted file mode 100644 index 64164940..00000000 --- a/ultrasonic/src/main/res/drawable/media_pause_large_dark.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/media_start_large_dark.xml b/ultrasonic/src/main/res/drawable/media_start_large_dark.xml deleted file mode 100644 index 0ebbb01e..00000000 --- a/ultrasonic/src/main/res/drawable/media_start_large_dark.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/menu_arrow.xml b/ultrasonic/src/main/res/drawable/menu_arrow.xml deleted file mode 100644 index f6db0abe..00000000 --- a/ultrasonic/src/main/res/drawable/menu_arrow.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 197a16ad..3c92e9e2 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -61,9 +61,7 @@ Obrazovka zapnuta Zobrazit album Náhodně - Playlist byl náhodně zamíchán Vizualizér - Načítám Stahuji - %s Přehrávám mix Playlist úspěšně uložen. @@ -95,7 +93,6 @@ Žánry Hudba Bez připojení - Náhodné přehrávání Náhodné Označené hvězdičkou Skladby @@ -106,26 +103,19 @@ Smazaný playlist %s Chyba smazání playlistu %s Ukončit - Navigace Nastavení Obnovit Knihovna médií Offline média - Došlo k chybě sítě. Pokus %1$d z %2$d. - Dostupných %d umělců. - Načítání dat serveru. - Načítání ze serveru. Hotovo! Playlisty Aktualizovat informace Aktualizované informace playlistu pro %s Chyba aktualizace informací playlistu %s - Chvilku strpení… Alba Umělci Vyhledávání Zobrazit více Nenalezeno, zkuste znovu - Kliknout pro vyhledání Skladby Hledat Média nenalezena @@ -136,7 +126,6 @@ Vybrat adresář Žánry nenalezeny Žádné uložené playlisty na serveru - Kontaktuji server, chvilku strpení. Vzhled Délka bufferu Vypnuto @@ -176,8 +165,6 @@ Interval obnovení chatu Zahodit záložku Zahodit záložku po dokončení přehrávání skladby - Zahození playlistu - Zahodit playlist po dokončení přehrávání všech skladeb Vyčistit historii vyhledávání Chyba připojení. Výchozí alba @@ -200,7 +187,6 @@ Nabyde účinnosti při příštím skenování hudby systému Android. Interval přeskočení Zadejte funkční adresu URL. - Zadejte správné uživatelské jméno (bez mezer za jménem). Maximum alb Maximum umělců 112 Kbps @@ -239,8 +225,6 @@ 3 skladby 5 skladeb Neomezeně - Pokračovat po připojení sluchátek - Aplikace spustí pozastavené přehrávání po připojení kabelu sluchátek do přístroje. 1 10 100 @@ -265,10 +249,8 @@ Adresa serveru Název Heslo - Vzdálený server Stahovat škálované obrázky ze serveru místo plné velikosti (šetří přenos dat) Škálování obrázků alb na serveru - Nepoužitý Uživatelské jméno Zobrazit ovládání na zamknuté obrazovce Zobrazí ovládání přehrávače na zamknuté obrazovce @@ -343,12 +325,6 @@ Zobrazit umělce albumArt Vícenásobné roky - Pokračovat v přehrávání po připojení bluetooth přístroje - Pozastavení přehrávání při odpojení bluetooth přístroje - Všechny bluetooth přístroje - Pouze audio (A2DP) přístroje - Vypnuto - Povolení tohoto nastavení může pomoci zlepšit funkci spuštění/pozastavení přehrávání na starších bluetooth přístrojích Možnosti ladění aplikace Zapisovat logy ladění do souboru Soubory logů jsou dostupné v %1$s/%2$s @@ -374,12 +350,6 @@ %d skladeb %d skladeb - - Zbývá %d den zkušební doby - Zbývají %d dny zkušební doby - Zbývá %d dní zkušební doby - Zbývá %d dní zkušební doby - Obecná api chyba: %1$s diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index 7a597eb3..dff7a6f8 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -73,9 +73,7 @@ Bildschirm an Album anzeigen Mischen - Die Wiedergabeliste wurde gemischt Grafik - Zwischenspeichern Herunterladen - %s Wiedergabeliste mischen Die Wiedergabeliste wurde gespeichert @@ -108,7 +106,6 @@ Musik Offline %s - Server einrichten - Gemischte Wiedergabe Zufällig Mit Stern Titel @@ -120,26 +117,19 @@ Löschen der Wiedergabeliste %s ist fehlgeschlagen Downloads Ende - Navigation Einstellungen Aktualisierung Medienbibliothek Offline Medien - Netzwerkfehler. Neuer Versuch %1$d von %2$d. - %dInterpreten gefunden - Lese vom Server. - Lese vom Server. Fertig! Wiedergabelisten Aktualisierungs-Informationen Wiedergabeliste für %s aktualisiert Aktualisierung der Wiedergabeliste %s ist fehlgeschlagen - Bitte warten… Alben Künstler*innen Suche Zeige mehr Keine Treffer, bitte erneut versuchen - Neue Suche Titel Suche Keine Medien gefunden @@ -150,7 +140,6 @@ Ordner wählen Keine Genres gefunden Keine Wiedergabelisten auf dem Server - Kontaktiere Server, bitte warten. Aussehen Puffer-Länge Deaktiviert @@ -190,8 +179,6 @@ Chat Aktualisierungsintervall Lesezeichen löschen Lesezeichen nach Wiedergabe löschen - Wiedergabeliste löschen - Wiedergableliste nach Wiedergabe aller Titel löschen Suchverlauf löschen Verbindungsfehler Standard Alben @@ -215,7 +202,6 @@ Wird beim nächsten Durchsuchen nach Musik durch Android wirksam. Sprunglänge Bitte eine gültige URL angeben. - Bitte einen gültigen Benutzernamen eingeben (ohne führende Leerzeichen). Maximale Alben Max Interpeten 112 Kbps @@ -254,8 +240,6 @@ 3 Titel 5 Titel Unbegrenzt - Fortsetzen mit Kopfhörer - Die App setzt eine pausierte Wiedergabe beim Anschließen der Kopfhörer fort. Gespielte Musik scrobbeln 1 10 @@ -281,10 +265,8 @@ Server Adresse Name Kennwort - Server entfernen Skalierte Cover vom Server laden (spart Bandbreite) Serverseitige Skalierung der Cover - Unbenutzt Benutzername Steuerelemente auf Sperrbildschirm Wiedergabeelemente auf dem Sperrbildschirm anzeigen @@ -361,11 +343,6 @@ Künstler*in zeigen Album Cover Mehrere Jahre - Wiedergabe fortsetzen, wenn ein Bluetooth Gerät verbunden wurde - Wiedergabe pausieren, wenn ein Bluetooth Gerät getrennt wurde - Alle Bluetooth Geräte - Nur Audio (A2DP) Geräte - Deaktiviert Dateien behalten Dateien löschen Logeinträge gelöscht diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 80b620cc..93ee8768 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -75,9 +75,7 @@ Pantalla encendida Mostrar Álbum Aleatorio - Lista de reproducción en modo aleatorio Visualizador - Almacenando en el buffer Descargando - %s Reproduciendo en modo aleatorio Lista de reproducción guardada con éxito. @@ -110,7 +108,6 @@ Música Sin conexión %s - Configurar servidor - Reproducción aleatoria Aleatorio Me gusta Canciones @@ -124,26 +121,19 @@ Fallo al eliminar la lista de reproducción %s Descargas Salir - Navegación Configuración Actualizar Biblioteca de medios Medios sin conexión - Se ha producido un error de red. Reintento %1$d de %2$d. - Obtenido(s) %d artista(s). - Leyendo del servidor. - Leyendo del servidor. ¡Hecho! Listas de reproducción Actualizar Información Actualizada la información de la lista de reproducción para %s Fallo al actualizar la información de la lista de reproducción para %s - Por favor espere… Álbumes Artistas Buscar Mostrar mas Sin resultados, por favor inténtalo de nuevo - Haz click para buscar Canciones Buscar No se han encontrado medios @@ -155,7 +145,6 @@ Seleccionar la carpeta No se han encontrado géneros No hay listas de reproducción almacenadas en el servidor - Contactando con el servidor, por favor espera. Apariencia Duración del Buffer Deshabilitado @@ -196,8 +185,6 @@ Intervalo de refresco del Chat Limpiar marcador Limpiar marcador tras la finalización de la reproducción de una canción - Limpiar lista de reproducción - Limpiar la lista de reproducción tras la finalización de la reproducción de todas las canciones Limpiar el historial de búsqueda Fallo de conexión. Álbumes predeterminados @@ -222,7 +209,6 @@ Tiene efecto la próxima vez que Android escanee la música de tu dispositivo. Intervalo de salto Por favor especifica una URL válida. - Por favor especifica un nombre de usuario válido (sin espacios al final). Máximo de Álbumes Máximo de Artistas 112 Kbps @@ -261,8 +247,6 @@ 3 canciónes 5 canciónes Ilimitado - Reanudar al insertar los auriculares - La aplicación reanudará la reproducción en pausa al insertar los auriculares en el dispositivo. Recuerda configurar tu nombre de usuario y contraseña en los servicios de Scrobble en el servidor Hacer Scrobble de mis reproducciones 1 @@ -291,10 +275,8 @@ Dirección del servidor Nombre Contraseña - Quitar servidor Descarga imágenes escaladas del servidor en lugar del tamaño completo (salva ancho de banda) Escalado de caratulas en el servidor - Sin usar Nombre de usuario Color del servidor Mostrar controles en la pantalla de bloqueo @@ -376,13 +358,6 @@ Mostrar artista Caratula del Álbum Múltiples años - Reanudar al conectar un dispositivo Bluetooth - Pausar al desconectar un dispositivo Bluetooth - Todos los dispositivos Bluetooth - Solo dispositivos de audio (A2DP) - Deshabilitado - Dispositivo Bluetooth con solo un único botón Reproducir / Pausa - Habilitar esto puede ayudar con los dispositivos Bluetooth más antiguos cuando la reproducción / pausa no funciona correctamente Opciones de depuración Escribir registro de depuración en un archivo Los archivos de registro están disponibles en %1$s/%2$s @@ -438,10 +413,6 @@ %d canción insertada después de la canción actual %d canciones insertadas después de la canción actual. - - Queda %d día de periodo de prueba - Quedan %d días de periodo de prueba - Error genérico de api: %1$s diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 5bf0ddb3..38fd3f38 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -73,9 +73,7 @@ Sur l\'écran Afficher l\'album Aléatoire - Playlist aléatoire Visualiseur - Mise en mémoire Téléchargement - %s En lecture aléatoire Playlist enregistrée avec succès ! @@ -107,7 +105,6 @@ Genres Musique Hors-ligne - Lecture aléatoire Aléatoire Favoris Titres @@ -121,26 +118,19 @@ Échec de suppression de la playlist %s Téléchargements Quitter - Navigation Paramètres Rafraichir Bibliothèque musicale Musique hors-ligne - Une erreur de réseau s\'est produite. Tentative %1$d sur %2$d. - %d artistes récupérés. - Lecture du serveur. - Lecture du serveur. Terminé ! Playlists Mise à jour des informations Informations de la playlist %s mises à jour Échec de mise à jour des informations de la playlist %s - Veuillez patienter… Albums Artistes Recherche Afficher plus Aucun résultat, veuillez essayer à nouveau - Cliquer pour rechercher Titres Recherche Aucun titre trouvé @@ -151,7 +141,6 @@ Sélectionner le dossier Aucun genre trouvé Aucune playlist sur le serveur - Contact du serveur, veuillez patienter. Apparence Longueur de la mémoire tampon Désactivé @@ -191,8 +180,6 @@ Délai de rafraichissement du salon de discussion Effacer le signet Effacer le signet à la fin de la lecture d\'un titre - Effacer la playlist - Effacer la playlist à la fin de la lecture de tous les titres Effacer l\'historique des recherches Connection échouée Albums par défaut @@ -217,7 +204,6 @@ Prendra effet la prochaine fois qu\'Android recensera les médias disponibles sur l\'appareil. Intervalle de saut Veuillez spécifier une URL valide. - Veuillez spécifier un nom d\'utilisateur valide (sans espace à la fin). Albums maximum Artistes maximum 112 Kb/s @@ -256,8 +242,6 @@ 3 morceaux 5 morceaux Illimité - Reprise à l\'insertion des écouteurs - L\'application reprendra la lecture lors de l\'insertion du casque dans l\'appareil. Pensez à configurer votre nom d’utilisateur et votre mot de passe dans le(s) service(s) Scrobble sur le serveur. Scrobbler mes lectures 1 @@ -286,10 +270,8 @@ Adresse du serveur Nom Mot de passe - Supprimer le serveur Télécharger sur le serveur des images réduites au lieu des images grand format (bande passante réduite) Mise à l\'échelle des pochettes d\'album sur le serveur - Inutilisé Nom d\'utilisateur Couleur du serveur Boutons de contrôle sur l\'écran de verrouillage @@ -369,13 +351,6 @@ Afficher l\'artiste Pochette d\'album Années multiples - Reprendre lorsqu’un appareil Bluetooth se connecte - Mettre en pause lorsqu’un appareil Bluetooth se déconnecte - Tous les appareils Bluetooth - Seulement les appareils audio (A2DP) - Désactivé - Appareil Bluetooth avec un seul bouton Lecture/Pause - Activer cela peut aider sur les anciens appareils Bluetooth lorsque Lecture/Pause ne fonctionne pas correctement Paramètres de debug Enregistrer les logs de debug dans des fichiers Les fichiers de log sont disponibles dans %1$s/%2$s @@ -404,10 +379,6 @@ %d titre %d titres - - %d jour restant à la période d\'essai - %d jours restant à la période d\'essai - Erreur de l\'API générique : %1$s diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index a8e758b3..9fb8f0f0 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -70,9 +70,7 @@ Kijelző be Ugrás az albumhoz Véletlen sorrendű - Véletlen sorrendű lejátszás Visualizer - Pufferelés Letöltés - %s Véletlen sorrendű Lejátszási lista mentése sikeres. @@ -104,7 +102,6 @@ Műfajok Zenék Kapcsolat nélküli - Véletlen sorrendű Véletlenszerű Csillaggal megjelölt Dalok @@ -115,26 +112,19 @@ Törölt lejátszási lista %s Lejátszási lista törlése sikertelen %s Kilépés - Navigáció Beállítások Frissítés Mediakönyvtár Kapcsolat nélküli médiák - Hálózati hiba történt! Újrapróbálkozás %1$d - %2$d. - %d előadó található a médiakönyvtárban. - Olvasás a kiszolgálóról… - Olvasás a kiszolgálóról… Kész! Lejátszási listák Módosítás Módosított lejátszási lista %s Lejátszási lista módosítása sikertelen %s - Kérem várjon!… Albumok Előadók Keresés Továbbiak Nincs találat, próbálja újra! - Érintse meg a kereséshez Dalok Keresés Nem található média! @@ -145,7 +135,6 @@ Mappa kiválasztása Műfajok nem találhatók! Nincs mentett lejátszási lista a kiszolgálón. - Csatlakozás a kiszolgálóhoz, kérem várjon! Megjelenés Pufferméret Letiltva @@ -185,8 +174,6 @@ Csevegés frissítési gyakorisága Könyvjelző törlése Könyvjelző törlése a dal lejátszása után. - Várólista törlése - Várólista törlése az összes dal lejátszása után. Keresési előzmények törlése Csatlakozási hiba! Albumok találati száma @@ -209,7 +196,6 @@ A következő alkalomtól lép életbe, amikor az Android zenefájlokat keres a telefonon. Ugrás időintervalluma Adjon meg egy érvényes URL-t! - Adjon meg egy érvényes felhasználónevet (szóközt nem tartalmazhat)! Albumok max. találati száma Előadók max. találati száma 112 Kbps @@ -248,8 +234,6 @@ 3 dal 5 dal Korlátlan - Folytatás a fejhallgató behelyezésekor - Az alkalmazás folytatja a szüneteltetett lejátszást a fejhallgató behelyezésekor a készülékbe. Ne felejtsd el beállítani a Scrobble szolgáltatónál használt felhasználóneved és jelszavad a szervereden Scrobble engedélyezése 1 @@ -276,10 +260,8 @@ Kiszolgáló címe Név Jelszó - Kiszolgáló eltávolítása Teljes méretű helyett átméretezett képek letöltése a kiszolgálóról (sávszélesség-takarékos). Albumborító átméretezés (Kiszolgáló-oldali) - Kiszolgáló Felhasználónév Képernyőzár kezelése Lejátszó-kezelőpanel megjelenítése a képernyőzáron. @@ -354,12 +336,6 @@ Ugrás az előadóhoz albumArt Több év - Folytatás Bluetooth eszköz csatlakozásakor - Szünet Bluetooth eszköz kikapcsolásakor - Minden Bluetooth eszköz - Csak audio (A2DP) eszközök - Kikapcsolva - Régebbi Bluetooth eszközök esetén segíthet, ha a Lejátszás/Szünet nem működik megfelelően Hibakeresési lehetőségek Hibakeresési napló írása fájlba A naplófájlok elérhetőek a következő helyen: %1$s/%2$s @@ -383,10 +359,6 @@ %d dal %d dal - - %d nap van hátra a próba időszakból. - %d nap van hátra a próba időszakból. - Általános api hiba: %1$s diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index fe7c0a1d..6d0bdcbc 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -58,9 +58,7 @@ Schermo acceso Visualizza Album Casuale - Playlist casuale Visualizzatore - Buffering In scaricamento - %s Riproduzione casuale Playlist salvata con successo @@ -92,7 +90,6 @@ Generi Musica Disconnesso - Riproduzione casuale Casuale Preferiti Canzoni @@ -103,25 +100,18 @@ Playlist %s eliminata Impossibile eliminare la playlist %s Esci - Navigazione Impostazioni Libreria Media Offline - Problema di rete. Tentativo %1$d di %2$d. - Ottenuti%d Artisti. - Lettura dal server. - Lettura dal server. Completato! Playlist Aggiorna Informazioni Aggiorna informazioni playlist per %s Impossibile aggiornare informazioni playlist per %s - Attendere, per favore… Album Artisti Cerca Mostra di più Nessun risultato, riprova per favore - Selezione per cercare Canzoni Cerca Nessun media trovato @@ -132,7 +122,6 @@ Seleziona cartella Nessun genere trovato Nessuna playlist salvata sul server - Server contattato, attendere. Aspetto Lunghezza buffer Disabilitato @@ -172,8 +161,6 @@ Intervallo Aggiornamento Chat Pulisci Segnalibro Pulisci segnalibro al completamento della riproduzione di una canzone - Pulisci Playlist - Pulisci playlist al completamento della riproduzione di tutte le canzoni Pulisci Storico Ricerca Errore connessione. Album predefiniti @@ -195,7 +182,6 @@ Nascondi Da Altro Effettivo alla prossima scansione Android per file musicali sul telefono. Specifica un URL valido per favore. - Per favore specifica un nome utente valido (senza spazi) N° Max Album N° Max Artisti 112 Kbps @@ -234,7 +220,6 @@ 3 canzoni 5 canzoni Illimitato - Riprendi all\'inserimento delle cuffie 1 10 100 @@ -258,10 +243,8 @@ Indirizzo Server Nome Password - Elimina Server Scarica dal server le immagini ridimensionate (risparmia larghezza di banda) Ridimensionamento copertine Album lato server - Inutilizzato Username Mostra i controlli del blocco schermo Mostra i controlli di riproduzione sulla schermata di blocco @@ -308,7 +291,6 @@ Commenta \"%s\" è stato rimosso dalla playlist Condividi canzoni via - Disabilitato Elimina Il periodo di prova è terminato. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index e358892d..534e08bf 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -75,9 +75,7 @@ Scherm aan Album tonen Willekeurig - Afspeellijst wordt willekeurig afgespeeld Visualisatie - Bezig met bufferen Bezig met downloaden - %s Bezig met willekeurig afspelen Afspeellijst is opgeslagen. @@ -110,7 +108,6 @@ Muziek Offline %s - Server instellen - Willekeurig afspelen Willekeurig Favorieten Nummers @@ -124,26 +121,19 @@ Afspeellijst %s kan niet worden verwijderd Downloads Afsluiten - Navigatie Instellingen Verversen Mediabibliotheek Offline media - Er is een netwerkfout opgetreden. Bezig met opnieuw proberen; poging %1$d van %2$d. - %d artiesten opgehaald. - Bezig met uitlezen van server… - Klaar! Afspeellijsten Informatie bijwerken Afspeellijstinformatie bijgewerkt voor %s Kan afspeellijstinformatie voor %s niet bijwerken - Even geduld… Albums Artiesten Zoeken Meer tonen Geen overeenkomsten; probeer het opnieuw - Druk om te zoeken Nummers Zoeken Geen media gevonden @@ -155,7 +145,6 @@ Map kiezen Geen genres gevonden Geen opgeslagen afspeellijsten op server - Bezig met verbinden met server; even geduld… Uiterlijk Bufferduur Uitgeschakeld @@ -196,8 +185,6 @@ Chat-ververstussenpoos Bladwijzer verwijderen Bladwijzer verwijderen nadat nummer is afgespeeld - Afspeellijst wissen - Afspeellijst wissen nadat alle nummers zijn afgespeeld Zoekgeschiedenis wissen Verbindingsfout. Standaardalbums @@ -222,7 +209,6 @@ Dit wordt toegepast bij de volgende keer dat Android je muziek doorzoekt. Overslaantussenpoos Geef een geldige URL op. - Geef een geldige gebruikersnaam op (geen spaties erachter). Max. aantal albums Max. aantal artiesten 112 Kbps @@ -261,8 +247,6 @@ 3 nummers 5 nummers Ongelimiteerd - Hervatten bij aansluiten van hoofdtelefoon - Het afspelen wordt hervat zodra er een hoofdtelefoon wordt aangesloten. Let op: stel je gebruikersnaam en wachtwoord van je scrobble-dienst(en) in op je Subsonic-server Scrobbelen 1 @@ -291,10 +275,8 @@ Serveradres Naam Wachtwoord - Server verwijderen Verkleinde afbeeldingen ophalen van server in plaats van volledige (bespaart bandbreedte) Verkleinde afbeeldingen ophalen van server - Ongebruikt Gebruikersnaam Serverkleur Vergrendelschermbediening tonen @@ -376,13 +358,6 @@ Artiest tonen Albumhoes Meerdere jaren - Hervatten bij verbinding met bluetoothapparaat - Pauzeren als verbinding met bluetoothapparaat verbroken is - Alle bluetoothapparaten - Alleen audio-apparaten (AD2P) - Uitgeschakeld - Bluetoothapparaat met één afspeel- en pauzeknop - Schakel dit in bij gebruik van oudere bluetoothapparaten om de afspeel- en pauzeerknop goed te laten werken Foutopsporingsopties Foutopsporingslogboek bijhouden De logboeken worden opgeslagen in %1$s/%2$s @@ -438,10 +413,6 @@ %d nummer ingevoegd na het huidige nummer %d nummers ingevoegd na het huidige nummer - - Nog %d dag over van de proefperiode - Nog %d dagen over van de proefperiode - Algemene API-fout: %1$s diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 285241f4..9caa2b90 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -61,9 +61,7 @@ Ekran włączony Wyświetl album Wymieszaj - Playlista została wymieszana Efekt wizualny - Buforowanie Pobieranie - %s Odtwarzanie losowe Playlista została zapisana. @@ -95,7 +93,6 @@ Gatunki Muzyka Offline - Losowo Losowe Ulubione Utwory @@ -106,26 +103,19 @@ Usunięto playlistę %s Usunięcie playlisty %s nie powiodło się Zakończ - Nawigacja Ustawienia Refresh Biblioteka mediów Media offline - Wystąpił błąd sieci. Ponawiam %1$d z %2$d. - Znaleziono %d artystów. - Trwa odczyt z serwera. - Odczyt z serwera zakończony! Playlisty Aktualizacja informacji Zaktualizowano informacje dla playlisty %s Błąc podczas aktualizacji playlisty %s - Proszę czekać… Albumy Artyści Wyszukaj Wyświetl więcej Brak wyników, proszę spróbować ponownie - Kliknij, aby wyszukać Utwory Wyszukiwanie Brak mediów @@ -136,7 +126,6 @@ Wybierz folder Brak gatunków Brak zapisanych playlist na serwerze - Trwa łączenie z serwerem, proszę czekać. Wygląd Wielkość bufora Wyłączone @@ -176,8 +165,6 @@ Okres odświeżania czatu Czyszczenie zakładek Czyść zakładkę po zakończeniu odtwarzania utworu - Czyszczenie playlist - Czyść playlistę po zakończeniu odtwarzania wszystkich utworów Wyczyść historię wyszukiwania Błąd połączenia. Domyślna ilość wyników - albumy @@ -200,7 +187,6 @@ Efekt widoczny będzie po następnym skanowaniu muzyki przez system Android Skok przewijania Proszę wprowadzić prawidłowy URL - Proszę wprowadzić prawidłową nazwę użytkownika (bez spacji na końcu) Maksymalna ilość wyników - albumy Maksymalna ilość wyników - artyści 112 Kbps @@ -239,8 +225,6 @@ 3 utwory 5 utworów Bez limitu - Wznawiaj po podłączeniu słuchawek - Aplikacja wznowi zatrzymane odtwarzanie po podpięciu słuchawek. 1 10 100 @@ -265,10 +249,8 @@ Adres serwera Nazwa Hasło - Usuń serwer Pobiera przeskalowane obrazy z serwera zamiast pełnego rozmiaru (oszczędza ilość przesyłanych danych) Skalowanie okładek po stronie serwera - Bez nazwy Nazwa użytkownika Wyświetlaj na ekranie blokady Wyświetla widżet odtwarzacza na ekranie blokady @@ -340,11 +322,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników Wyświetlaj artystę Okładka Z różnych lat - Resume when a Bluetooth device is connected - Pause when a Bluetooth device is disconnected - All Bluetooth devices - Only audio (A2DP) devices - Wyłączone Configured servers Are you sure you want to delete the server? Editing server @@ -363,12 +340,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników %d utworów %d utworów - - %d dzień pozostał do zakończenia okresu próbnego - %d dni pozostały do zakończenia okresu próbnego - %d dni pozostało do zakończenia okresu próbnego - %d dni pozostało do zakończenia okresu próbnego - Ogólny błąd interfejsu API: %1$s diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 0aaf5b8b..c7f4b36c 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -73,9 +73,7 @@ Tela Ligada Mostrar Álbum Misturar - Playlist foi misturada Visualizador - Armazenando Baixando - %s Tocando misturado Playlist salva com sucesso. @@ -107,7 +105,6 @@ Gêneros Música Offline - Misturar Músicas Aleatórias Favoritas Músicas @@ -121,26 +118,19 @@ Falha ao excluir a playlist %s Downloads Sair - Navegação Configurações Atualizar Biblioteca de Mídia Mídia Offline - Ocorreu um erro de rede. Tentativa %1$d de %2$d. - Obtive %d Artistas. - Lendo do servidor. - Lendo do servidor. Pronto! Playlists Atualizar Informação Informação da playlist atualizada para %s Falha ao atualizar a informação da playlist para %s - Por favor aguarde… Álbuns Artistas Pesquisar Mostrar Mais Nada coincide, tente novamente - Clique para pesquisar Músicas Pesquisar Nenhuma mídia encontrada @@ -151,7 +141,6 @@ Selecionar Pasta Nenhum gênero encontrado Não existe nenhuma playlist no servidor - Contactando o servidor, por favor aguarde. Aparência Tamanho do Buffer Desativado @@ -191,8 +180,6 @@ Intervalo de Atualização do Chat Limpar Favoritos Limpar favoritos após terminar de tocar a música - Limpar Playlist - Limpar a playlist após terminar de tocar todas as músicas Limpar Histórico de Pesquisas Falha na conexão. Álbuns Padrões @@ -217,7 +204,6 @@ Será efetivado na próxima vez que o Android procurar por músicas em seu celular. Intervalo de Salto Especifique uma URL válida. - Especifique um nome de usuário válido (sem espaços). Máximo de Álbuns Máximo de Artistas 112 Kbps @@ -256,8 +242,6 @@ 3 músicas 5 músicas Ilimitado - Retomar ao Inserir Fone de Ouvido - O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo Lembre-se de configurar usuário e senha nos serviços Scrobble do servidor Registrar Minhas Músicas 1 @@ -286,10 +270,8 @@ Endereço do Servidor Nome Senha - Excluir Servidor Baixar imagens reduzidas do servidor ao invés do tamanho completo (economiza banda) Reduzir Arte dos Álbuns - Não usado Login Controles na Tela de Bloqueio Mostrar controles de reprodução na tela de bloqueio @@ -368,12 +350,6 @@ Mostrar Artista albumArt Anos Múltiplos - Retomar ao Conectar Dispositivo Bluetooth - Pausar ao Desconectar Dispositivo Bluetooth - Todos os dispositivos Bluetooth - Somente dispositivos de áudio (A2DP) - Desativado - Ativar isso pode ajudar com dispositivos Bluetooth mais antigos quando Reproduzir/Pausar não funciona corretamente Opções de Depuração Log de Depuração em Arquivo Os arquivos com log estão disponíveis em %1$s/%2$s @@ -402,10 +378,6 @@ %d música %d músicas - - %d dia restante do período de teste - %d dias restantes do período de teste - Erro de api genérico: %1$s diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 681f7630..04600188 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -61,9 +61,7 @@ Ecrã Ligado Mostrar Álbum Misturar - Playlist foi misturada Visualizador - Armazenando Descarregando - %s Tocando misturado Playlist salva com sucesso. @@ -95,7 +93,6 @@ Gêneros Música Offline - Misturar Músicas Aleatórias Favoritas Músicas @@ -106,26 +103,19 @@ Playlist apagada %s Falha ao apagar a playlist %s Sair - Navegação Configurações Refresh Biblioteca de Mídia Mídia Offline - Ocorreu um erro de rede. Tentativa %1$d de %2$d. - Obtive %d Artistas. - Lendo do servidor. - Lendo do servidor. Pronto! Playlists Atualizar Informação Informação da playlist atualizada para %s Falha ao atualizar a informação da playlist para %s - Por favor aguarde… Álbuns Artistas Pesquisar Mostrar Mais Nada coincide, tente novamente - Clique para pesquisar Músicas Pesquisar Nenhuma mídia encontrada @@ -136,7 +126,6 @@ Selecionar Pasta Nenhum gênero encontrado Não existe nenhuma playlist no servidor - Contactando o servidor, por favor aguarde. Aparência Tamanho do Buffer Disabilitando @@ -176,8 +165,6 @@ Intervalo de Atualização do Chat Limpar Favoritos Limpar favoritos após terminar de tocar a música - Limpar Playlist - Limpar a playlist após terminar de tocar todas as músicas Limpar Histórico de Pesquisas Falha na conexão. Álbuns Padrões @@ -200,7 +187,6 @@ Será realizado na próxima vez que o Android procurar por músicas em seu telemóvel. Intervalo de Salto Especifique uma URL válida. - Especifique um nome de usuário válido (sem espaços). Máximo de Álbuns Máximo de Artistas 112 Kbps @@ -239,8 +225,6 @@ 3 músicas 5 músicas Ilimitado - Retomar ao inserir Auscultadores - O aplicativo retomará a reprodução em pausa na inserção dos auscultadores no dispositivo. 1 10 100 @@ -265,10 +249,8 @@ Endereço do Servidor Nome Senha - Apagar Servidor Descarrega imagens reduzidas do servidor ao invés do tamanho completo (economiza banda) Reduzir Arte dos Álbuns - Não usado Login Controles no Ecrã de Bloqueio Mostra controles de reprodução no ecrã de bloqueio @@ -340,11 +322,6 @@ Mostrar Artista albumArt Múltiplos Anos - Resume when a Bluetooth device is connected - Pause when a Bluetooth device is disconnected - All Bluetooth devices - Only audio (A2DP) devices - Disabilitando Configured servers Are you sure you want to delete the server? Editing server @@ -361,10 +338,6 @@ %d música %d músicas - - %d dia restante do período de teste - %d dias restantes do período de teste - Erro de api genérico: %1$s diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 51a3a2bf..8c2fcbc0 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -70,9 +70,7 @@ Включение дисплея Показать альбом Случайное воспроизведение - Плейлист в случайном порядке Визуализатор - Буферизация Загрузка - %s Игра в случайном порядке Плейлист был успешно сохранен. @@ -104,7 +102,6 @@ Жанры Музыка Не в сети - Играть в случайном порядке Случайный Отмеченные Песни @@ -117,26 +114,19 @@ Удалить плейлист %s Не удалось удалить плейлист %s Выход - Навигация Настройки Обновить Медиа библиотека Медиа Оффлайн - Произошла ошибка сети. Повторная %1$d из %2$d. - Получил %d исполнители. - Чтение с сервера - Чтение с сервера. Готово! Плейлисты Обновление информации Обновлена информация о плейлисте для %s Не удалось обновить информацию о плейлисте для %s - Пожалуйста, подождите#8230; Альбомы Исполнители Поиск Показать еще Нет совпадений, пожалуйста попробуйте еще раз - Нажми для поиска Песни Поиск Медиа не найдена @@ -147,7 +137,6 @@ Выбрать папку Жанры не найдены Нет сохраненных плейлистов на сервере - Свяжитесь с сервером, пожалуйста, подождите. Появление Размер буфера Отключить @@ -187,8 +176,6 @@ Интервал обновления чата Очистить закладку Очистить закладку после завершения воспроизведения песни - Очистить плейлист - Очистите плейлист после завершения воспроизведения всех песен Очистить историю поиска Ошибка подключения. Альбомы по умолчанию @@ -211,7 +198,6 @@ Вступает в силу в следующий раз, Android сканирует ваш телефон на предмет музыки Пропустить интервал Пожалуйста, укажите действительный URL. - Пожалуйста, укажите правильное имя пользователя (без пробелов). Максимум альбомов Максимум исполнителей 112 Kbps @@ -250,8 +236,6 @@ 3 песни 5 песен Неограниченный - Возобновить подключение наушников - Приложение возобновит приостановленное воспроизведение после того, как в устройство будут вставлены проводные наушники. Не забудьте установить своего пользователя и пароль в Скроббл сервисах на сервере. Скробблить мои воспроизведения 1 @@ -278,10 +262,8 @@ Адрес сервера Имя Пароль - Удалить сервер Загрузка масштабированных изображений с сервера вместо полноразмерного (экономит трафик) Серверное масштабирование обложек альбомов - Неиспользуемый Имя пользователя Показать блокировку экрана Показать элементы управления воспроизведением на экране блокировки @@ -356,12 +338,6 @@ Показать исполнителей albumArt Несколько лет - Возобновить при подключении устройства Bluetooth - Пауза при отключении устройства Bluetooth - Все устройства Bluetooth - Только аудио (A2DP) устройства - Отключено - Включение этого может помочь со старыми устройствами Bluetooth, когда Воспроизведение/Пауза работает некорректно. Настройки отладки Записать журнал отладки в файл Файлы журнала доступны по адресу %1$s/%2$s @@ -390,12 +366,6 @@ %d песен %d песен - - Остался %d день пробного периода - Осталось %d дня пробного периода - Осталось %d дней пробного периода - Осталось %d дней пробного периода - Общая ошибка API: %1$s diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index bdf1e475..c1ee0952 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -74,9 +74,7 @@ 开启屏幕常亮 显示专辑 随机 - 已随机排列播放列表 可视化 - 缓冲中 下载中 - %s 随机播放 已成功保存播放列表。 @@ -108,7 +106,6 @@ 流派 音乐 离线 - 随机播放 随机 收藏夹 歌曲 @@ -122,26 +119,19 @@ 播放列表删除失败%s 下载 退出 - 导航 设置 刷新 媒体库 离线媒体 - 发生网络错误,正在重试 %1$d of %2$d. - 有 %d 位艺术家。 - 正在加载服务器。 - 正在加载服务器。完成! 播放列表 更新信息 已更新此播放列表信息 - %s 更新播放列表信息失败 - %s - 请稍等… 专辑 艺人 搜索 显示更多 没有匹配的结果,请重试 - 点击搜索 歌曲 搜索 找不到歌曲 @@ -152,7 +142,6 @@ 选择文件夹 找不到流派 服务器上没有保存的播放列表 - 服务器连接中,请稍等。 外观 缓冲长度 已禁用 @@ -193,8 +182,6 @@ 聊天消息刷新时间间隔 清空书签 歌曲播放完毕后清除书签 - 清空播放列表 - 所有歌曲播放完毕后清空播放列表 清空搜索历史 连接失败 默认专辑 @@ -217,7 +204,6 @@ 在安卓系统下次扫描音乐时生效。 快进间隔 请填写有效的URL。 - 请填写有效用户名 (请去除尾部空格)。 最大专辑 最大艺术家 112 Kbps @@ -256,8 +242,6 @@ 3 首歌 5 首歌 不限制 - 插入耳机时恢复播放 - 应用将在有线耳机插入设备时恢复已暂停的播放。 请记得在服务器上的 Scrobble 服务中设置您的用户名和密码 1 10 @@ -285,10 +269,8 @@ 服务器地址 名称 密码 - 删除服务器 从服务器下载缩放图像而不是全尺寸(节省数据流量) 服务器端专辑图片缩放 - 未启用 用户名 服务器颜色 锁屏显示控制器 @@ -370,13 +352,6 @@ 显示艺术家 albumArt Multiple Years - 连接蓝牙设备时恢复播放 - 断开蓝牙设备时暂停播放 - 所有蓝牙设备 - 仅音频 (A2DP) 设备 - 已禁用 - 启用蓝牙设备上的单键播放/暂停 - 当播放/暂停无法正常工作时,启用此功能可能对较旧的蓝牙设备有所帮助 调试选项 将调试日志写入文件 日志文件可在 %1$s/%2$s 获取 @@ -422,9 +397,6 @@ 在当前歌曲之后插入了 %d 首歌曲。 - - 试用期还剩 %d 天 - 一般api错误: %1$s diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index d12844da..784f9731 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -54,6 +54,5 @@ 名稱 已停用 註記 - 已停用 刪除 diff --git a/ultrasonic/src/main/res/values/arrays.xml b/ultrasonic/src/main/res/values/arrays.xml index 511d7be6..e645bdd6 100644 --- a/ultrasonic/src/main/res/values/arrays.xml +++ b/ultrasonic/src/main/res/values/arrays.xml @@ -231,11 +231,6 @@ @string/settings.share_hours @string/settings.share_days - - @string/settings.playback.bluetooth_all - @string/settings.playback.bluetooth_a2dp - @string/settings.playback.bluetooth_disabled - @string/language.default @string/language.zh_CN diff --git a/ultrasonic/src/main/res/values/playback_preferences_keys.xml b/ultrasonic/src/main/res/values/playback_preferences_keys.xml deleted file mode 100644 index 99c2cfa9..00000000 --- a/ultrasonic/src/main/res/values/playback_preferences_keys.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - playback.resume_play_on_headphones_plug - \ No newline at end of file diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 3025eb13..13260419 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -77,9 +77,7 @@ Shuffle Shuffle mode enabled Shuffle mode disabled - Playlist was shuffled Visualizer - Buffering Downloading - %s Playing shuffle Playlist was successfully saved. @@ -127,7 +125,6 @@ Music Offline %s - Set up Server - Shuffle Play Random Starred Songs @@ -141,26 +138,19 @@ Failed to delete playlist %s Downloads Exit - Navigation Settings Refresh Media Library Offline Media - A network error occurred. Retrying %1$d of %2$d. - Got %d Artists. - Reading from server. - Reading from server. Done! Playlists Update Information Updated playlist information for %s Failed to update playlist information for %s - Please wait… Albums Artists Search Show More No matches, please try again - Click to search Songs Search No media found @@ -172,9 +162,6 @@ Select Folder No genres found No saved playlists on server - Contacting server, please wait. - allowSelfSignedCertificate - enableLdapUserSupport Appearance Buffer Length Disabled @@ -215,8 +202,6 @@ Chat Refresh Interval Clear Bookmark Clear bookmark upon completion of playback of a song - Clear Playlist - Clear the playlist upon completion of playback of all songs Clear Search History Connection failure. Default Albums @@ -241,7 +226,6 @@ Takes effect next time Android scans your phone for music. Skip Interval Please specify a valid URL. - Please specify a valid username (no trailing spaces). Max Albums Max Artists 112 Kbps @@ -282,8 +266,6 @@ 3 songs 5 songs Unlimited - Resume on headphones insertion - App will resume paused playback on wired headphones insertion into device. Remember to set up your user and password in the Scrobble service(s) on the server Scrobble my plays 1 @@ -312,10 +294,8 @@ Server Address Name Password - Remove Server Download scaled images from the server instead of full size (saves bandwidth) Server-Side Album Art Scaling - Unused Username Server color Show Lock Screen Controls @@ -399,14 +379,6 @@ Show Artist albumArt Multiple Years - http://example.com - Resume when a Bluetooth device is connected - Pause when a Bluetooth device is disconnected - All Bluetooth devices - Only audio (A2DP) devices - Disabled - Bluetooth device with a single Play/Pause button - Enabling this may help with older Bluetooth devices when Play/Pause doesn\'t work correctly Debug options Write debug log to file The log files are available at %1$s/%2$s @@ -465,10 +437,6 @@ %d song inserted after current song %d songs inserted after current song - - %d day left of trial period - %d days left of trial period - Generic api error: %1$s From 41f5520f1f886723780033c4bd42feb3170c9785 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 5 Apr 2022 22:52:02 +0200 Subject: [PATCH 19/67] Remove unused resources & update lint baseline --- ultrasonic/lint-baseline.xml | 400 +++++------------------------------ 1 file changed, 57 insertions(+), 343 deletions(-) diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index cd999750..3e743d06 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -1,15 +1,15 @@ - + + errorLine1=" val view = inflater.inflate(R.layout.jukebox_volume, null)" + errorLine2=" ~~~~"> + file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt" + line="331" + column="66"/> - - - - @@ -56,17 +45,6 @@ column="73"/> - - - - @@ -88,7 +66,7 @@ errorLine2=" ~~~~~~~~"> @@ -99,7 +77,18 @@ errorLine2=" ~~~~~~~~"> + + + + @@ -114,17 +103,6 @@ column="10"/> - - - - - - - - @@ -154,7 +121,7 @@ errorLine2=" ~~~~~~~~"> @@ -165,7 +132,7 @@ errorLine2=" ~~~~~~~~"> @@ -176,7 +143,7 @@ errorLine2=" ~~~~~~~~"> @@ -187,96 +154,19 @@ errorLine2=" ~~~~~~~~"> + id="IntentFilterExportedReceiver" + message="As of Android 12, `android:exported` must be set; use `true` to make the activity \ available to other apps, and `false` otherwise." + errorLine1=" <receiver android:name=".receiver.MediaButtonIntentReceiver">" + errorLine2=" ~~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + file="src/main/AndroidManifest.xml" + line="130" + column="10"/> + + + + + message="The resource `R.string.common_play` appears to be unused" + errorLine1=" <string name="common.play">Play</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.common_play_previous` appears to be unused" + errorLine1=" <string name="common.play_previous">Play Previous</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -838,39 +596,6 @@ column="6"/> - - - - - - - - - - - - - - - - Date: Wed, 6 Apr 2022 21:00:27 +0200 Subject: [PATCH 20/67] Fix memory settings --- .circleci/config.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1daa6b7d..3bdd57a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,11 +1,17 @@ -version: 3 +version: 2.1 +parameters: + memory-config: + type: string + default: "-Xmx6g -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*" jobs: build: docker: - image: cimg/android:2022.03.1 working_directory: ~/ultrasonic environment: - JVM_OPTS: -Xmx3200m + JVM_OPTS: << pipeline.parameters.memory-config >> + JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >> + GRADLE_OPTS: << pipeline.parameters.memory-config >> steps: - checkout - restore_cache: @@ -18,6 +24,7 @@ jobs: command: | sed -i '/^org.gradle.jvmargs/d' gradle.properties sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties + cat gradle.properties - run: name: checkstyle command: ./gradlew -Pqc ktlintCheck @@ -44,7 +51,7 @@ jobs: - save_cache: paths: - ~/.gradle - key: v1-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }} + key: v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }} - store_artifacts: path: ultrasonic/build/reports destination: reports @@ -77,6 +84,10 @@ jobs: docker: - image: cimg/android:2022.03.1 working_directory: ~/ultrasonic + environment: + JVM_OPTS: << pipeline.parameters.memory-config >> + JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >> + GRADLE_OPTS: << pipeline.parameters.memory-config >> steps: - checkout - restore_cache: From a98c9e2ffd92df32cb927f4b4e657ce9ba762ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holger=20Mu=CC=88ller?= Date: Wed, 6 Apr 2022 10:27:51 +0200 Subject: [PATCH 21/67] fixed copy paste error with repeat button --- .../kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 08b4fb1a..475094db 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -436,7 +436,7 @@ class PlayerFragment : requireContext(), R.attr.media_repeat_off ) ) - shuffleButton.alpha = 0.6f + repeatButton.alpha = 0.6f } 1 -> { repeatButton.setImageDrawable( @@ -444,7 +444,7 @@ class PlayerFragment : requireContext(), R.attr.media_repeat_single ) ) - shuffleButton.alpha = 1f + repeatButton.alpha = 1f } 2 -> { repeatButton.setImageDrawable( @@ -452,7 +452,7 @@ class PlayerFragment : requireContext(), R.attr.media_repeat_all ) ) - shuffleButton.alpha = 1f + repeatButton.alpha = 1f } else -> { } From 5230ce011d701e4d7149ce476369424bf185a4c7 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 7 Apr 2022 15:14:14 +0200 Subject: [PATCH 22/67] Make full body logging an optional debug setting. --- .../ultrasonic/api/subsonic/SubsonicAPIClient.kt | 6 ++---- .../org/moire/ultrasonic/di/MusicServiceModule.kt | 13 ++++++++++++- .../moire/ultrasonic/fragment/EditServerFragment.kt | 3 ++- .../kotlin/org/moire/ultrasonic/util/Constants.kt | 1 + .../kotlin/org/moire/ultrasonic/util/Settings.kt | 3 +++ ultrasonic/src/main/res/values/strings.xml | 2 ++ ultrasonic/src/main/res/xml/settings.xml | 6 ++++++ 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index 9a7ea98d..6f93f0df 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -34,7 +34,7 @@ private const val READ_TIMEOUT = 60_000L */ class SubsonicAPIClient( config: SubsonicClientConfiguration, - private val okLogger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger.DEFAULT, + private val okLogger: HttpLoggingInterceptor, baseOkClient: OkHttpClient = OkHttpClient.Builder().build() ) { private val versionInterceptor = VersionInterceptor(config.minimalProtocolVersion) @@ -108,9 +108,7 @@ class SubsonicAPIClient( val api: SubsonicAPIDefinition get() = wrappedApi private fun OkHttpClient.Builder.addLogging() { - val loggingInterceptor = HttpLoggingInterceptor(okLogger) - loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY - this.addInterceptor(loggingInterceptor) + this.addInterceptor(okLogger) } private fun OkHttpClient.Builder.allowSelfSignedCertificates() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 7751557c..70262804 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -1,4 +1,5 @@ @file:JvmName("MusicServiceModule") + package org.moire.ultrasonic.di import kotlin.math.abs @@ -22,6 +23,7 @@ import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings /** * This Koin module contains the registration of classes related to the Music Services @@ -60,7 +62,16 @@ val musicServiceModule = module { ) } - single { TimberOkHttpLogger() } + single { + val level = if (Settings.debugFullBody) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.HEADERS + } + HttpLoggingInterceptor(TimberOkHttpLogger()) + .setLevel(level) + } + single { SubsonicAPIClient(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index 84de4a97..c5e19cfc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -19,6 +19,7 @@ import java.io.IOException import java.net.MalformedURLException import java.net.URL import java.util.Locale +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.BuildConfig @@ -408,7 +409,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { currentServerSetting!!.ldapSupport, BuildConfig.DEBUG ) - val subsonicApiClient = SubsonicAPIClient(configuration) + val subsonicApiClient = SubsonicAPIClient(configuration, get()) // Execute a ping to retrieve the API version. // This is accepted to fail if the authentication is incorrect yet. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index 6fb9efeb..c9113e35 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -108,6 +108,7 @@ object Constants { const val PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory" const val PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted" const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile" + const val PREFERENCES_KEY_DEBUG_LOG_BODY = "debugLogBody" const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage" const val FILENAME_PLAYLIST_SER = "downloadstate.ser" const val ALBUM_ART_FILE = "folder.jpeg" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 58458f9e..daa24d75 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -234,6 +234,9 @@ object Settings { @JvmStatic var debugLogToFile by BooleanSetting(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE, false) + @JvmStatic + var debugFullBody by BooleanSetting(Constants.PREFERENCES_KEY_DEBUG_LOG_BODY, false) + @JvmStatic val preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext()) diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 13260419..8e668ccd 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -381,6 +381,8 @@ Multiple Years Debug options Write debug log to file + Debug the body of HTTP calls + This will slow down streaming requests The log files are available at %1$s/%2$s There are %1$s log files taking up ~%2$s MB space in the %3$s directory. Do you want to keep these? Keep files diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index c03bdc62..2e23ed84 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -341,6 +341,12 @@ a:title="@string/settings.debug.log_to_file" a:summary="" app:iconSpaceReserved="false"/> + \ No newline at end of file From f936ad690c5b53fa30b302ae6a8448361a215f84 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 7 Apr 2022 15:27:52 +0200 Subject: [PATCH 23/67] Remove GC logging --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3bdd57a8..2e385a7b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,6 +3,9 @@ parameters: memory-config: type: string default: "-Xmx6g -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*" + memory-config-debug: + type: string + default: "-Xmx6g -Xms256m -XX:MaxMetaspaceSize=1g" jobs: build: docker: From 97798446201c37c02cc04056754a4c04b61a5a2e Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 7 Apr 2022 15:29:24 +0200 Subject: [PATCH 24/67] Solve more simple... --- .../ultrasonic/api/subsonic/SubsonicAPIClient.kt | 6 ++++-- .../org/moire/ultrasonic/di/MusicServiceModule.kt | 13 +------------ .../moire/ultrasonic/fragment/EditServerFragment.kt | 3 +-- .../kotlin/org/moire/ultrasonic/util/Constants.kt | 1 - .../kotlin/org/moire/ultrasonic/util/Settings.kt | 3 --- ultrasonic/src/main/res/values/strings.xml | 2 -- ultrasonic/src/main/res/xml/settings.xml | 6 ------ 7 files changed, 6 insertions(+), 28 deletions(-) diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index 6f93f0df..70d09743 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -34,7 +34,7 @@ private const val READ_TIMEOUT = 60_000L */ class SubsonicAPIClient( config: SubsonicClientConfiguration, - private val okLogger: HttpLoggingInterceptor, + private val okLogger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger.DEFAULT, baseOkClient: OkHttpClient = OkHttpClient.Builder().build() ) { private val versionInterceptor = VersionInterceptor(config.minimalProtocolVersion) @@ -108,7 +108,9 @@ class SubsonicAPIClient( val api: SubsonicAPIDefinition get() = wrappedApi private fun OkHttpClient.Builder.addLogging() { - this.addInterceptor(okLogger) + val loggingInterceptor = HttpLoggingInterceptor(okLogger) + loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS + this.addInterceptor(loggingInterceptor) } private fun OkHttpClient.Builder.allowSelfSignedCertificates() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 70262804..7751557c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -1,5 +1,4 @@ @file:JvmName("MusicServiceModule") - package org.moire.ultrasonic.di import kotlin.math.abs @@ -23,7 +22,6 @@ import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.Settings /** * This Koin module contains the registration of classes related to the Music Services @@ -62,16 +60,7 @@ val musicServiceModule = module { ) } - single { - val level = if (Settings.debugFullBody) { - HttpLoggingInterceptor.Level.BODY - } else { - HttpLoggingInterceptor.Level.HEADERS - } - HttpLoggingInterceptor(TimberOkHttpLogger()) - .setLevel(level) - } - + single { TimberOkHttpLogger() } single { SubsonicAPIClient(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index c5e19cfc..84de4a97 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -19,7 +19,6 @@ import java.io.IOException import java.net.MalformedURLException import java.net.URL import java.util.Locale -import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.BuildConfig @@ -409,7 +408,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { currentServerSetting!!.ldapSupport, BuildConfig.DEBUG ) - val subsonicApiClient = SubsonicAPIClient(configuration, get()) + val subsonicApiClient = SubsonicAPIClient(configuration) // Execute a ping to retrieve the API version. // This is accepted to fail if the authentication is incorrect yet. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index c9113e35..6fb9efeb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -108,7 +108,6 @@ object Constants { const val PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory" const val PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted" const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile" - const val PREFERENCES_KEY_DEBUG_LOG_BODY = "debugLogBody" const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage" const val FILENAME_PLAYLIST_SER = "downloadstate.ser" const val ALBUM_ART_FILE = "folder.jpeg" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index daa24d75..58458f9e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -234,9 +234,6 @@ object Settings { @JvmStatic var debugLogToFile by BooleanSetting(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE, false) - @JvmStatic - var debugFullBody by BooleanSetting(Constants.PREFERENCES_KEY_DEBUG_LOG_BODY, false) - @JvmStatic val preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext()) diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 8e668ccd..13260419 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -381,8 +381,6 @@ Multiple Years Debug options Write debug log to file - Debug the body of HTTP calls - This will slow down streaming requests The log files are available at %1$s/%2$s There are %1$s log files taking up ~%2$s MB space in the %3$s directory. Do you want to keep these? Keep files diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 2e23ed84..c03bdc62 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -341,12 +341,6 @@ a:title="@string/settings.debug.log_to_file" a:summary="" app:iconSpaceReserved="false"/> - \ No newline at end of file From 6da83db9df1eda69aad81a0a99e008f1b985b327 Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 8 Apr 2022 18:07:13 +0200 Subject: [PATCH 25/67] Set memory to 3.2g --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2e385a7b..fac63d0b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,11 +1,11 @@ version: 2.1 parameters: memory-config: - type: string - default: "-Xmx6g -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*" - memory-config-debug: type: string - default: "-Xmx6g -Xms256m -XX:MaxMetaspaceSize=1g" + default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g" + memory-config-debug: + type: string + default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*" jobs: build: docker: From 3ca25ed1c6687a8cf9c25912bf92708ba5763e05 Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 8 Apr 2022 18:08:56 +0200 Subject: [PATCH 26/67] Rework ActiveServer handling. Remove blocking call on setting the server. Implement offline server display more cleanly. Reconfigure the SourceFactory when the active server has changed --- detekt-baseline.xml | 4 +- .../ultrasonic/activity/NavigationActivity.kt | 22 +++---- .../ultrasonic/adapters/ServerRowAdapter.kt | 47 ++++++++------ .../ultrasonic/data/ActiveServerProvider.kt | 39 +++++++++--- .../fragment/ServerSelectorFragment.kt | 63 ++++++++----------- .../ultrasonic/model/ServerSettingsModel.kt | 11 ++-- .../ultrasonic/playback/APIDataSource.kt | 11 +++- .../ultrasonic/playback/PlaybackService.kt | 17 ++++- .../service/MediaPlayerController.kt | 8 +++ .../ultrasonic/service/MusicServiceFactory.kt | 9 ++- .../org/moire/ultrasonic/service/RxBus.kt | 39 ++---------- 11 files changed, 147 insertions(+), 123 deletions(-) diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 535a4028..205ff394 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -2,8 +2,6 @@ - ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background - ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) ) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name) @@ -12,7 +10,7 @@ LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) - LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) + LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192 MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index fac3e504..5eacc74a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -40,7 +40,7 @@ import androidx.navigation.ui.setupWithNavController import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.google.android.material.navigation.NavigationView -import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.disposables.CompositeDisposable import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R @@ -54,6 +54,7 @@ import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.InfoDialog @@ -83,8 +84,8 @@ class NavigationActivity : AppCompatActivity() { private var headerBackgroundImage: ImageView? = null private lateinit var appBarConfiguration: AppBarConfiguration - private var themeChangedEventSubscription: Disposable? = null - private var playerStateSubscription: Disposable? = null + + private var rxBusSubscription: CompositeDisposable = CompositeDisposable() private val serverSettingsModel: ServerSettingsModel by viewModel() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() @@ -181,25 +182,25 @@ class NavigationActivity : AppCompatActivity() { hideNowPlaying() } - playerStateSubscription = RxBus.playerStateObservable.subscribe { + rxBusSubscription += RxBus.playerStateObservable.subscribe { if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED) showNowPlaying() else hideNowPlaying() } - themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe { + rxBusSubscription += RxBus.themeChangedEventObservable.subscribe { recreate() } + rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { + updateNavigationHeaderForServer() + } + serverRepository.liveServerCount().observe(this) { count -> cachedServerCount = count ?: 0 updateNavigationHeaderForServer() } - - ActiveServerProvider.liveActiveServerId.observe(this) { - updateNavigationHeaderForServer() - } } private fun updateNavigationHeaderForServer() { @@ -239,8 +240,7 @@ class NavigationActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() - themeChangedEventSubscription?.dispose() - playerStateSubscription?.dispose() + rxBusSubscription.dispose() imageLoaderProvider.clearImageLoader() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt index 8790bf7c..1ec4fbe7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ServerRowAdapter.kt @@ -30,7 +30,7 @@ import org.moire.ultrasonic.util.Util */ internal class ServerRowAdapter( private var context: Context, - private var data: Array, + passedData: Array, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, @@ -38,6 +38,12 @@ internal class ServerRowAdapter( private val serverEditRequestedCallback: ((Int) -> Unit) ) : BaseAdapter() { + private var data: MutableList = mutableListOf() + + init { + setData(passedData) + } + companion object { private const val MENU_ID_EDIT = 1 private const val MENU_ID_DELETE = 2 @@ -49,12 +55,19 @@ internal class ServerRowAdapter( context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater fun setData(data: Array) { - this.data = data + this.data.clear() + + // In read mode show the offline server as well + if (!manageMode) { + this.data.add(ActiveServerProvider.OFFLINE_DB) + } + + this.data.addAll(data) notifyDataSetChanged() } override fun getCount(): Int { - return if (manageMode) data.size else data.size + 1 + return data.size } override fun getItem(position: Int): Any { @@ -69,11 +82,11 @@ internal class ServerRowAdapter( * Creates the Row representation of a Server Setting */ @Suppress("LongMethod") - override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { - var index = position + override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? { + var position = pos // Skip "Offline" in manage mode - if (manageMode) index++ + if (manageMode) position++ var vi: View? = convertView if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false) @@ -83,22 +96,17 @@ internal class ServerRowAdapter( val layout = vi?.findViewById(R.id.server_layout) val image = vi?.findViewById(R.id.server_image) val serverMenu = vi?.findViewById(R.id.server_menu) - val setting = data.singleOrNull { t -> t.index == index } + val setting = data.singleOrNull { t -> t.index == position } - if (index == 0) { - text?.text = context.getString(R.string.main_offline) - description?.text = "" - } else { - text?.text = setting?.name ?: "" - description?.text = setting?.url ?: "" - if (setting == null) serverMenu?.visibility = View.INVISIBLE - } + text?.text = setting?.name ?: "" + description?.text = setting?.url ?: "" + if (setting == null) serverMenu?.visibility = View.INVISIBLE val icon: Drawable? val background: Drawable? // Configure icons for the row - if (index == 0) { + if (setting?.id == ActiveServerProvider.OFFLINE_DB_ID) { serverMenu?.visibility = View.INVISIBLE icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off) background = ContextCompat.getDrawable(context, R.drawable.circle) @@ -116,7 +124,7 @@ internal class ServerRowAdapter( image?.background = background // Highlight the Active Server's row by changing its background - if (index == activeServerProvider.getActiveServer().index) { + if (position == activeServerProvider.getActiveServer().index) { layout?.background = ContextCompat.getDrawable(context, R.drawable.select_ripple) } else { layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple) @@ -128,7 +136,7 @@ internal class ServerRowAdapter( R.drawable.select_ripple_circle ) - serverMenu?.setOnClickListener { view -> serverMenuClick(view, index) } + serverMenu?.setOnClickListener { view -> serverMenuClick(view, position) } return vi } @@ -192,7 +200,8 @@ internal class ServerRowAdapter( return true } MENU_ID_DELETE -> { - serverDeletedCallback.invoke(position) + val server = getItem(position) as ServerSetting + serverDeletedCallback.invoke(server.id) return true } MENU_ID_UP -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index e132c67f..1526dd7e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -1,6 +1,5 @@ package org.moire.ultrasonic.data -import androidx.lifecycle.MutableLiveData import androidx.room.Room import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -11,6 +10,7 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.di.DB_FILENAME import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService +import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -52,12 +52,32 @@ class ActiveServerProvider( } // Fallback to Offline - setActiveServerId(OFFLINE_DB_ID) + setActiveServerById(OFFLINE_DB_ID) } return OFFLINE_DB } + /** + * Resolves the index (sort order) of a server to its id (unique) + * @param index: The index of the server in the server selector + * @return id: The unique id of the server + */ + fun getServerIdFromIndex(index: Int): Int { + if (index <= OFFLINE_DB_INDEX) { + // Offline mode is selected + return OFFLINE_DB_ID + } + + var id: Int + + runBlocking { + id = repository.findByIndex(index)?.id ?: 0 + } + + return id + } + /** * Sets the Active Server by the Server Index in the Server Selector List * @param index: The index of the Active Server in the Server Selector List @@ -66,13 +86,13 @@ class ActiveServerProvider( Timber.d("setActiveServerByIndex $index") if (index <= OFFLINE_DB_INDEX) { // Offline mode is selected - setActiveServerId(OFFLINE_DB_ID) + setActiveServerById(OFFLINE_DB_ID) return } launch { val serverId = repository.findByIndex(index)?.id ?: 0 - setActiveServerId(serverId) + setActiveServerById(serverId) } } @@ -180,8 +200,6 @@ class ActiveServerProvider( minimumApiVersion = null ) - val liveActiveServerId: MutableLiveData = MutableLiveData(getActiveServerId()) - /** * Queries if the Active Server is the "Offline" mode of Ultrasonic * @return True, if the "Offline" mode is selected @@ -198,13 +216,16 @@ class ActiveServerProvider( } /** - * Sets the Id of the Active Server + * Sets the Active Server by its unique id + * @param serverId: The id of the desired server */ - fun setActiveServerId(serverId: Int) { + fun setActiveServerById(serverId: Int) { resetMusicService() Settings.activeServer = serverId - liveActiveServerId.postValue(serverId) + + Timber.i("setActiveServerById done, new id: %s", serverId) + RxBus.activeServerChangePublisher.onNext(serverId) } /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index 730d8bd1..0798950b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -9,14 +9,12 @@ import android.widget.ListView import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ServerRowAdapter import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.service.MediaPlayerController @@ -26,6 +24,8 @@ import timber.log.Timber /** * Displays the list of configured servers, they can be selected or edited + * + * TODO: Manage mode is unused. Remove it... */ class ServerSelectorFragment : Fragment() { companion object { @@ -59,6 +59,7 @@ class ServerSelectorFragment : Fragment() { SERVER_SELECTOR_MANAGE_MODE, false ) ?: false + if (manageMode) { FragmentTitle.setTitle(this, R.string.settings_server_manage_servers) } else { @@ -72,31 +73,26 @@ class ServerSelectorFragment : Fragment() { serverSettingsModel, activeServerProvider, manageMode, - { - i -> - onServerDeleted(i) - }, - { - i -> - editServer(i) - } + ::deleteServerById, + ::editServerByIndex ) listView?.adapter = serverRowAdapter - listView?.onItemClickListener = AdapterView.OnItemClickListener { - _, _, position, _ -> + listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ -> + + val server = parent.getItemAtPosition(position) as ServerSetting if (manageMode) { - editServer(position + 1) + editServerByIndex(position + 1) } else { - setActiveServer(position) + setActiveServerById(server.id) findNavController().popBackStack(R.id.mainFragment, false) } } val fab = view.findViewById(R.id.server_add_fab) fab.setOnClickListener { - editServer(-1) + editServerByIndex(-1) } } @@ -113,44 +109,37 @@ class ServerSelectorFragment : Fragment() { /** * Sets the active server when a list item is clicked */ - private fun setActiveServer(index: Int) { - // TODO this is still a blocking call - we shouldn't leave this activity before the active server is updated. - // Maybe this can be refactored by using LiveData, or this can be made more user friendly with a ProgressDialog - runBlocking { - controller.clearIncomplete() - withContext(Dispatchers.IO) { - if (activeServerProvider.getActiveServer().index != index) { - activeServerProvider.setActiveServerByIndex(index) - } - } - controller.isJukeboxEnabled = - activeServerProvider.getActiveServer().jukeboxByDefault + private fun setActiveServerById(id: Int) { + + controller.clearIncomplete() + + if (activeServerProvider.getActiveServer().id != id) { + ActiveServerProvider.setActiveServerById(id) } - Timber.i("Active server was set to: $index") } /** * This Callback handles the deletion of a Server Setting */ - private fun onServerDeleted(index: Int) { + private fun deleteServerById(id: Int) { ErrorDialog.Builder(context) .setTitle(R.string.server_menu_delete) .setMessage(R.string.server_selector_delete_confirmation) .setPositiveButton(R.string.common_delete) { dialog, _ -> dialog.dismiss() - val activeServerIndex = activeServerProvider.getActiveServer().index - val id = ActiveServerProvider.getActiveServerId() + // Get the id of the current active server + val activeServerId = ActiveServerProvider.getActiveServerId() // If the currently active server is deleted, go offline - if (index == activeServerIndex) setActiveServer(-1) + if (id == activeServerId) setActiveServerById(ActiveServerProvider.OFFLINE_DB_ID) - serverSettingsModel.deleteItem(index) + serverSettingsModel.deleteItemById(id) // Clear the metadata cache - activeServerProvider.deleteMetaDatabase(id) + activeServerProvider.deleteMetaDatabase(activeServerId) - Timber.i("Server deleted: $index") + Timber.i("Server deleted, id: $id") } .setNegativeButton(R.string.common_cancel) { dialog, _ -> dialog.dismiss() @@ -161,7 +150,7 @@ class ServerSelectorFragment : Fragment() { /** * Starts the Edit Server Fragment to edit the details of a server */ - private fun editServer(index: Int) { + private fun editServerByIndex(index: Int) { val bundle = Bundle() bundle.putInt(EDIT_SERVER_INTENT_INDEX, index) findNavController().navigate(R.id.serverSelectorToEditServer, bundle) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt index 4f4d1226..3e62541d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ServerSettingsModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.runBlocking import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSettingDao import timber.log.Timber @@ -30,6 +31,8 @@ class ServerSettingsModel( /** * Retrieves the list of the configured servers from the database. * This function is asynchronous, uses LiveData to provide the Setting. + * + * It does not include the Offline "server". */ fun getServerList(): LiveData> { // This check should run before returning any result @@ -92,14 +95,14 @@ class ServerSettingsModel( /** * Removes a Setting from the database */ - fun deleteItem(index: Int) { - if (index == 0) return + fun deleteItemById(id: Int) { + if (id == OFFLINE_DB_ID) return viewModelScope.launch { - val itemToBeDeleted = repository.findByIndex(index) + val itemToBeDeleted = repository.findById(id) if (itemToBeDeleted != null) { repository.delete(itemToBeDeleted) - Timber.d("deleteItem deleted index: $index") + Timber.d("deleteItem deleted id: $id") reindexSettings() activeServerProvider.invalidateCache() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt index a2747301..75789ae7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -7,12 +7,12 @@ package org.moire.ultrasonic.playback +import android.annotation.SuppressLint import android.net.Uri import androidx.core.net.toUri import androidx.media3.common.C import androidx.media3.common.PlaybackException import androidx.media3.common.util.Assertions -import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util import androidx.media3.datasource.BaseDataSource import androidx.media3.datasource.DataSourceException @@ -43,15 +43,15 @@ import timber.log.Timber * priority) the `dataSpec`, [.setRequestProperty] and the default parameters used to * construct the instance. */ +@SuppressLint("UnsafeOptInUsageError") @Suppress("MagicNumber") -@UnstableApi open class APIDataSource private constructor( subsonicAPIClient: SubsonicAPIClient ) : BaseDataSource(true), HttpDataSource { /** [DataSource.Factory] for [APIDataSource] instances. */ - class Factory(private val subsonicAPIClient: SubsonicAPIClient) : HttpDataSource.Factory { + class Factory(private var subsonicAPIClient: SubsonicAPIClient) : HttpDataSource.Factory { private val defaultRequestProperties: RequestProperties = RequestProperties() private var transferListener: TransferListener? = null @@ -75,6 +75,10 @@ open class APIDataSource private constructor( return this } + fun setAPIClient(newClient: SubsonicAPIClient) { + this.subsonicAPIClient = newClient + } + override fun createDataSource(): APIDataSource { val dataSource = APIDataSource( subsonicAPIClient @@ -318,6 +322,7 @@ open class APIDataSource private constructor( return C.RESULT_END_OF_INPUT } bytesRead += read.toLong() + // TODO // bytesTransferred(read) return read } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 03f556c5..2a08df67 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -29,21 +29,27 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession +import io.reactivex.rxjava3.disposables.CompositeDisposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.service.RxBus +import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var player: ExoPlayer private lateinit var mediaLibrarySession: MediaLibrarySession + private lateinit var apiDataSource: APIDataSource.Factory private lateinit var dataSourceFactory: DataSource.Factory private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback + private var rxBusSubscription = CompositeDisposable() + /* * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, * and thereby customarily it is required to rebuild it.. @@ -64,11 +70,18 @@ class PlaybackService : MediaLibraryService(), KoinComponent { override fun onCreate() { super.onCreate() initializeSessionAndPlayer() + + rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { + // Update the API endpoint when the active server has changed + val newClient: SubsonicAPIClient by inject() + apiDataSource.setAPIClient(newClient) + } } override fun onDestroy() { player.release() mediaLibrarySession.release() + rxBusSubscription.dispose() super.onDestroy() } @@ -88,8 +101,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent { val subsonicAPIClient: SubsonicAPIClient by inject() // Create a MediaSource which passes calls through our OkHttp Stack + apiDataSource = APIDataSource.Factory(subsonicAPIClient) + dataSourceFactory = APIDataSource.Factory(subsonicAPIClient) - val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(dataSourceFactory) + val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(apiDataSource) // Create a renderer with HW rendering support val renderer = DefaultRenderersFactory(this) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index d591d630..98bb9206 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -18,6 +18,7 @@ import androidx.media3.common.Timeline import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors +import io.reactivex.rxjava3.disposables.CompositeDisposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp @@ -61,6 +62,8 @@ class MediaPlayerController( private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() private val activeServerProvider: ActiveServerProvider by inject() + private val rxBusSubscription: CompositeDisposable = CompositeDisposable() + private var sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java)) @@ -109,6 +112,11 @@ class MediaPlayerController( // controller?.play() }, MoreExecutors.directExecutor()) + rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { + // Update the Jukebox state when the active server has changed + isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault + } + created = true Timber.i("MediaPlayerController created") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt index 02ddb4a5..865dc29d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt @@ -27,8 +27,14 @@ import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.di.OFFLINE_MUSIC_SERVICE import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE import org.moire.ultrasonic.di.musicServiceModule +import timber.log.Timber -// TODO Refactor everywhere to use DI way to get MusicService, and then remove this class +/* + * TODO: When resetMusicService is called, a large number of classes are completely newly instantiated, + * which take quite a bit of time. + * + * Instead it would probably be faster to listen to Rx + */ object MusicServiceFactory : KoinComponent { @JvmStatic fun getMusicService(): MusicService { @@ -45,6 +51,7 @@ object MusicServiceFactory : KoinComponent { */ @JvmStatic fun resetMusicService() { + Timber.i("Regenerating Koin Music Service Module") unloadKoinModules(musicServiceModule) loadKoinModules(musicServiceModule) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 1ffb89b7..eb49df0d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -1,23 +1,19 @@ package org.moire.ultrasonic.service -import android.os.Bundle -import android.support.v4.media.session.MediaSessionCompat import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.subjects.PublishSubject -import java.util.concurrent.TimeUnit import org.moire.ultrasonic.domain.PlayerState class RxBus { companion object { - var mediaSessionTokenPublisher: PublishSubject = + + var activeServerChangePublisher: PublishSubject = PublishSubject.create() - val mediaSessionTokenObservable: Observable = - mediaSessionTokenPublisher.observeOn(AndroidSchedulers.mainThread()) - .replay(1) - .autoConnect(0) + var activeServerChangeObservable: Observable = + activeServerChangePublisher.observeOn(AndroidSchedulers.mainThread()) val themeChangedEventPublisher: PublishSubject = PublishSubject.create() @@ -43,38 +39,11 @@ class RxBus { .replay(1) .autoConnect(0) - val playbackPositionPublisher: PublishSubject = - PublishSubject.create() - val playbackPositionObservable: Observable = - playbackPositionPublisher.observeOn(AndroidSchedulers.mainThread()) - .throttleFirst(1, TimeUnit.SECONDS) - .replay(1) - .autoConnect(0) - // Commands val dismissNowPlayingCommandPublisher: PublishSubject = PublishSubject.create() val dismissNowPlayingCommandObservable: Observable = dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) - - val playFromMediaIdCommandPublisher: PublishSubject> = - PublishSubject.create() - val playFromMediaIdCommandObservable: Observable> = - playFromMediaIdCommandPublisher.observeOn(AndroidSchedulers.mainThread()) - - val playFromSearchCommandPublisher: PublishSubject> = - PublishSubject.create() - val playFromSearchCommandObservable: Observable> = - playFromSearchCommandPublisher.observeOn(AndroidSchedulers.mainThread()) - - val skipToQueueItemCommandPublisher: PublishSubject = - PublishSubject.create() - val skipToQueueItemCommandObservable: Observable = - skipToQueueItemCommandPublisher.observeOn(AndroidSchedulers.mainThread()) - - fun releaseMediaSessionToken() { - mediaSessionTokenPublisher = PublishSubject.create() - } } data class StateWithTrack(val state: PlayerState, val track: DownloadFile?, val index: Int = -1) From e5021959c3eb2c3f34c37c72dd6e7d5c9441ef4c Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 8 Apr 2022 21:24:04 +0200 Subject: [PATCH 27/67] Fix a small bug in togglePlayPause() --- .../org/moire/ultrasonic/service/MediaPlayerController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 98bb9206..e3978c22 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -289,7 +289,7 @@ class MediaPlayerController( @Synchronized fun togglePlayPause() { if (playbackState == Player.STATE_IDLE) autoPlayStart = true - if (controller?.isPlaying == false) { + if (controller?.isPlaying == true) { controller?.pause() } else { controller?.play() From 92ef78a36ac4054d29e3002101e8f0ecc8a6ad6b Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 8 Apr 2022 21:28:14 +0200 Subject: [PATCH 28/67] Move alpha value to const val. --- .../moire/ultrasonic/fragment/PlayerFragment.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 475094db..417af338 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -422,9 +422,9 @@ class PlayerFragment : private fun updateShuffleButtonState(isEnabled: Boolean) { if (isEnabled) { - shuffleButton.alpha = 1f + shuffleButton.alpha = ALPHA_ACTIVATED } else { - shuffleButton.alpha = 0.6f + shuffleButton.alpha = ALPHA_DEACTIVATED } } @@ -436,7 +436,7 @@ class PlayerFragment : requireContext(), R.attr.media_repeat_off ) ) - repeatButton.alpha = 0.6f + repeatButton.alpha = ALPHA_DEACTIVATED } 1 -> { repeatButton.setImageDrawable( @@ -444,7 +444,7 @@ class PlayerFragment : requireContext(), R.attr.media_repeat_single ) ) - repeatButton.alpha = 1f + repeatButton.alpha = ALPHA_ACTIVATED } 2 -> { repeatButton.setImageDrawable( @@ -452,7 +452,7 @@ class PlayerFragment : requireContext(), R.attr.media_repeat_all ) ) - repeatButton.alpha = 1f + repeatButton.alpha = ALPHA_ACTIVATED } else -> { } @@ -954,7 +954,7 @@ class PlayerFragment : super.onSelectedChanged(viewHolder, actionState) if (actionState == ACTION_STATE_DRAG) { - viewHolder?.itemView?.alpha = 0.6f + viewHolder?.itemView?.alpha = ALPHA_DEACTIVATED } } @@ -1256,5 +1256,7 @@ class PlayerFragment : companion object { private const val PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5 + private const val ALPHA_ACTIVATED = 1f + private const val ALPHA_DEACTIVATED = 0.4f } } From 7d33770fd6e8a661a76ea1dd20b2f4d04224d699 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 12 Apr 2022 17:12:17 +0200 Subject: [PATCH 29/67] Add some more logging calls --- .../ultrasonic/playback/APIDataSource.kt | 24 +++++++++---------- .../ultrasonic/playback/CachedDataSource.kt | 10 ++++++++ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt index 75789ae7..f5b652ad 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -132,6 +132,13 @@ open class APIDataSource private constructor( @Suppress("LongMethod", "NestedBlockDepth") @Throws(HttpDataSourceException::class) override fun open(dataSpec: DataSpec): Long { + Timber.i( + "APIDatasource: Open: %s %s %s", + dataSpec.uri, + dataSpec.position, + dataSpec.toString() + ) + this.dataSpec = dataSpec bytesRead = 0 bytesToRead = 0 @@ -140,22 +147,15 @@ open class APIDataSource private constructor( val components = dataSpec.uri.toString().split('|') val id = components[0] val bitrate = components[1].toInt() - - Timber.i("DATASOURCE: %s", "Start") - // FIXME - // WRONG API CLIENT val request = subsonicAPIClient.api.stream(id, bitrate, offset = 0) val response: retrofit2.Response? val streamResponse: StreamResponse - Timber.i("DATASOURCE: %s", "Start2") + try { this.response = request.execute() - Timber.i("DATASOURCE: %s", "Start3") response = this.response streamResponse = response!!.toStreamResponse() - Timber.i("DATASOURCE: %s", "Start4") responseByteStream = streamResponse.stream - Timber.i("DATASOURCE: %s", "Start5") } catch (e: IOException) { throw HttpDataSourceException.createForIOException( e, dataSpec, HttpDataSourceException.TYPE_OPEN @@ -193,8 +193,6 @@ open class APIDataSource private constructor( ) } - Timber.i("DATASOURCE: %s", "Start6") - // If we requested a range starting from a non-zero position and received a 200 rather than a // 206, then the server does not support partial requests. We'll need to manually skip to the // requested position. @@ -216,13 +214,13 @@ open class APIDataSource private constructor( closeConnectionQuietly() throw e } - Timber.i("DATASOURCE: %s", "Start7") return bytesToRead } @Throws(HttpDataSourceException::class) override fun read(buffer: ByteArray, offset: Int, length: Int): Int { + Timber.i("APIDatasource: Read: %s %s %s", buffer, offset, length) return try { readInternal(buffer, offset, length) } catch (e: IOException) { @@ -233,6 +231,7 @@ open class APIDataSource private constructor( } override fun close() { + Timber.i("APIDatasource: Close") if (openedNetwork) { openedNetwork = false transferEnded() @@ -322,8 +321,7 @@ open class APIDataSource private constructor( return C.RESULT_END_OF_INPUT } bytesRead += read.toLong() - // TODO - // bytesTransferred(read) + bytesTransferred(read) return read } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index e79e66dd..e200b5f4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -22,6 +22,7 @@ import java.io.InputStream import org.moire.ultrasonic.util.AbstractFile import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Storage +import timber.log.Timber @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) class CachedDataSource( @@ -92,6 +93,13 @@ class CachedDataSource( private var cacheFile: AbstractFile? = null override fun open(dataSpec: DataSpec): Long { + Timber.i( + "CachedDatasource: Open: %s %s %s", + dataSpec.uri, + dataSpec.position, + dataSpec.toString() + ) + this.dataSpec = dataSpec bytesRead = 0 bytesToRead = 0 @@ -112,6 +120,7 @@ class CachedDataSource( } override fun read(buffer: ByteArray, offset: Int, length: Int): Int { + Timber.i("CachedDatasource: Read: %s %s %s", buffer, offset, length) return if (cachePath != null) { try { readInternal(buffer, offset, length) @@ -156,6 +165,7 @@ class CachedDataSource( } override fun close() { + Timber.i("CachedDatasource: close") if (openedFile) { openedFile = false responseByteStream?.close() From 1564379bd1c437a0224fd3505bf201d18d33bdcc Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 16 Apr 2022 00:04:52 +0200 Subject: [PATCH 30/67] Fix a bunch of problems with the DataSources --- .../ultrasonic/fragment/PlayerFragment.kt | 3 +- .../ultrasonic/playback/APIDataSource.kt | 4 +- .../ultrasonic/playback/CachedDataSource.kt | 121 ++++++++++-------- .../moire/ultrasonic/service/Downloader.kt | 3 +- .../service/MediaPlayerController.kt | 2 + ultrasonic/src/main/res/values-cs/strings.xml | 1 - ultrasonic/src/main/res/values-de/strings.xml | 1 - ultrasonic/src/main/res/values-es/strings.xml | 1 - ultrasonic/src/main/res/values-fr/strings.xml | 1 - ultrasonic/src/main/res/values-hu/strings.xml | 1 - ultrasonic/src/main/res/values-it/strings.xml | 1 - ultrasonic/src/main/res/values-nl/strings.xml | 1 - ultrasonic/src/main/res/values-pl/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - ultrasonic/src/main/res/values-pt/strings.xml | 1 - ultrasonic/src/main/res/values-ru/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 1 - ultrasonic/src/main/res/values/strings.xml | 2 +- 18 files changed, 78 insertions(+), 69 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 417af338..785ccd19 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -1079,8 +1079,7 @@ class PlayerFragment : Player.STATE_BUFFERING -> { val downloadStatus = resources.getString( - R.string.download_playerstate_downloading, - Util.formatPercentage(progress) + R.string.download_playerstate_loading ) progressBar.secondaryProgress = progress setTitle(this@PlayerFragment, downloadStatus) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt index f5b652ad..0a071dbe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -147,7 +147,7 @@ open class APIDataSource private constructor( val components = dataSpec.uri.toString().split('|') val id = components[0] val bitrate = components[1].toInt() - val request = subsonicAPIClient.api.stream(id, bitrate, offset = 0) + val request = subsonicAPIClient.api.stream(id, bitrate, offset = dataSpec.position) val response: retrofit2.Response? val streamResponse: StreamResponse @@ -220,7 +220,7 @@ open class APIDataSource private constructor( @Throws(HttpDataSourceException::class) override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - Timber.i("APIDatasource: Read: %s %s %s", buffer, offset, length) + Timber.d("APIDatasource: Read: %s %s", offset, length) return try { readInternal(buffer, offset, length) } catch (e: IOException) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index e200b5f4..19789fd9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -10,15 +10,15 @@ package org.moire.ultrasonic.playback import android.net.Uri import androidx.core.net.toUri import androidx.media3.common.C +import androidx.media3.common.PlaybackException import androidx.media3.common.util.Util import androidx.media3.datasource.BaseDataSource import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSpec -import androidx.media3.datasource.HttpDataSource -import androidx.media3.datasource.cache.CacheDataSource -import androidx.media3.datasource.cache.CacheDataSource.CacheIgnoredReason +import androidx.media3.datasource.HttpDataSource.HttpDataSourceException import java.io.IOException import java.io.InputStream +import java.io.InterruptedIOException import org.moire.ultrasonic.util.AbstractFile import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Storage @@ -26,30 +26,13 @@ import timber.log.Timber @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) class CachedDataSource( - private var upstreamDataSource: DataSource, - private var eventListener: EventListener? + private var upstreamDataSource: DataSource ) : BaseDataSource(false) { class Factory( private var upstreamDataSourceFactory: DataSource.Factory ) : DataSource.Factory { - private var eventListener: EventListener? = null - - /** - * Sets the {link EventListener} to which events are delivered. - * - * - * The default is `null`. - * - * @param eventListener The [EventListener]. - * @return This factory. - */ - fun setEventListener(eventListener: EventListener?): Factory { - this.eventListener = eventListener - return this - } - override fun createDataSource(): CachedDataSource { return createDataSourceInternal( upstreamDataSourceFactory.createDataSource() @@ -60,30 +43,11 @@ class CachedDataSource( upstreamDataSource: DataSource ): CachedDataSource { return CachedDataSource( - upstreamDataSource, - eventListener + upstreamDataSource ) } } - /** Listener of [CacheDataSource] events. */ - interface EventListener { - /** - * Called when bytes have been read from the cache. - * - * @param cacheSizeBytes Current cache size in bytes. - * @param cachedBytesRead Total bytes read from the cache since this method was last called. - */ - fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) - - /** - * Called when the current request ignores cache. - * - * @param reason Reason cache is bypassed. - */ - fun onCacheIgnored(reason: @CacheIgnoredReason Int) - } - private var bytesToRead: Long = 0 private var bytesRead: Long = 0 private var dataSpec: DataSpec? = null @@ -94,9 +58,7 @@ class CachedDataSource( override fun open(dataSpec: DataSpec): Long { Timber.i( - "CachedDatasource: Open: %s %s %s", - dataSpec.uri, - dataSpec.position, + "CachedDatasource: Open: %s", dataSpec.toString() ) @@ -112,6 +74,8 @@ class CachedDataSource( if (cacheLength > 0) { transferInitializing(dataSpec) bytesToRead = cacheLength + transferStarted(dataSpec) + skipFully(dataSpec.position, dataSpec) return bytesToRead } @@ -120,13 +84,14 @@ class CachedDataSource( } override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - Timber.i("CachedDatasource: Read: %s %s %s", buffer, offset, length) + if (offset > 0 || length > 4) + Timber.d("CachedDatasource: Read: %s %s", offset, length) return if (cachePath != null) { try { readInternal(buffer, offset, length) } catch (e: IOException) { - throw HttpDataSource.HttpDataSourceException.createForIOException( - e, Util.castNonNull(dataSpec), HttpDataSource.HttpDataSourceException.TYPE_READ + throw HttpDataSourceException.createForIOException( + e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ ) } } else { @@ -148,14 +113,63 @@ class CachedDataSource( } val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLength) if (read == -1) { + Timber.i("CachedDatasource: EndOfInput") return C.RESULT_END_OF_INPUT } bytesRead += read.toLong() - // TODO - // bytesTransferred(read) + bytesTransferred(read) return read } + /** + * Attempts to skip the specified number of bytes in full. + * + * @param bytesToSkip The number of bytes to skip. + * @param dataSpec The [DataSpec]. + * @throws HttpDataSourceException If the thread is interrupted during the operation, or an error + * occurs while reading from the source, or if the data ended before skipping the specified + * number of bytes. + */ + @Suppress("ThrowsCount") + @Throws(HttpDataSourceException::class) + private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) { + var bytesToSkip = bytesToSkip + if (bytesToSkip == 0L) { + return + } + val skipBuffer = ByteArray(4096) + try { + while (bytesToSkip > 0) { + val readLength = + bytesToSkip.coerceAtMost(skipBuffer.size.toLong()).toInt() + val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength) + if (Thread.currentThread().isInterrupted) { + throw InterruptedIOException() + } + if (read == -1) { + throw HttpDataSourceException( + dataSpec, + PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, + HttpDataSourceException.TYPE_OPEN + ) + } + bytesToSkip -= read.toLong() + bytesTransferred(read) + } + return + } catch (e: IOException) { + if (e is HttpDataSourceException) { + throw e + } else { + throw HttpDataSourceException( + dataSpec, + PlaybackException.ERROR_CODE_IO_UNSPECIFIED, + HttpDataSourceException.TYPE_OPEN + ) + } + } + } + /* * This method is called by StatsDataSource to verify that the loading succeeded, * so its important that we return the correct value here.. @@ -165,9 +179,10 @@ class CachedDataSource( } override fun close() { - Timber.i("CachedDatasource: close") + Timber.i("CachedDatasource: close %s", openedFile) if (openedFile) { openedFile = false + transferEnded() responseByteStream?.close() responseByteStream = null } @@ -193,6 +208,10 @@ class CachedDataSource( cacheFile = Storage.getFromPath(filePath)!! responseByteStream = cacheFile!!.getFileInputStream() - return cacheFile!!.getDocumentFileDescriptor("r")!!.length + val descriptor = cacheFile!!.getDocumentFileDescriptor("r") + val length = descriptor!!.length + descriptor.close() + + return length } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 30e75bf9..967fff92 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -68,7 +68,8 @@ class Downloader( override fun run() { try { Timber.w("Checking Downloads") - checkDownloadsInternal() + // FIXME + // checkDownloadsInternal() } catch (all: Exception) { Timber.e(all, "checkDownloads() failed.") } finally { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index e3978c22..fa917baf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -298,11 +298,13 @@ class MediaPlayerController( @Synchronized fun seekTo(position: Int) { + Timber.i("SeekTo: %s", position) controller?.seekTo(position.toLong()) } @Synchronized fun seekTo(index: Int, position: Int) { + Timber.i("SeekTo: %s %s", index, position) controller?.seekTo(index, position.toLong()) } diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 2a8c3889..e4e015e2 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -62,7 +62,6 @@ Zobrazit album Náhodně Vizualizér - Stahuji - %s Přehrávám mix Playlist úspěšně uložen. Chyba ukládání playlistu, zkuste později. diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index 03e25e43..efdf3aa2 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -76,7 +76,6 @@ Album anzeigen Mischen Grafik - Herunterladen - %s Wiedergabeliste mischen Die Wiedergabeliste wurde gespeichert Konnte die Wiedergabeliste nicht speichern, bitte später erneut versuchen. diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index f2f44fc8..eced3d24 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -76,7 +76,6 @@ Mostrar Álbum Aleatorio Visualizador - Descargando - %s Reproduciendo en modo aleatorio Lista de reproducción guardada con éxito. Fallo al guardar la lista de reproducción, por favor reinténtalo mas tarde. diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index de7d1611..111c4cd0 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -76,7 +76,6 @@ Afficher l\'album Aléatoire Visualiseur - Téléchargement - %s En lecture aléatoire Playlist enregistrée avec succès ! Échec de l\'enregistrement de la playlist, veuillez réessayer plus tard. diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index f7a43478..04e907e7 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -71,7 +71,6 @@ Ugrás az albumhoz Véletlen sorrendű Visualizer - Letöltés - %s Véletlen sorrendű Lejátszási lista mentése sikeres. Lejátszási lista mentése sikertelen, próbálja később! diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index e4c1ca0c..00e94efe 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -59,7 +59,6 @@ Visualizza Album Casuale Visualizzatore - In scaricamento - %s Riproduzione casuale Playlist salvata con successo Impossibile salvare la playlist, riprovare più tardi. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 7b7cc123..93101248 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -76,7 +76,6 @@ Album tonen Willekeurig Visualisatie - Bezig met downloaden - %s Bezig met willekeurig afspelen Afspeellijst is opgeslagen. Afspeellijst kan niet worden opgeslagen. Probeer het later opnieuw. diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 459080d4..745b3d31 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -62,7 +62,6 @@ Wyświetl album Wymieszaj Efekt wizualny - Pobieranie - %s Odtwarzanie losowe Playlista została zapisana. Błąd zapisu playlisty. Proszę spróbować później. diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 6f9098f1..7a31aba0 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -74,7 +74,6 @@ Mostrar Álbum Misturar Visualizador - Baixando - %s Tocando misturado Playlist salva com sucesso. Falha ao salvar a playlist, Tente mais tarde. diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 43fe5bf3..f3e6b768 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -62,7 +62,6 @@ Mostrar Álbum Misturar Visualizador - Descarregando - %s Tocando misturado Playlist salva com sucesso. Falha ao salvar a playlist, Tente mais tarde. diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 78490e76..fb6a8e12 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -76,7 +76,6 @@ Показать альбом Случайное воспроизведение Визуализатор - Загрузка - %s Игра в случайном порядке Плейлист был успешно сохранен. Не удалось сохранить плейлист, попробуйте позже. diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 3cba252c..0431a0db 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -76,7 +76,6 @@ 显示专辑 随机 可视化 - 下载中 - %s 随机播放 已成功保存播放列表。 保存播放列表失败,请重试。 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 13260419..6b60f2ba 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -78,7 +78,7 @@ Shuffle mode enabled Shuffle mode disabled Visualizer - Downloading - %s + Buffering … Playing shuffle Playlist was successfully saved. Failed to save playlist, please try later. From a3a0c7f41df35325fce9c3c9309c4c390672f805 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 16 Apr 2022 11:23:05 +0200 Subject: [PATCH 31/67] Minor --- .../kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt | 1 + .../src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index 19789fd9..15d04580 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -83,6 +83,7 @@ class CachedDataSource( return upstreamDataSource.open(dataSpec) } + @Suppress("MagicNumber") override fun read(buffer: ByteArray, offset: Int, length: Int): Int { if (offset > 0 || length > 4) Timber.d("CachedDatasource: Read: %s %s", offset, length) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 967fff92..30e75bf9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -68,8 +68,7 @@ class Downloader( override fun run() { try { Timber.w("Checking Downloads") - // FIXME - // checkDownloadsInternal() + checkDownloadsInternal() } catch (all: Exception) { Timber.e(all, "checkDownloads() failed.") } finally { From 762aeec5d3ef1433eff49c723b1184e2cf5edd73 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 16 Apr 2022 21:20:10 +0200 Subject: [PATCH 32/67] Cleanup --- .../media3_notification_small_icon.xml | 9 ------- ultrasonic/src/main/res/values-cs/strings.xml | 2 -- ultrasonic/src/main/res/values-de/strings.xml | 24 ------------------- ultrasonic/src/main/res/values-es/strings.xml | 5 ---- ultrasonic/src/main/res/values-fr/strings.xml | 12 ---------- ultrasonic/src/main/res/values-hu/strings.xml | 5 ---- ultrasonic/src/main/res/values-it/strings.xml | 2 -- ultrasonic/src/main/res/values-nl/strings.xml | 5 ---- ultrasonic/src/main/res/values-pl/strings.xml | 2 -- .../src/main/res/values-pt-rBR/strings.xml | 5 ---- ultrasonic/src/main/res/values-pt/strings.xml | 2 -- ultrasonic/src/main/res/values-ru/strings.xml | 5 ---- .../src/main/res/values-zh-rCN/strings.xml | 10 -------- ultrasonic/src/main/res/values/strings.xml | 3 --- 14 files changed, 91 deletions(-) delete mode 100644 ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml diff --git a/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml b/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml deleted file mode 100644 index e2943794..00000000 --- a/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index e4e015e2..bcf81ec5 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -179,8 +179,6 @@ Řadit skladby podle čísla CD Řadit seznam skladeb dle čísla CD a čísla skladby Připojovat jméno umělce, bitrate a příponu souboru - Přehrávání bez pauz - Zapnout přehrávání bez pauz Skrýt hudební soubory před ostatními aplikacemi. Skrýt před ostatními Nabyde účinnosti při příštím skenování hudby systému Android. diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index efdf3aa2..762ecb7f 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -41,11 +41,8 @@ Name OK Anheften - Pause - Abspielen Zuletzt spielen Als nächstes spielen - Vorheriges abspielen Jetzt spielen Zufällig spielen Öffentlich @@ -135,15 +132,10 @@ Löschen der Wiedergabeliste %s ist fehlgeschlagen Downloads Beenden - Navigation Einstellungen Aktualisierung Medienbibliothek Offline Medien - Netzwerkfehler. Neuer Versuch %1$d von %2$d. - %d Künstler*innen gefunden - Lese vom Server. - Lese vom Server. Fertig! Wiedergabelisten Aktualisierungs-Informationen Wiedergabeliste für %s aktualisiert @@ -223,14 +215,11 @@ Bitrate und Dateityp hinter der Künstler*in anzeigen Zeige Aktuelle Wiedergabe bei Play Zeige Aktuelle Wiedergabe nach dem Start der Wiedergabe in der Medienansicht - Lückenlose Wiedergabe - Lückenlose Wiedergabe aktivieren Musikdateien vor anderen Apps verbergen Vor anderen verbergen Wird beim nächsten Durchsuchen nach Musik durch Android wirksam. Sprunglänge Bitte eine gültige URL angeben. - Bitte einen gültigen Benutzernamen eingeben (ohne führende Leerzeichen). Max. Anzahl der Alben Max. Anzahl der Künstler*innen 112 Kbps @@ -271,8 +260,6 @@ 3 Titel 5 Titel Unbegrenzt - Fortsetzen mit Kopfhörer - Die App setzt eine pausierte Wiedergabe beim Anschließen der Kopfhörer fort. Benutzername und Passwort des Scrobble Service(s) müssen im Server gesetzt sein Gespielte Musik scrobbeln 1 @@ -385,13 +372,6 @@ Künstler*in anzeigen Album Cover Mehrere Jahre - Wiedergabe fortsetzen, wenn ein Bluetooth Gerät verbunden wurde - Wiedergabe pausieren, wenn ein Bluetooth Gerät getrennt wurde - Alle Bluetooth Geräte - Nur Audio (A2DP) Geräte - Deaktiviert - Bluetooth Gerät mit einer Play/Pause Taste - Dies kann bei älteren Bluetooth Geräten helfen, wenn Play/Pause nicht richtig funktioniert Debug Optionen Schreibe Debug Log in Datei Die Log Dateien sind unter %1$s/%2$s verfügbar @@ -444,10 +424,6 @@ %d Titel nach aktuellen Titel hinzugefügt %d Titel nach aktuellen Titel hinzugefügt - - %d Tag Testphase übrig - %d Tage Testphase übrig - Allgemeiner API Fehler: %1$s diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index eced3d24..6ef40581 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -41,11 +41,8 @@ Nombre OK Anclar - Pausar - Reproducir Reproducir última Reproducir a continuación - Reproducir anterior Reproducir ahora Reproducción aleatoria Public @@ -218,8 +215,6 @@ Añadir el nombre del artista con la tasa de bits y la extensión del archivo Mostrar reproduciendo ahora al reproducir Cambiar a reproduciendo ahora después de iniciar la reproducción en la vista multimedia - Reproducción sin pausas - Activa la reproducción sin pausas Oculta los archivos de música desde otras aplicaciones. Ocultar desde otras Tiene efecto la próxima vez que Android escanee la música de tu dispositivo. diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 111c4cd0..6be37c10 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -41,11 +41,8 @@ Nom OK Épingler - Pause - Lecture Jouer en dernier Jouer à la suite - Lire le précédent Jouer maintenant Jouer aléatoirement Public @@ -107,7 +104,6 @@ Musique Hors-ligne %s - Configurer le serveur - Lecture aléatoire Aléatoire Favoris Titres @@ -201,8 +197,6 @@ Trier la liste des titres par numéro de disques/pistes Afficher le débit et l’extension de fichier Ajouter le nom d\'artiste, débit et suffixe du fichier - Lecture sans interruption - Activer la lecture sans interruption Masquer les fichiers musicaux et les couvertures d\'album aux autres applis (Galerie, Musique, etc.) Masquer aux autres Prendra effet la prochaine fois qu\'Android recensera les médias disponibles sur l\'appareil. @@ -355,12 +349,6 @@ Afficher l\'artiste Pochette d\'album Années multiples - Reprendre lorsqu’un appareil Bluetooth se connecte - Mettre en pause lorsqu’un appareil Bluetooth se déconnecte - Tous les appareils Bluetooth - Seulement les appareils audio (A2DP) - Désactivé - Activer cela peut aider sur les anciens appareils Bluetooth lorsque Lecture/Pause ne fonctionne pas correctement Paramètres de debug Enregistrer les logs de debug dans des fichiers Les fichiers de log sont disponibles dans %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 04e907e7..5327976b 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -39,11 +39,8 @@ Név OK Tárolás (Megőrzés az eszközön) - Szünet - Lejátszás Lejátszás (Utolsóként) Lejátszás (Következőként) - Előző lejátszása Lejátszás Véletlen sorrendű lejátszás Nyilvános @@ -188,8 +185,6 @@ Dalok rendezése albumok szerint Dalok rendezése albumsorszám és dalsorszám szerint. Bitráta és fájlkiterjesztés megjelenítése az előadónév mellett. - Egybefüggő lejátszás - Kihagyás (dalszünet) nélküli egybefüggő lejátszás (Gapless). Zenefájlok elrejtése egyéb alkalmazások elől. Elrejtés A következő alkalomtól lép életbe, amikor az Android zenefájlokat keres a telefonon. diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 00e94efe..802f288f 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -175,8 +175,6 @@ Ordina Canzoni secondo Disco Ordina lista canzoni secondo il numero disco e traccia Aggiungi nome artista con bitrare ed estensione file - Riproduzione Ininterrotta - Abilita riproduzione ininterrotta Nascondi file musicali di altre app Nascondi Da Altro Effettivo alla prossima scansione Android per file musicali sul telefono. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 93101248..84e4cd5e 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -41,11 +41,8 @@ Naam Oké Vastmaken - Pauzeren - Afspelen Laatste afspelen Volgende afspelen - Vorige afspelen Nu afspelen Willekeurig afspelen Openbaar @@ -218,8 +215,6 @@ Bitsnelheid en bestandsextensie toevoegen aan artiestennaam Nu aan het afspelen tonen op afspeelscherm Toon ‘Nu aan het afspelen’ in de mediaweergave - Naadloze overgang - Naadloze overgang tussen nummers inschakelen Muziekbestanden verbergen voor andere apps. Verbergen voor andere apps Dit wordt toegepast bij de volgende keer dat Android je muziek doorzoekt. diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 745b3d31..1ad64489 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -179,8 +179,6 @@ Sortuj utwory wg dysku Sortuje listę utworów wg numeru dysku i numeru utworu Dołącza bitrate i typ pliku do nazwy artysty - Odtwarzanie bez przerw - Włącz odtwarzanie bez przerw między utworami Ukrywa pliki muzyczne przed innymi aplikacjami. Ukryj pliki Efekt widoczny będzie po następnym skanowaniu muzyki przez system Android diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 7a31aba0..f5aede4e 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -41,11 +41,8 @@ Nome OK Fixar - Pausar - Tocar Tocar por Último Tocar na Próxima - Tocar a Anterior Tocar Agora Tocar Aleatoriamente Público @@ -195,8 +192,6 @@ Classificar músicas pelo número do álbum e faixas Mostrar Bitrate se Sufixo do Arquivo Adicionar o nome do artista com a taxa de bits e sufixo do arquivo - Reprodução sem Interrupção - Ativar reprodução sem interrupção Esconder arquivos de músicas de outros aplicativos Esconder de Outros Será efetivado na próxima vez que o Android procurar por músicas em seu celular. diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index f3e6b768..404a807d 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -179,8 +179,6 @@ Classificar Músicas por Álbum Classificar músicas pelo número do álbum e faixas. Adiciona o nome do artista com a taxa de bits e sufixo do ficheiro - Reprodução sem Interrupção - Habilita reprodução sem interrupção Esconder músicas de outros aplicativos. Esconder de Outros Será realizado na próxima vez que o Android procurar por músicas em seu telemóvel. diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index fb6a8e12..99e79a04 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -41,11 +41,8 @@ Имя Ок Пин - Пауза - Воспроизведение Воспроизвести последний Воспроизвести следующий - Воспроизвести предыдущий Воспроизвести сейчас Играть в случайном порядке Публичный @@ -206,8 +203,6 @@ Время кэша каталогов Сортировать список песен по номеру диска и треку Добавить имя исполнителя с битрейтом и суффиксом файла - Воспроизведение без промежутка - Включить воспроизведение без паузы Включить воспроизведение без паузы Скрыть от других Вступает в силу в следующий раз, Android сканирует ваш телефон на предмет музыки diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 0431a0db..a6e84a59 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -41,11 +41,8 @@ 名称 确定 固定 - 暂停 - 播放 最后一首 下一首 - 上一首 现在播放 随机播放 公开 @@ -107,7 +104,6 @@ 音乐 离线 %s - 已设置服务器 - 随机播放 随机 收藏夹 歌曲 @@ -353,12 +349,6 @@ 显示艺术家 albumArt Multiple Years - 连接蓝牙设备时恢复播放 - 断开蓝牙设备时暂停播放 - 所有蓝牙设备 - 仅音频 (A2DP) 设备 - 已禁用 - 当播放/暂停无法正常工作时,启用此功能可能对较旧的蓝牙设备有所帮助 调试选项 将调试日志写入文件 日志文件可在 %1$s/%2$s 获取 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 6b60f2ba..1cddd882 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -41,11 +41,8 @@ Name OK Pin - Pause - Play Play Last Play Next - Play Previous Play Now Play Shuffled Public From 788538ee6a70147be2b6a673b0922dc560a39de5 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 17 Apr 2022 06:55:24 +0200 Subject: [PATCH 33/67] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit c7d9af4529bd9bfba49c46208819d7ef53542cd4 Author: Holger Müller Date: Sat Apr 16 17:56:41 2022 +0200 fixed CI failure commit 59e18bc5c0264a4a41ead2d819142458c8053ee8 Author: Holger Müller Date: Sat Apr 16 17:42:49 2022 +0200 removed unused dataSourceFactory, commented debug output --- .../kotlin/org/moire/ultrasonic/playback/APIDataSource.kt | 2 +- .../org/moire/ultrasonic/playback/CachedDataSource.kt | 4 ++-- .../kotlin/org/moire/ultrasonic/playback/PlaybackService.kt | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt index 0a071dbe..469e1f30 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt @@ -220,7 +220,7 @@ open class APIDataSource private constructor( @Throws(HttpDataSourceException::class) override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - Timber.d("APIDatasource: Read: %s %s", offset, length) + // Timber.d("APIDatasource: Read: %s %s", offset, length) return try { readInternal(buffer, offset, length) } catch (e: IOException) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index 15d04580..64381403 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -85,8 +85,8 @@ class CachedDataSource( @Suppress("MagicNumber") override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - if (offset > 0 || length > 4) - Timber.d("CachedDatasource: Read: %s %s", offset, length) + // if (offset > 0 || length > 4) + // Timber.d("CachedDatasource: Read: %s %s", offset, length) return if (cachePath != null) { try { readInternal(buffer, offset, length) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 2a08df67..1d446797 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -44,7 +44,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var player: ExoPlayer private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var apiDataSource: APIDataSource.Factory - private lateinit var dataSourceFactory: DataSource.Factory private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback @@ -102,14 +101,13 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // Create a MediaSource which passes calls through our OkHttp Stack apiDataSource = APIDataSource.Factory(subsonicAPIClient) - - dataSourceFactory = APIDataSource.Factory(subsonicAPIClient) val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(apiDataSource) // Create a renderer with HW rendering support val renderer = DefaultRenderersFactory(this) - if (Settings.useHwOffload) renderer.setEnableAudioOffload(true) + if (Settings.useHwOffload) + renderer.setEnableAudioOffload(true) // Create the player player = ExoPlayer.Builder(this) From 3691428a6884bbfbc300e7b2df5b4cb33918d6e1 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 17 Apr 2022 06:58:39 +0200 Subject: [PATCH 34/67] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 7a8f20ade7d2522c1281b63a31c3e9630df12553 Author: Holger Müller Date: Fri Apr 8 23:21:15 2022 +0200 make build working with target sdk 31 commit 5890c32b7eb91621ce628ac6d900cb00a1481d71 Author: Holger Müller Date: Fri Apr 8 22:57:23 2022 +0200 make build working with target sdk 31 commit 1c7c4839b3eed5eef8298c2ddce22812e14d6329 Merge: 48b0cdea 92ef78a3 Author: Holger Müller Date: Fri Apr 8 21:55:14 2022 +0200 Merge remote-tracking branch 'upstream/media3-flat' into media3-flat commit 48b0cdea83433cc29760972c522a497f812d6d30 Merge: 9101980c 3ca25ed1 Author: Holger Müller Date: Fri Apr 8 19:58:55 2022 +0200 Merge remote-tracking branch 'upstream/media3-flat' into media3-flat commit 9101980cb6d8972ee6e49b58737b36c4ac36e495 Merge: 515690ab 97798446 Author: Holger Müller Date: Fri Apr 8 17:07:54 2022 +0200 Merge remote-tracking branch 'upstream/media3-flat' into media3-flat commit 515690abaccd5405709b1fe877a9ef9c9bbf3e21 Author: Holger Müller Date: Thu Apr 7 08:55:42 2022 +0200 made button off mode better visible --- .../ultrasonic/playback/PlaybackService.kt | 9 +++++--- .../provider/UltrasonicAppWidgetProvider.kt | 21 +++++++++++++------ .../ultrasonic/service/DownloadService.kt | 8 ++++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 1d446797..edd24315 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -15,9 +15,9 @@ */ package org.moire.ultrasonic.playback -import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent +import android.os.Build import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.C.CONTENT_TYPE_MUSIC @@ -132,11 +132,14 @@ class PlaybackService : MediaLibraryService(), KoinComponent { .build() } - @SuppressLint("UnspecifiedImmutableFlag") private fun getPendingIntentForContent(): PendingIntent { val intent = Intent(this, NavigationActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - val flags = PendingIntent.FLAG_UPDATE_CURRENT + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // needed starting Android 12 (S = 31) + flags = flags or PendingIntent.FLAG_IMMUTABLE + } intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) return PendingIntent.getActivity(this, 0, intent, flags) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt index 7a12fdfe..ddc11e3f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt @@ -7,13 +7,13 @@ package org.moire.ultrasonic.provider -import android.annotation.SuppressLint import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.Build import android.os.Environment import android.view.KeyEvent import android.widget.RemoteViews @@ -164,7 +164,6 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { /** * Link up various button actions using [PendingIntent]. */ - @SuppressLint("UnspecifiedImmutableFlag") private fun linkButtons(context: Context, views: RemoteViews, playerActive: Boolean) { var intent = Intent( context, @@ -173,8 +172,13 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { if (playerActive) intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) intent.action = "android.intent.action.MAIN" intent.addCategory("android.intent.category.LAUNCHER") + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // needed starting Android 12 (S = 31) + flags = flags or PendingIntent.FLAG_IMMUTABLE + } var pendingIntent = - PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT) + PendingIntent.getActivity(context, 10, intent, flags) views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent) views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent) @@ -185,7 +189,12 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) ) - pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0) + flags = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // needed starting Android 12 (S = 31) + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags) views.setOnClickPendingIntent(R.id.control_play, pendingIntent) intent = Intent(Constants.CMD_PROCESS_KEYCODE) intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) @@ -193,7 +202,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT) ) - pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0) + pendingIntent = PendingIntent.getBroadcast(context, 12, intent, flags) views.setOnClickPendingIntent(R.id.control_next, pendingIntent) intent = Intent(Constants.CMD_PROCESS_KEYCODE) intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) @@ -201,7 +210,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS) ) - pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0) + pendingIntent = PendingIntent.getBroadcast(context, 13, intent, flags) views.setOnClickPendingIntent(R.id.control_previous, pendingIntent) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index e9cc351d..f6b2ed49 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -7,7 +7,6 @@ package org.moire.ultrasonic.service -import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -154,11 +153,14 @@ class DownloadService : Service() { return notificationBuilder.build() } - @SuppressLint("UnspecifiedImmutableFlag") private fun getPendingIntentForContent(): PendingIntent { val intent = Intent(this, NavigationActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - val flags = PendingIntent.FLAG_UPDATE_CURRENT + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // needed starting Android 12 (S = 31) + flags = flags or PendingIntent.FLAG_IMMUTABLE + } intent.putExtra(Constants.INTENT_SHOW_PLAYER, true) return PendingIntent.getActivity(this, 0, intent, flags) } From 69c78f4c370c6a8f34c63def426670c4896fb400 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 20 Apr 2022 13:15:22 +0200 Subject: [PATCH 35/67] Enable StrictMode logging, rm unused prefs --- .../kotlin/org/moire/ultrasonic/app/UApp.kt | 3 ++ .../ultrasonic/fragment/SettingsFragment.kt | 27 -------------- .../org/moire/ultrasonic/util/Constants.kt | 7 ---- .../org/moire/ultrasonic/util/Settings.kt | 6 ---- ultrasonic/src/main/res/xml/settings.xml | 36 ------------------- 5 files changed, 3 insertions(+), 76 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index fb83096f..0f869aee 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -1,6 +1,7 @@ package org.moire.ultrasonic.app import android.content.Context +import android.os.StrictMode import androidx.multidex.MultiDexApplication import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -25,6 +26,8 @@ class UApp : MultiDexApplication() { init { instance = this + if (BuildConfig.DEBUG) + StrictMode.enableDefaults() } override fun onCreate() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index d7043da0..784059dc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -17,7 +17,6 @@ import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.Preference -import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import java.io.File import kotlin.math.ceil @@ -112,10 +111,6 @@ class SettingsFragment : chatRefreshInterval = findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH_INTERVAL) directoryCacheTime = findPreference(Constants.PREFERENCES_KEY_DIRECTORY_CACHE_TIME) mediaButtonsEnabled = findPreference(Constants.PREFERENCES_KEY_MEDIA_BUTTONS) - lockScreenEnabled = findPreference(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS) - sendBluetoothAlbumArt = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART) - sendBluetoothNotifications = - findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS) sharingDefaultDescription = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION) sharingDefaultGreeting = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING) @@ -128,21 +123,6 @@ class SettingsFragment : sharingDefaultGreeting!!.text = shareGreeting setupClearSearchPreference() setupCacheLocationPreference() - - // After API26 foreground services must be used for music playback, and they must have a notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationsCategory = - findPreference(Constants.PREFERENCES_KEY_CATEGORY_NOTIFICATIONS) - var preferenceToRemove = - findPreference(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION) - if (preferenceToRemove != null) notificationsCategory!!.removePreference( - preferenceToRemove - ) - preferenceToRemove = findPreference(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION) - if (preferenceToRemove != null) notificationsCategory!!.removePreference( - preferenceToRemove - ) - } } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -208,9 +188,6 @@ class SettingsFragment : Constants.PREFERENCES_KEY_HIDE_MEDIA -> { setHideMedia(sharedPreferences.getBoolean(key, false)) } - Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS -> { - setBluetoothPreferences(sharedPreferences.getBoolean(key, true)) - } Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE -> { setDebugLogToFile(sharedPreferences.getBoolean(key, false)) } @@ -351,10 +328,6 @@ class SettingsFragment : toast(activity, R.string.settings_hide_media_toast, false) } - private fun setBluetoothPreferences(enabled: Boolean) { - sendBluetoothAlbumArt!!.isEnabled = enabled - } - private fun setCacheLocation(path: String) { if (path != "") { val uri = Uri.parse(path) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index 6fb9efeb..687282b3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -74,9 +74,6 @@ object Constants { const val PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload" const val PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength" const val PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout" - const val PREFERENCES_KEY_SHOW_NOTIFICATION = "showNotification" - const val PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION = "alwaysShowNotification" - const val PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS = "showLockScreen" const val PREFERENCES_KEY_MAX_ALBUMS = "maxAlbums" const val PREFERENCES_KEY_MAX_SONGS = "maxSongs" const val PREFERENCES_KEY_MAX_ARTISTS = "maxArtists" @@ -84,20 +81,16 @@ object Constants { const val PREFERENCES_KEY_DEFAULT_SONGS = "defaultSongs" const val PREFERENCES_KEY_DEFAULT_ARTISTS = "defaultArtists" const val PREFERENCES_KEY_SHOW_NOW_PLAYING = "showNowPlaying" - const val PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS = "playbackControlSettings" const val PREFERENCES_KEY_CLEAR_SEARCH_HISTORY = "clearSearchHistory" const val PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay" const val PREFERENCES_KEY_INCREMENT_TIME = "incrementTime" const val PREFERENCES_KEY_SHOW_NOW_PLAYING_DETAILS = "showNowPlayingDetails" const val PREFERENCES_KEY_ID3_TAGS = "useId3Tags" const val PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture" - const val PREFERENCES_KEY_TEMP_LOSS = "tempLoss" const val PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval" const val PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime" const val PREFERENCES_KEY_CLEAR_BOOKMARK = "clearBookmark" const val PREFERENCES_KEY_DISC_SORT = "discAndTrackSort" - const val PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS = "sendBluetoothNotifications" - const val PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART = "sendBluetoothAlbumArt" const val PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS = "sharingAlwaysAskForDetails" const val PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION = "sharingDefaultDescription" const val PREFERENCES_KEY_DEFAULT_SHARE_GREETING = "sharingDefaultGreeting" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 58458f9e..c62e1ce5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -183,12 +183,6 @@ object Settings { var shouldClearBookmark by BooleanSetting(Constants.PREFERENCES_KEY_CLEAR_BOOKMARK, false) - // Inverted for readability - var shouldSendBluetoothNotifications by BooleanSetting( - Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS, - true - ) - var shouldAskForShareDetails by BooleanSetting(Constants.PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS, true) diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index c03bdc62..7eb7dc81 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -113,42 +113,6 @@ a:summary="@string/settings.show_now_playing_summary" a:title="@string/settings.show_now_playing" app:iconSpaceReserved="false"/> - - - - - - Date: Wed, 20 Apr 2022 17:06:30 +0200 Subject: [PATCH 36/67] Cleanup MediaPlayerLifecycleSupport.kt --- .../moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 586762ce..0246fc4c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -27,8 +27,6 @@ class MediaPlayerLifecycleSupport : KoinComponent { private val mediaPlayerController by inject() private var created = false - private var headsetEventReceiver: BroadcastReceiver? = null - private var mediaButtonEventSubscription: Disposable? = null fun onCreate() { onCreate(false, null) @@ -73,8 +71,6 @@ class MediaPlayerLifecycleSupport : KoinComponent { ) mediaPlayerController.clear(false) - mediaButtonEventSubscription?.dispose() - applicationContext().unregisterReceiver(headsetEventReceiver) mediaPlayerController.onDestroy() created = false From 647435fe55ceb4dd9f1634459d52b19ed00ca1e4 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 20 Apr 2022 20:57:51 +0200 Subject: [PATCH 37/67] launch restore on Main Thread --- .../moire/ultrasonic/fragment/NowPlayingFragment.kt | 1 + .../ultrasonic/service/MediaPlayerLifecycleSupport.kt | 3 --- .../ultrasonic/service/PlaybackStateSerializer.kt | 11 +++++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 9a1f2489..0af42e17 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -136,6 +136,7 @@ class NowPlayingFragment : Fragment() { .navigate(R.id.trackCollectionFragment, bundle) } } + requireView().setOnTouchListener { _: View?, event: MotionEvent -> handleOnTouch(event) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 0246fc4c..70e9df92 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -7,13 +7,10 @@ package org.moire.ultrasonic.service -import android.content.BroadcastReceiver import android.content.Intent import android.view.KeyEvent -import io.reactivex.rxjava3.disposables.Disposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Util.ifNotNull diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index 7115140a..a8d62a7c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -33,7 +33,8 @@ class PlaybackStateSerializer : KoinComponent { private val lock: Lock = ReentrantLock() private val setup = AtomicBoolean(false) - private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) fun serialize( songs: Iterable, @@ -42,7 +43,7 @@ class PlaybackStateSerializer : KoinComponent { ) { if (!setup.get()) return - appScope.launch { + ioScope.launch { if (lock.tryLock()) { try { serializeNow(songs, currentPlayingIndex, currentPlayingPosition) @@ -78,7 +79,7 @@ class PlaybackStateSerializer : KoinComponent { fun deserialize(afterDeserialized: (State?) -> Unit?) { - appScope.launch { + ioScope.launch { try { lock.lock() deserializeNow(afterDeserialized) @@ -103,6 +104,8 @@ class PlaybackStateSerializer : KoinComponent { state.currentPlayingPosition ) - afterDeserialized(state) + mainScope.launch { + afterDeserialized(state) + } } } From 6115ac995ff6d0120bd07d19e1e3d0684b984803 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 18 Apr 2022 09:26:13 +0200 Subject: [PATCH 38/67] Remove legacyPlayerState --- .../moire/ultrasonic/view/VisualizerView.java | 7 +- .../ultrasonic/activity/NavigationActivity.kt | 3 +- .../ultrasonic/adapters/TrackViewHolder.kt | 1 + .../ultrasonic/fragment/NowPlayingFragment.kt | 17 ++-- .../service/MediaPlayerController.kt | 77 ++++++------------- .../org/moire/ultrasonic/service/RxBus.kt | 8 +- 6 files changed, 41 insertions(+), 72 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java index 3a567399..331b8903 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java @@ -18,6 +18,8 @@ */ package org.moire.ultrasonic.view; +import static org.koin.java.KoinJavaComponent.inject; + import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -29,14 +31,11 @@ import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import org.moire.ultrasonic.audiofx.VisualizerController; -import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.service.MediaPlayerController; import kotlin.Lazy; import timber.log.Timber; -import static org.koin.java.KoinJavaComponent.inject; - /** * A simple class that draws waveform data received from a * {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture} @@ -130,7 +129,7 @@ public class VisualizerView extends View return; } - if (mediaPlayerControllerLazy.getValue().getLegacyPlayerState() != PlayerState.STARTED) + if (!mediaPlayerControllerLazy.getValue().isPlaying()) { return; } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 5eacc74a..031f56a6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -46,7 +46,6 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSettingDao -import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.provider.SearchSuggestionProvider @@ -183,7 +182,7 @@ class NavigationActivity : AppCompatActivity() { } rxBusSubscription += RxBus.playerStateObservable.subscribe { - if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED) + if (it.state == STATE_READY) showNowPlaying() else hideNowPlaying() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 2b9c2ec5..47843388 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -109,6 +109,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } rxSubscription = RxBus.playerStateObservable.subscribe { + Timber.i("NEW PLAY STATE") setPlayIcon(it.index == bindingAdapterPosition && it.track == downloadFile) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 0af42e17..b34d30ee 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -22,7 +22,6 @@ import java.lang.Exception import kotlin.math.abs import org.koin.android.ext.android.inject import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider @@ -85,20 +84,18 @@ class NowPlayingFragment : Fragment() { @SuppressLint("ClickableViewAccessibility") private fun update() { try { - val playerState = mediaPlayerController.legacyPlayerState - - if (playerState === PlayerState.PAUSED) { - playButton!!.setImageDrawable( - getDrawableFromAttribute( - requireContext(), R.attr.media_play - ) - ) - } else if (playerState === PlayerState.STARTED) { + if (mediaPlayerController.isPlaying) { playButton!!.setImageDrawable( getDrawableFromAttribute( requireContext(), R.attr.media_pause ) ) + } else { + playButton!!.setImageDrawable( + getDrawableFromAttribute( + requireContext(), R.attr.media_play + ) + ) } val file = mediaPlayerController.currentPlayingLegacy diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index fa917baf..037955a4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -13,7 +13,6 @@ import androidx.core.net.toUri import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player -import androidx.media3.common.Player.STATE_BUFFERING import androidx.media3.common.Timeline import androidx.media3.session.MediaController import androidx.media3.session.SessionToken @@ -91,13 +90,11 @@ class MediaPlayerController( } override fun onPlaybackStateChanged(playbackState: Int) { - translatePlaybackState(playbackState = playbackState) playerStateChangedHandler() publishPlaybackState() } override fun onIsPlayingChanged(isPlaying: Boolean) { - translatePlaybackState(isPlaying = isPlaying) playerStateChangedHandler() publishPlaybackState() } @@ -121,57 +118,29 @@ class MediaPlayerController( Timber.i("MediaPlayerController created") } - @Suppress("DEPRECATION") - fun translatePlaybackState( - playbackState: Int = controller?.playbackState ?: 0, - isPlaying: Boolean = controller?.isPlaying ?: false - ) { - legacyPlayerState = when (playbackState) { - STATE_BUFFERING -> PlayerState.DOWNLOADING - Player.STATE_ENDED -> { - PlayerState.COMPLETED - } - Player.STATE_IDLE -> { - PlayerState.IDLE - } - Player.STATE_READY -> { - if (isPlaying) { - PlayerState.STARTED - } else { - PlayerState.PAUSED - } - } - else -> { - PlayerState.IDLE - } - } - } - private fun playerStateChangedHandler() { - val playerState = legacyPlayerState val currentPlaying = legacyPlaylistManager.currentPlaying - when { - playerState === PlayerState.PAUSED -> { - playbackStateSerializer.serialize( - playList, currentMediaItemIndex, playerPosition - ) + when (playbackState) { + Player.STATE_READY -> { + if (isPlaying) { + scrobbler.scrobble(currentPlaying, false) + } else { + playbackStateSerializer.serialize( + playList, currentMediaItemIndex, playerPosition + ) + } } - playerState === PlayerState.STARTED -> { - scrobbler.scrobble(currentPlaying, false) - } - playerState === PlayerState.COMPLETED -> { + Player.STATE_ENDED -> { scrobbler.scrobble(currentPlaying, true) } } // Update widget if (currentPlaying != null) { - updateWidget(playerState, currentPlaying.track) + updateWidget(currentPlaying.track) } - - Timber.d("Processed player state change") } private fun onTrackCompleted() { @@ -190,23 +159,23 @@ class MediaPlayerController( } private fun publishPlaybackState() { - RxBus.playerStatePublisher.onNext( - RxBus.StateWithTrack( - state = legacyPlayerState, - track = legacyPlaylistManager.currentPlaying, - index = currentMediaItemIndex - ) + val newState = RxBus.StateWithTrack( + track = legacyPlaylistManager.currentPlaying, + index = currentMediaItemIndex, + isPlaying = isPlaying, + state = playbackState ) + RxBus.playerStatePublisher.onNext(newState) + Timber.i("New PlaybackState: %s", newState) } - private fun updateWidget(playerState: PlayerState, song: Track?) { - val started = playerState === PlayerState.STARTED + private fun updateWidget(song: Track?) { val context = UApp.applicationContext() - UltrasonicAppWidgetProvider4X1.instance?.notifyChange(context, song, started, false) - UltrasonicAppWidgetProvider4X2.instance?.notifyChange(context, song, started, true) - UltrasonicAppWidgetProvider4X3.instance?.notifyChange(context, song, started, false) - UltrasonicAppWidgetProvider4X4.instance?.notifyChange(context, song, started, false) + UltrasonicAppWidgetProvider4X1.instance?.notifyChange(context, song, isPlaying, false) + UltrasonicAppWidgetProvider4X2.instance?.notifyChange(context, song, isPlaying, true) + UltrasonicAppWidgetProvider4X3.instance?.notifyChange(context, song, isPlaying, false) + UltrasonicAppWidgetProvider4X4.instance?.notifyChange(context, song, isPlaying, false) } fun onDestroy() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index eb49df0d..8107569d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -5,7 +5,6 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.subjects.PublishSubject -import org.moire.ultrasonic.domain.PlayerState class RxBus { companion object { @@ -46,7 +45,12 @@ class RxBus { dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) } - data class StateWithTrack(val state: PlayerState, val track: DownloadFile?, val index: Int = -1) + data class StateWithTrack( + val track: DownloadFile?, + val index: Int = -1, + val isPlaying: Boolean = false, + val state: Int + ) } operator fun CompositeDisposable.plusAssign(disposable: Disposable) { From 5d4aff1f21892550dbd0be4bd8913d2862308098 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 18 Apr 2022 09:56:36 +0200 Subject: [PATCH 39/67] Cleanup restore --- .../service/MediaPlayerController.kt | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 037955a4..32e9d29c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -22,7 +22,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider -import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.playback.LegacyPlaylistManager import org.moire.ultrasonic.playback.PlaybackService @@ -82,6 +81,26 @@ class MediaPlayerController( controller = mediaControllerFuture.get() controller?.addListener(object : Player.Listener { + + /* + * Log all events + */ +// override fun onEvents(player: Player, events: Player.Events) { +// //Timber.i("Media3 Event: %s", events) +// } + + // override fun onIsLoadingChanged(isLoading: Boolean) { +// super.onIsLoadingChanged(isLoading) +// } +// +// override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { +// super.onPlayWhenReadyChanged(playWhenReady, reason) +// } +// +// override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) { +// super.onPlaylistMetadataChanged(mediaMetadata) +// } +// /* * This will be called everytime the playlist has changed. */ @@ -191,19 +210,21 @@ class MediaPlayerController( @Synchronized fun restore( - songs: List?, + songs: List, currentPlayingIndex: Int, currentPlayingPosition: Int, autoPlay: Boolean, newPlaylist: Boolean ) { + val insertionMode = if (newPlaylist) InsertionMode.CLEAR + else InsertionMode.APPEND + addToPlaylist( songs, cachePermanently = false, autoPlay = false, - playNext = false, shuffle = false, - newPlaylist = newPlaylist + insertionMode = insertionMode ) if (currentPlayingIndex != -1) { @@ -295,32 +316,6 @@ class MediaPlayerController( } } - @Synchronized - @Deprecated("Use InsertionMode Syntax") - @Suppress("LongParameterList") - fun addToPlaylist( - songs: List?, - cachePermanently: Boolean, - autoPlay: Boolean, - playNext: Boolean, - shuffle: Boolean, - newPlaylist: Boolean - ) { - if (songs == null) return - - val insertionMode = when { - newPlaylist -> InsertionMode.CLEAR - playNext -> InsertionMode.AFTER_CURRENT - else -> InsertionMode.APPEND - } - - val filteredSongs = songs.filterNotNull() - - addToPlaylist( - filteredSongs, cachePermanently, autoPlay, shuffle, insertionMode - ) - } - @Synchronized fun addToPlaylist( songs: List, @@ -514,10 +509,6 @@ class MediaPlayerController( return controller?.duration?.toInt() ?: return 0 } - @Deprecated("Use Controller.playbackState and Controller.isPlaying") - @set:Synchronized - var legacyPlayerState: PlayerState = PlayerState.IDLE - val playbackState: Int get() = controller?.playbackState ?: 0 From 2aaa3c2119f4f761a29e9e80f88f2c8dfcc5d4ec Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 20 Apr 2022 21:40:22 +0200 Subject: [PATCH 40/67] Fix de(serilization) --- .../ultrasonic/adapters/TrackViewHolder.kt | 1 - .../kotlin/org/moire/ultrasonic/app/UApp.kt | 13 +++- .../service/MediaPlayerController.kt | 76 +++++++------------ .../service/MediaPlayerLifecycleSupport.kt | 25 +++--- 4 files changed, 54 insertions(+), 61 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 47843388..2b9c2ec5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -109,7 +109,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable } rxSubscription = RxBus.playerStateObservable.subscribe { - Timber.i("NEW PLAY STATE") setPlayIcon(it.index == bindingAdapterPosition && it.track == downloadFile) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index 0f869aee..d5d31b6b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -3,6 +3,9 @@ package org.moire.ultrasonic.app import android.content.Context import android.os.StrictMode import androidx.multidex.MultiDexApplication +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin import org.koin.core.logger.Level @@ -24,6 +27,8 @@ import timber.log.Timber.DebugTree class UApp : MultiDexApplication() { + private var ioScope = CoroutineScope(Dispatchers.IO) + init { instance = this if (BuildConfig.DEBUG) @@ -36,8 +41,12 @@ class UApp : MultiDexApplication() { if (BuildConfig.DEBUG) { Timber.plant(DebugTree()) } - if (Settings.debugLogToFile) { - FileLoggerTree.plantToTimberForest() + + // In general we should not access the settings from the main thread to avoid blocking... + ioScope.launch { + if (Settings.debugLogToFile) { + FileLoggerTree.plantToTimberForest() + } } startKoin { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 32e9d29c..1ed527ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -29,7 +29,6 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4 -import org.moire.ultrasonic.service.DownloadService.Companion.getInstance import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Settings @@ -72,7 +71,7 @@ class MediaPlayerController( var controller: MediaController? = null - fun onCreate() { + fun onCreate(onCreated: () -> Unit) { if (created) return externalStorageMonitor.onCreate { reset() } isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault @@ -80,32 +79,25 @@ class MediaPlayerController( mediaControllerFuture.addListener({ controller = mediaControllerFuture.get() + Timber.i("MediaController Instance received") + controller?.addListener(object : Player.Listener { /* * Log all events */ -// override fun onEvents(player: Player, events: Player.Events) { -// //Timber.i("Media3 Event: %s", events) -// } + override fun onEvents(player: Player, events: Player.Events) { + for (i in 0 until events.size()) { + Timber.i("Media3 Event, event type: %s", events[i]) + } + } - // override fun onIsLoadingChanged(isLoading: Boolean) { -// super.onIsLoadingChanged(isLoading) -// } -// -// override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { -// super.onPlayWhenReadyChanged(playWhenReady, reason) -// } -// -// override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) { -// super.onPlaylistMetadataChanged(mediaMetadata) -// } -// /* * This will be called everytime the playlist has changed. */ override fun onTimelineChanged(timeline: Timeline, reason: Int) { legacyPlaylistManager.rebuildPlaylist(controller!!) + serializeCurrentSession() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -125,6 +117,10 @@ class MediaPlayerController( } }) + onCreated() + + Timber.i("MediaPlayerController creation complete") + // controller?.play() }, MoreExecutors.directExecutor()) @@ -134,7 +130,7 @@ class MediaPlayerController( } created = true - Timber.i("MediaPlayerController created") + Timber.i("MediaPlayerController started") } private fun playerStateChangedHandler() { @@ -145,10 +141,6 @@ class MediaPlayerController( Player.STATE_READY -> { if (isPlaying) { scrobbler.scrobble(currentPlaying, false) - } else { - playbackStateSerializer.serialize( - playList, currentMediaItemIndex, playerPosition - ) } } Player.STATE_ENDED -> { @@ -156,6 +148,11 @@ class MediaPlayerController( } } + // Save playback state + playbackStateSerializer.serialize( + playList, currentMediaItemIndex, playerPosition + ) + // Update widget if (currentPlaying != null) { updateWidget(currentPlaying.track) @@ -237,8 +234,9 @@ class MediaPlayerController( seekTo(currentPlayingIndex, currentPlayingPosition) } + prepare() + if (autoPlay) { - prepare() play() } @@ -246,11 +244,6 @@ class MediaPlayerController( } } - @Synchronized - fun preload() { - getInstance() - } - @Synchronized fun play(index: Int) { controller?.seekTo(index, 0L) @@ -356,12 +349,6 @@ class MediaPlayerController( } else { downloader.checkDownloads() } - - playbackStateSerializer.serialize( - legacyPlaylistManager.playlist, - currentMediaItemIndex, - playerPosition - ) } @Synchronized @@ -370,11 +357,7 @@ class MediaPlayerController( val filteredSongs = songs.filterNotNull() downloader.downloadBackground(filteredSongs, save) - playbackStateSerializer.serialize( - legacyPlaylistManager.playlist, - currentMediaItemIndex, - playerPosition - ) + serializeCurrentSession() } fun stopJukeboxService() { @@ -439,12 +422,6 @@ class MediaPlayerController( downloader.clearActiveDownloads() downloader.clearBackground() - playbackStateSerializer.serialize( - legacyPlaylistManager.playlist, - currentMediaItemIndex, - playerPosition - ) - jukeboxMediaPlayer.updatePlaylist() } @@ -453,13 +430,16 @@ class MediaPlayerController( controller?.removeMediaItem(position) + jukeboxMediaPlayer.updatePlaylist() + } + + @Synchronized + private fun serializeCurrentSession() { playbackStateSerializer.serialize( legacyPlaylistManager.playlist, currentMediaItemIndex, playerPosition ) - - jukeboxMediaPlayer.updatePlaylist() } @Synchronized @@ -625,7 +605,7 @@ class MediaPlayerController( } init { - Timber.i("MediaPlayerController constructed") + Timber.i("MediaPlayerController instance initiated") } enum class InsertionMode { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 70e9df92..dfdbeac3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -29,32 +29,37 @@ class MediaPlayerLifecycleSupport : KoinComponent { onCreate(false, null) } - private fun onCreate(autoPlay: Boolean, afterCreated: Runnable?) { + private fun onCreate(autoPlay: Boolean, afterRestore: Runnable?) { if (created) { - afterCreated?.run() + afterRestore?.run() return } - mediaPlayerController.onCreate() - if (autoPlay) mediaPlayerController.preload() + mediaPlayerController.onCreate { + restoreLastSession(autoPlay, afterRestore) + } + CacheCleaner().clean() + created = true + Timber.i("LifecycleSupport created") + } + + private fun restoreLastSession(autoPlay: Boolean, afterRestore: Runnable?) { playbackStateSerializer.deserialize { + Timber.i("Restoring %s songs", it!!.songs.size) + mediaPlayerController.restore( - it!!.songs, + it.songs, it.currentPlayingIndex, it.currentPlayingPosition, autoPlay, false ) - afterCreated?.run() + afterRestore?.run() } - - CacheCleaner().clean() - created = true - Timber.i("LifecycleSupport created") } fun onDestroy() { From c2ac1d436fd4360f7ff57baf112d12a1284189fb Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 20 Apr 2022 21:56:30 +0200 Subject: [PATCH 41/67] Use previous values for channel id --- .../moire/ultrasonic/playback/MediaNotificationProvider.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 8ef86f09..17d0b0eb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -148,9 +148,9 @@ internal class MediaNotificationProvider(context: Context) : } companion object { - private const val NOTIFICATION_ID = 1001 - private const val NOTIFICATION_CHANNEL_ID = "default_channel_id" - private const val NOTIFICATION_CHANNEL_NAME = "Now playing" + private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic" + private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service" + private const val NOTIFICATION_ID = 3033 private fun getSmallIconResId(): Int { return R.drawable.ic_stat_ultrasonic } From b6e890b26cd7aa30c50fe6946f783e5681b9531e Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 20 Apr 2022 22:03:17 +0200 Subject: [PATCH 42/67] Cleanup --- ultrasonic/lint-baseline.xml | 57 +------------------ .../ultrasonic/activity/NavigationActivity.kt | 5 +- ultrasonic/src/main/res/values-cs/strings.xml | 10 ---- ultrasonic/src/main/res/values-de/strings.xml | 12 ---- ultrasonic/src/main/res/values-es/strings.xml | 12 ---- ultrasonic/src/main/res/values-fr/strings.xml | 12 ---- ultrasonic/src/main/res/values-hu/strings.xml | 10 ---- ultrasonic/src/main/res/values-it/strings.xml | 10 ---- ultrasonic/src/main/res/values-nl/strings.xml | 12 ---- ultrasonic/src/main/res/values-pl/strings.xml | 10 ---- .../src/main/res/values-pt-rBR/strings.xml | 12 ---- ultrasonic/src/main/res/values-pt/strings.xml | 10 ---- ultrasonic/src/main/res/values-ru/strings.xml | 10 ---- .../src/main/res/values-zh-rCN/strings.xml | 12 ---- ultrasonic/src/main/res/values/strings.xml | 12 ---- 15 files changed, 4 insertions(+), 202 deletions(-) diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 3e743d06..62a8f675 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -12,17 +12,6 @@ column="66"/> - - - - @@ -257,50 +246,6 @@ column="1"/> - - - - - - - - - - - - - - - - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 031f56a6..af7c8fd4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -27,6 +27,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.FragmentContainerView +import androidx.media3.common.MediaItem import androidx.media3.common.Player.STATE_BUFFERING import androidx.media3.common.Player.STATE_READY import androidx.navigation.NavController @@ -417,8 +418,8 @@ class NavigationActivity : AppCompatActivity() { if (nowPlayingView != null) { val playerState: Int = mediaPlayerController.playbackState if (playerState == STATE_BUFFERING || playerState == STATE_READY) { - val file: DownloadFile? = mediaPlayerController.currentPlayingLegacy - if (file != null) { + val item: MediaItem? = mediaPlayerController.currentMediaItem + if (item != null) { nowPlayingView?.visibility = View.VISIBLE } } else { diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index bcf81ec5..5e8dd6ef 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -238,10 +238,6 @@ 75 Historie hledání vyčištěna Nastavení vyhledávání - Odesílat upozornění přehrávání přes bluetooth - Odesílat bluetooth upozornění - Odesílat obrázky alb přes bluetooth (může způsobit selhávání bluetooth upozornění) - Obrázky alb přes bluetooth Spravovat servery Adresa serveru Název @@ -249,12 +245,6 @@ Stahovat škálované obrázky ze serveru místo plné velikosti (šetří přenos dat) Škálování obrázků alb na serveru Uživatelské jméno - Zobrazit ovládání na zamknuté obrazovce - Zobrazí ovládání přehrávače na zamknuté obrazovce - Zobrazení upozornění - Vždy zobrazovat upozornění - Vždy zobrazovat upozornění přehrávané skladby při vytvoření playlistu - Zobrazovat přehrávanou skladbu ve stavovém panelu Zobrazovat přehrávanou skladbu Zobrazovat přehrávanou skladbu v aktivitách Zobrazovat číslo skladby diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index 762ecb7f..b8f35122 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -278,12 +278,6 @@ 75 Suchhistorie gelöscht Sucheinstellungen - Wiedergabe-Benachrichtigungen über Bluetooth senden - Bluetooth-Benachrichtigung - Albumcover über Bluetooth versenden (kann dazu führen, dass Bluetooth-Benachrichtigungen fehlschlagen) - Album Cover über Bluetooth - Die aktuellen Wiedergabeliste wird nicht an verbundene Geräte gesendet. Das kann die Kompatibilität mit AVRCP 1.3 Geräten herstellen, wenn die aktuelle Titelanzeige nicht dargestellt wird - Deaktiviere senden der aktuellen Wiedergabeliste Server verwalten Server Adresse Name @@ -292,12 +286,6 @@ Serverseitige Skalierung der Cover Benutzername Server Farbe - Steuerelemente auf Sperrbildschirm - Wiedergabeelemente auf dem Sperrbildschirm anzeigen - Benachrichtigungen anzeigen - Immer Benachrichtigungen zeigen - Benachrichtigung beim Abspielen immer anzeigen, wenn Einträge in der Wiedergabeliste sind - Abspielbenachrichtigung in der Statusleiste anzeigen Aktuellen Titel anzeigen Aktuellen Titel in allen Aktivitäten anzeigen Titelnummer anzeigen diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 6ef40581..0a6e78d5 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -278,12 +278,6 @@ 75 Se ha limpiado el historial de búsqueda Configuración de la búsqueda - Enviar notificaciones de reproducción vía Bluetooth - Enviar notificaciones Bluetooth - Enviar la carátula del álbum vía Bluetooth (Puede causar que las notificaciones Bluetooth fallen) - Carátula del Álbum vía Bluetooth - La lista de reproducción actual no se enviará a los dispositivos conectados. Esto puede restaurar la compatibilidad con dispositivos AVRCP 1.3, cuando la visualización de la pista actual no se actualiza - Desactivar el envío de la lista de reproducción actual Administrar servidores Dirección del servidor Nombre @@ -292,12 +286,6 @@ Escalado de caratulas en el servidor Nombre de usuario Color del servidor - Mostrar controles en la pantalla de bloqueo - Mostrar controles de reproducción en la pantalla de bloqueo - Mostrar notificación - Mostrar siempre la notificación - Mostrar siempre la notificación de reproduciendo ahora cuando la lista de reproducción contiene datos - Mostrar la notificación de reproduciendo ahora en la barra de estado Mostrar reproduciendo ahora Mostrar la pista que se esta reproduciendo en todas las actividades Mostrar número de pista diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 6be37c10..c70bd81b 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -258,12 +258,6 @@ 75 Historique des recherches effacé Paramètres de recherche - Envoyer des notifications de lecture via Bluetooth - Envoyer une notification Bluetooth - Envoyer la pochette de l\'album via Bluetooth (peut causer l\'échec des notifications Bluetooth) - Pochette de l\'album via Bluetooth - La liste de lecture ne sera pas envoyée aux appareils connectés. Cela peut restaurer la compatibilité avec les appareils AVRCP 1.3 lorsque l’affichage de la piste actuelle n’est pas mise à jour. - Désactiver l’envoi de la liste de lecture Gérer les serveurs Adresse du serveur Nom @@ -272,12 +266,6 @@ Mise à l\'échelle des pochettes d\'album sur le serveur Nom d\'utilisateur Couleur du serveur - Boutons de contrôle sur l\'écran de verrouillage - Afficher les contrôles de lecture sur l\'écran de verrouillage - Notifications - Toujours afficher les notifications - Toujours afficher la notification de lecture en cours lorsque la liste de lecture est remplie - Afficher la notification d\'un nouveau titre en lecture dans la barre d\'état Montrer la lecture en cours Afficher les pistes en cours de lecture dans les autres activités d\'Ultrasonic Afficher le numéro du titre diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 5327976b..9e508a6c 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -246,10 +246,6 @@ 75 Keresési előzmények törölve. Keresés beállításai - Lejátszási értesítések küldése Bluetooth-on. - Bluetooth értesítések küldése - Albumborító küldése Bluetooth-on (Problémát okozhat a Bluetooth értesítéseknél) - Albumborító Bluetooth-on Kiszolgálók kezelése Kiszolgáló címe Név @@ -257,12 +253,6 @@ Teljes méretű helyett átméretezett képek letöltése a kiszolgálóról (sávszélesség-takarékos). Albumborító átméretezés (Kiszolgáló-oldali) Felhasználónév - Képernyőzár kezelése - Lejátszó-kezelőpanel megjelenítése a képernyőzáron. - Értesítések megjelenítése - Állandó kijelzés - Lejátszás jelzése az értesítési sávon, míg a várólista aktív. - Lejátszás jelzése az értesítési sávon. Lejátszó-kezelőpanel Lejátszó-kezelőpanel megjelenítése minden oldalon. Sorszám megjelenítése diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 802f288f..9253f199 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -233,22 +233,12 @@ 75 Storico ricerche eliminato Impostazioni ricerca - Invia notifiche di riproduzione via Bluetooth - Invia notifica Bluetooth - Invia copertine degli album tramite Bluetooth (potrebbe causare errori nelle notifiche) - Copertine Album tramite Bluetooth Indirizzo Server Nome Password Scarica dal server le immagini ridimensionate (risparmia larghezza di banda) Ridimensionamento copertine Album lato server Username - Mostra i controlli del blocco schermo - Mostra i controlli di riproduzione sulla schermata di blocco - Mostra notifica - Mostra sempre notifica - Mostra sempre la notifica In Riproduzione quando viene popolata la playlist - Mostra la notifica In Riproduzione nella barra di stato Mostra In Riproduzione Mostra la traccia attualmente in riproduzione in tutte le attività Visualizza numero traccia diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 84e4cd5e..e5b3e6a7 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -278,12 +278,6 @@ 75 Zoekgeschiedenis gewist Zoekinstellingen - Afspeelmeldingen sturen via bluetooth - Bluetoothmelding sturen - Albumhoezen versturen via bluetooth (dit kan leiden tot mislukte bluetoothmeldingen) - Albumhoezen versturen via bluetooth - De lijst ‘Nu aan het afspelen’ wordt niet gedeeld met verbonden apparaten. Hierdoor wordt de comptabiliteit met AVCRP 1.3-apparaten hersteld als het huidige nummer niet wordt bijgewerkt. - ‘Nu aan het afspelen’-lijst niet delen Manage Servers Serveradres Naam @@ -292,12 +286,6 @@ Verkleinde afbeeldingen ophalen van server Gebruikersnaam Serverkleur - Vergrendelschermbediening tonen - Toont afspeelbediening op het vergrendelscherm - Melding tonen - Altijd melding tonen - Altijd een \"nu aan het afspelen\"-melding tonen tijdens het samenstellen van een afspeellijst - \"Nu aan het afspelen\"-melding tonen op de statusbalk \"Nu aan het afspelen\"-melding tonen Toont het momenteel afspelende nummer in alle activiteiten Itemnummer tonen diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 1ad64489..7e5e6373 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -238,10 +238,6 @@ 75 Wyczyść historię wyszukiwania Ustawienia wyszukiwania - Wysyła powiadomienia o odtwarzaniu przez Bluetooth - Wysyłaj powiadomienia Bluetooth - Wysyła okładki przez Bluetooth (może powodować problemy z powiadomieniami) - Okładki przez Bluetooth Manage Servers Adres serwera Nazwa @@ -249,12 +245,6 @@ Pobiera przeskalowane obrazy z serwera zamiast pełnego rozmiaru (oszczędza ilość przesyłanych danych) Skalowanie okładek po stronie serwera Nazwa użytkownika - Wyświetlaj na ekranie blokady - Wyświetla widżet odtwarzacza na ekranie blokady - Wyświetlaj powiadomienia - Zawsze wyświetlaj powiadomienia - Zawsze wyświetla powiadomienia o odtwarzanych utworach, gdy playlista jest wypełniona - Wyświetla powiadomienia o odtwarzanym utworze na pasku statusu Wyświetlaj powiadomienia o utworach Wyświetla bieżący utwór we wszystkich aktywnościach Wyświetlaj numer utworu diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index f5aede4e..c8201db1 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -253,12 +253,6 @@ 75 Histórico de pesquisas apagado Configurações de Pesquisa - Enviar notificações de reprodução via Bluetooth - Notificações via Bluetooth - Enviar a arte do álbum via Bluetooth (Pode causar falhas nas notificações do Bluetooth) - Arte do Álbum via Bluetooth - A Lista Tocando Agora não será enviada aos dispositivos conectados. Isso pode restaurar a compatibilidade com dispositivos AVRCP 1.3 quando a exibição da trilha atual não é atualizada - Desativar Envio da Lista Tocando Agora Gerenciar Servidores Endereço do Servidor Nome @@ -266,12 +260,6 @@ Baixar imagens reduzidas do servidor ao invés do tamanho completo (economiza banda) Reduzir Arte dos Álbuns Login - Controles na Tela de Bloqueio - Mostrar controles de reprodução na tela de bloqueio - Mostrar Notificações - Sempre Mostrar Notificações - Sempre mostrar a reprodução atual quando uma playlist é preenchida - Mostrar a notificação de reprodução atual na barra de status Mostrar Reprodução Atual Mostrar a faixa tocada atualmente em todas as atividades Mostrar o Número da Faixa diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 404a807d..373681e2 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -238,10 +238,6 @@ 75 Histórico de pesquisas apagado Configurações de Pesquisa - Envia notificações de reprodução via Bluetooth - Notificações via Bluetooth - Envia a arte do álbum via Bluetooth (Pode causar falhas nas notificações do Bluetooth) - Arte do Álbum via Bluetooth Manage Servers Endereço do Servidor Nome @@ -249,12 +245,6 @@ Descarrega imagens reduzidas do servidor ao invés do tamanho completo (economiza banda) Reduzir Arte dos Álbuns Login - Controles no Ecrã de Bloqueio - Mostra controles de reprodução no ecrã de bloqueio - Mostrar Notificações - Sempre Mostrar Notificações - Sempre mostrar a reprodução atual quando uma playlist é preenchida - Mostra a notificação de reprodução atual na barra de estado Mostrar Reprodução Atual Mostrar a faixa tocada atualmente em todas as atividades Mostrar o Número da Faixa diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 99e79a04..2bbb030a 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -264,10 +264,6 @@ 75 История поиска очищена Настройки поиска - Отправлять уведомления о воспроизведении через Bluetooth - Отправить уведомление Bluetooth - Отправить обложку альбома через Bluetooth (может привести к сбою уведомлений Bluetooth) - Обложка альбома через Bluetooth Управление серверами Адрес сервера Имя @@ -275,12 +271,6 @@ Загрузка масштабированных изображений с сервера вместо полноразмерного (экономит трафик) Серверное масштабирование обложек альбомов Имя пользователя - Показать блокировку экрана - Показать элементы управления воспроизведением на экране блокировки - Показывать уведомления - Всегда показывать уведомления - Всегда показывать воспроизведение, когда плейлист заполнен - Показать уведомление о воспроизведении в строке состояния Показать что сейчас играет Показать текущий воспроизводимый трек во всех активностях Показать номер трека diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index a6e84a59..6f20b74f 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -258,12 +258,6 @@ 75 搜索记录已清除 搜索设置 - 通过蓝牙发送播放通知 - 发送蓝牙通知 - 通过蓝牙发送专辑封面(可能导致蓝牙通知失败) - 通过蓝牙发送专辑封面 - 现在播放列表不会发送到已连接的设备。 当前曲目显示未更新时,这可能会恢复AVRCP 1.3的设备的兼容性。 - 禁用发送正在播放列表 管理服务器 服务器地址 名称 @@ -272,12 +266,6 @@ 服务器端专辑图片缩放 用户名 服务器颜色 - 锁屏显示控制器 - 在锁定屏幕上显示播放控件 - 显示通知 - 总是显示通知 - 当播放列表有音乐时,总是在通知栏显示播放信息 - 在状态栏中显示正在播放通知 显示正在播放 在所有活动页面显示正在播放信息 显示曲目编号 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 1cddd882..5ca00267 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -281,12 +281,6 @@ 75 Search history cleared Search Settings - Send playback notifications via Bluetooth - Send Bluetooth Notification - Send album art over Bluetooth (May cause Bluetooth notifications to fail) - Album Art Over Bluetooth - Now Playing List won\'t be sent to connected devices. This may restore compatibility with AVRCP 1.3 devices, when current track display is not updated - Disable sending of Now Playing List Manage Servers Server Address Name @@ -295,12 +289,6 @@ Server-Side Album Art Scaling Username Server color - Show Lock Screen Controls - Show playback controls on the lock screen - Show Notification - Always Show Notification - Always show now playing notification when playlist is populated - Show now playing notification in the status bar Show Now Playing Show currently playing track in all activities Show Track Number From dda86b42c7143224964698f3e8177912c44d3120 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 20 Apr 2022 22:54:59 +0200 Subject: [PATCH 43/67] Fix prefs --- .../ultrasonic/fragment/SettingsFragment.kt | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 784059dc..70374c21 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -74,9 +74,6 @@ class SettingsFragment : private var chatRefreshInterval: ListPreference? = null private var directoryCacheTime: ListPreference? = null private var mediaButtonsEnabled: CheckBoxPreference? = null - private var lockScreenEnabled: CheckBoxPreference? = null - private var sendBluetoothNotifications: CheckBoxPreference? = null - private var sendBluetoothAlbumArt: CheckBoxPreference? = null private var showArtistPicture: CheckBoxPreference? = null private var sharingDefaultDescription: EditTextPreference? = null private var sharingDefaultGreeting: EditTextPreference? = null @@ -120,7 +117,7 @@ class SettingsFragment : showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE) customCacheLocation = findPreference(Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION) - sharingDefaultGreeting!!.text = shareGreeting + sharingDefaultGreeting?.text = shareGreeting setupClearSearchPreference() setupCacheLocationPreference() } @@ -294,23 +291,16 @@ class SettingsFragment : sharingDefaultExpiration!!.summary = sharingDefaultExpiration!!.text sharingDefaultDescription!!.summary = sharingDefaultDescription!!.text sharingDefaultGreeting!!.summary = sharingDefaultGreeting!!.text - if (!mediaButtonsEnabled!!.isChecked) { - lockScreenEnabled!!.isChecked = false - lockScreenEnabled!!.isEnabled = false - } - if (!sendBluetoothNotifications!!.isChecked) { - sendBluetoothAlbumArt!!.isChecked = false - sendBluetoothAlbumArt!!.isEnabled = false - } - if (debugLogToFile!!.isChecked) { - debugLogToFile!!.summary = getString( + + if (debugLogToFile?.isChecked == true) { + debugLogToFile?.summary = getString( R.string.settings_debug_log_path, ultrasonicDirectory, FileLoggerTree.FILENAME ) } else { - debugLogToFile!!.summary = "" + debugLogToFile?.summary = "" } - showArtistPicture!!.isEnabled = shouldUseId3Tags + showArtistPicture?.isEnabled = shouldUseId3Tags } private fun setHideMedia(hide: Boolean) { From d550eabf8877bf1280607bc23438c0fedea897a0 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 21 Apr 2022 10:12:05 +0200 Subject: [PATCH 44/67] Always call prepare on adding items --- .../kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt | 1 - .../org/moire/ultrasonic/service/MediaPlayerController.kt | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index af7c8fd4..bcd991ff 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -50,7 +50,6 @@ import org.moire.ultrasonic.data.ServerSettingDao import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.provider.SearchSuggestionProvider -import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.RxBus diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 1ed527ea..d4ae2bb8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -343,8 +343,9 @@ class MediaPlayerController( if (shuffle) isShufflePlayEnabled = true + prepare() + if (autoPlay) { - prepare() play(0) } else { downloader.checkDownloads() From 2e1e627b7aca03f755a65018b3637367431cbd14 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 21 Apr 2022 10:45:37 +0200 Subject: [PATCH 45/67] - Set Wake mode flag according to Offline status - Use Rx to trigger CheckDownloads() - Fix #680 by listening to PositionDiscontinuity - Throttle RxEvents --- .../ultrasonic/playback/PlaybackService.kt | 14 ++++++++----- .../ultrasonic/service/DownloadService.kt | 6 ++++-- .../moire/ultrasonic/service/Downloader.kt | 14 +++++++------ .../service/MediaPlayerController.kt | 20 ++++++++++++++----- .../org/moire/ultrasonic/service/RxBus.kt | 2 ++ 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index edd24315..17818963 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -35,6 +35,7 @@ import org.koin.core.component.inject import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Constants @@ -74,9 +75,16 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // Update the API endpoint when the active server has changed val newClient: SubsonicAPIClient by inject() apiDataSource.setAPIClient(newClient) + + // Set the player wake mode + player.setWakeMode(getWakeModeFlag()) } } + private fun getWakeModeFlag(): Int { + return if (ActiveServerProvider.isOffline()) C.WAKE_MODE_LOCAL else C.WAKE_MODE_NETWORK + } + override fun onDestroy() { player.release() mediaLibrarySession.release() @@ -90,10 +98,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent { @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) private fun initializeSessionAndPlayer() { - /* - * TODO: - * * Could be refined to use WAKE_MODE_LOCAL when offline.... - */ setMediaNotificationProvider(MediaNotificationProvider(UApp.applicationContext())) @@ -112,7 +116,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // Create the player player = ExoPlayer.Builder(this) .setAudioAttributes(getAudioAttributes(), true) - .setWakeMode(C.WAKE_MODE_NETWORK) + .setWakeMode(getWakeModeFlag()) .setHandleAudioBecomingNoisy(true) .setMediaSourceFactory(DefaultMediaSourceFactory(cacheDataSourceFactory)) .setRenderersFactory(renderer) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index f6b2ed49..4f585402 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -56,7 +56,7 @@ class DownloadService : Service() { updateNotification() instance = this - Timber.i("DownloadService created") + Timber.i("DownloadService initiated") } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { @@ -74,10 +74,11 @@ class DownloadService : Service() { mediaSession = null } catch (ignored: Throwable) { } - Timber.i("DownloadService stopped") + Timber.i("DownloadService destroyed") } fun notifyDownloaderStopped() { + Timber.i("DownloadService stopped") isInForeground = false stopForeground(true) stopSelf() @@ -193,6 +194,7 @@ class DownloadService : Service() { } else { context.startService(Intent(context, DownloadService::class.java)) } + Timber.i("DownloadService started") } Util.sleepQuietly(100L) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 30e75bf9..9681e4aa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -64,6 +64,13 @@ class Downloader( private val rxBusSubscription: CompositeDisposable = CompositeDisposable() + init { + // Check downloads if the playlist changed + rxBusSubscription += RxBus.playlistObservable.subscribe { + checkDownloads() + } + } + private var downloadChecker = object : Runnable { override fun run() { try { @@ -100,11 +107,6 @@ class Downloader( wifiLock = Util.createWifiLock(toString()) wifiLock?.acquire() } - - // Check downloads if the playlist changed - rxBusSubscription += RxBus.playlistObservable.subscribe { - checkDownloads() - } } fun stop() { @@ -133,7 +135,7 @@ class Downloader( @Suppress("ComplexMethod", "ComplexCondition") @Synchronized - fun checkDownloadsInternal() { + private fun checkDownloadsInternal() { if (!Util.isExternalStoragePresent() || !storageMonitor.isExternalStorageAvailable) { return } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index d4ae2bb8..13d9dda7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -115,6 +115,20 @@ class MediaPlayerController( legacyPlaylistManager.updateCurrentPlaying(mediaItem) publishPlaybackState() } + + /* + * If the same item is contained in a playlist multiple times directly after each + * other, Media3 on emits a PositionDiscontinuity event. + * Can be removed if https://github.com/androidx/media/issues/68 is fixed. + */ + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + playerStateChangedHandler() + publishPlaybackState() + } }) onCreated() @@ -149,9 +163,7 @@ class MediaPlayerController( } // Save playback state - playbackStateSerializer.serialize( - playList, currentMediaItemIndex, playerPosition - ) + serializeCurrentSession() // Update widget if (currentPlaying != null) { @@ -347,8 +359,6 @@ class MediaPlayerController( if (autoPlay) { play(0) - } else { - downloader.checkDownloads() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 8107569d..93116743 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.subjects.PublishSubject +import java.util.concurrent.TimeUnit class RxBus { companion object { @@ -37,6 +38,7 @@ class RxBus { playlistPublisher.observeOn(AndroidSchedulers.mainThread()) .replay(1) .autoConnect(0) + .throttleLatest(1, TimeUnit.SECONDS) // Commands val dismissNowPlayingCommandPublisher: PublishSubject = From 7ba599f58c3f9abd8898f7b580b9d3fd98d83106 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 21 Apr 2022 14:39:16 +0200 Subject: [PATCH 46/67] Remove throttle again, causes calls from wrong thread... --- ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 93116743..3f792ca9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -38,7 +38,6 @@ class RxBus { playlistPublisher.observeOn(AndroidSchedulers.mainThread()) .replay(1) .autoConnect(0) - .throttleLatest(1, TimeUnit.SECONDS) // Commands val dismissNowPlayingCommandPublisher: PublishSubject = From 9cdba9a27a48219d72f316cbc921325ca9cb51dc Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 21 Apr 2022 14:42:24 +0200 Subject: [PATCH 47/67] Ensure main thread usage in Rx subscription. --- .../org/moire/ultrasonic/fragment/PlayerFragment.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 785ccd19..bb06c722 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -393,13 +393,18 @@ class PlayerFragment : } // Observe playlist changes and update the UI - // FIXME rxBusSubscription += RxBus.playlistObservable.subscribe { - onPlaylistChanged() + // Use launch to ensure running it in the main thread + launch { + onPlaylistChanged() + } } rxBusSubscription += RxBus.playerStateObservable.subscribe { - update() + // Use launch to ensure running it in the main thread + launch { + update() + } } mediaPlayerController.controller?.addListener(object : Player.Listener { From 1d236aa6e35561944c8e1d61e28f93ee92a6b69f Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 21 Apr 2022 14:52:11 +0200 Subject: [PATCH 48/67] RM unused PlayerState.kt --- .../org/moire/ultrasonic/domain/PlayerState.kt | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PlayerState.kt diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PlayerState.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PlayerState.kt deleted file mode 100644 index 7ae5dd66..00000000 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PlayerState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.moire.ultrasonic.domain - -enum class PlayerState { - IDLE, - DOWNLOADING, - PREPARING, - PREPARED, - STARTED, - STOPPED, - PAUSED, - COMPLETED -} From 827654c0c1f37040f1476591644c3883041574cc Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 21 Apr 2022 20:24:43 +0200 Subject: [PATCH 49/67] Create lock before launching the coroutine, return always a copy of the list and not the real one. --- .../org/moire/ultrasonic/playback/LegacyPlaylistManager.kt | 2 +- .../org/moire/ultrasonic/service/PlaybackStateSerializer.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 0afaccd2..74cf6a8a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -77,7 +77,7 @@ class LegacyPlaylistManager : KoinComponent { // Public facing playlist (immutable) val playlist: List - get() = _playlist + get() = _playlist.toList() @get:Synchronized val playlistDuration: Long diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index a8d62a7c..6d015cb5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -43,8 +43,8 @@ class PlaybackStateSerializer : KoinComponent { ) { if (!setup.get()) return - ioScope.launch { - if (lock.tryLock()) { + if (lock.tryLock()) { + ioScope.launch { try { serializeNow(songs, currentPlayingIndex, currentPlayingPosition) } finally { From 707339b88b3895ebb904de73c3d8f506b9663f8c Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 22 Apr 2022 21:03:57 +0200 Subject: [PATCH 50/67] Try to fix the mess :) --- .../playback/LegacyPlaylistManager.kt | 2 +- .../moire/ultrasonic/service/Downloader.kt | 8 ++-- .../service/MediaPlayerController.kt | 26 ++++++++++--- .../service/PlaybackStateSerializer.kt | 37 +++++++++---------- .../org/moire/ultrasonic/service/RxBus.kt | 26 ++++++++++--- 5 files changed, 64 insertions(+), 35 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 74cf6a8a..0afaccd2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -77,7 +77,7 @@ class LegacyPlaylistManager : KoinComponent { // Public facing playlist (immutable) val playlist: List - get() = _playlist.toList() + get() = _playlist @get:Synchronized val playlistDuration: Long diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 9681e4aa..f16a2d39 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -150,18 +150,20 @@ class Downloader( // Store the result in a flag to know if changes have occurred var listChanged = cleanupActiveDownloads() + val playlist = legacyPlaylistManager.playlist + // Check if need to preload more from playlist val preloadCount = Settings.preloadCount // Start preloading at the current playing song var start = mediaController.currentMediaItemIndex - if (start == -1) start = 0 + if (start == -1 || start > playlist.size) start = 0 - val end = (start + preloadCount).coerceAtMost(mediaController.mediaItemCount) + val end = (start + preloadCount).coerceAtMost(playlist.size) for (i in start until end) { - val download = legacyPlaylistManager.playlist[i] + val download = playlist[i] // Set correct priority (the lower the number, the higher the priority) download.priority = i diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 13d9dda7..1ff9aa0b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -18,6 +18,9 @@ import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors import io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp @@ -61,6 +64,8 @@ class MediaPlayerController( private val rxBusSubscription: CompositeDisposable = CompositeDisposable() + private var mainScope = CoroutineScope(Dispatchers.Main) + private var sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java)) @@ -94,10 +99,10 @@ class MediaPlayerController( /* * This will be called everytime the playlist has changed. + * We run the event through RxBus in order to throttle them */ override fun onTimelineChanged(timeline: Timeline, reason: Int) { legacyPlaylistManager.rebuildPlaylist(controller!!) - serializeCurrentSession() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -143,6 +148,20 @@ class MediaPlayerController( isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault } + rxBusSubscription += RxBus.throttledPlaylistObservable.subscribe { + // Even though Rx should launch on the main thread it doesn't always :( + mainScope.launch { + serializeCurrentSession() + } + } + + rxBusSubscription += RxBus.throttledPlayerStateObservable.subscribe { + // Even though Rx should launch on the main thread it doesn't always :( + mainScope.launch { + serializeCurrentSession() + } + } + created = true Timber.i("MediaPlayerController started") } @@ -162,9 +181,6 @@ class MediaPlayerController( } } - // Save playback state - serializeCurrentSession() - // Update widget if (currentPlaying != null) { updateWidget(currentPlaying.track) @@ -367,8 +383,6 @@ class MediaPlayerController( if (songs == null) return val filteredSongs = songs.filterNotNull() downloader.downloadBackground(filteredSongs, save) - - serializeCurrentSession() } fun stopJukeboxService() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index 6d015cb5..552c399c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -9,8 +9,6 @@ package org.moire.ultrasonic.service import android.content.Context import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.locks.Lock -import java.util.concurrent.locks.ReentrantLock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -30,9 +28,6 @@ class PlaybackStateSerializer : KoinComponent { private val context by inject() - private val lock: Lock = ReentrantLock() - private val setup = AtomicBoolean(false) - private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) @@ -41,25 +36,24 @@ class PlaybackStateSerializer : KoinComponent { currentPlayingIndex: Int, currentPlayingPosition: Int ) { - if (!setup.get()) return + if (isSerializing.get() || !isSetup.get()) return - if (lock.tryLock()) { - ioScope.launch { - try { - serializeNow(songs, currentPlayingIndex, currentPlayingPosition) - } finally { - lock.unlock() - } - } + isSerializing.set(true) + + ioScope.launch { + serializeNow(songs, currentPlayingIndex, currentPlayingPosition) + }.invokeOnCompletion { + isSerializing.set(false) } } fun serializeNow( - songs: Iterable, + referencedList: Iterable, currentPlayingIndex: Int, currentPlayingPosition: Int ) { val state = State() + val songs = referencedList.toList() for (downloadFile in songs) { state.songs.add(downloadFile.track) @@ -78,16 +72,15 @@ class PlaybackStateSerializer : KoinComponent { } fun deserialize(afterDeserialized: (State?) -> Unit?) { - + if (isDeserializing.get()) return ioScope.launch { try { - lock.lock() deserializeNow(afterDeserialized) - setup.set(true) + isSetup.set(true) } catch (all: Exception) { Timber.e(all, "Had a problem deserializing:") } finally { - lock.unlock() + isDeserializing.set(false) } } } @@ -108,4 +101,10 @@ class PlaybackStateSerializer : KoinComponent { afterDeserialized(state) } } + + companion object { + private val isSetup = AtomicBoolean(false) + private val isSerializing = AtomicBoolean(false) + private val isDeserializing = AtomicBoolean(false) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 3f792ca9..03859659 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.service +import android.os.Looper import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -8,42 +9,55 @@ import io.reactivex.rxjava3.subjects.PublishSubject import java.util.concurrent.TimeUnit class RxBus { + companion object { + private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper()) + var activeServerChangePublisher: PublishSubject = PublishSubject.create() var activeServerChangeObservable: Observable = - activeServerChangePublisher.observeOn(AndroidSchedulers.mainThread()) + activeServerChangePublisher.observeOn(mainThread()) val themeChangedEventPublisher: PublishSubject = PublishSubject.create() val themeChangedEventObservable: Observable = - themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + themeChangedEventPublisher.observeOn(mainThread()) val musicFolderChangedEventPublisher: PublishSubject = PublishSubject.create() val musicFolderChangedEventObservable: Observable = - musicFolderChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + musicFolderChangedEventPublisher.observeOn(mainThread()) val playerStatePublisher: PublishSubject = PublishSubject.create() val playerStateObservable: Observable = - playerStatePublisher.observeOn(AndroidSchedulers.mainThread()) + playerStatePublisher.observeOn(mainThread()) .replay(1) .autoConnect(0) + val throttledPlayerStateObservable: Observable = + playerStatePublisher.observeOn(mainThread()) + .replay(1) + .autoConnect(0) + .throttleLatest(300, TimeUnit.MILLISECONDS) val playlistPublisher: PublishSubject> = PublishSubject.create() val playlistObservable: Observable> = - playlistPublisher.observeOn(AndroidSchedulers.mainThread()) + playlistPublisher.observeOn(mainThread()) .replay(1) .autoConnect(0) + val throttledPlaylistObservable: Observable> = + playlistPublisher.observeOn(mainThread()) + .replay(1) + .autoConnect(0) + .throttleLatest(300, TimeUnit.MILLISECONDS) // Commands val dismissNowPlayingCommandPublisher: PublishSubject = PublishSubject.create() val dismissNowPlayingCommandObservable: Observable = - dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + dismissNowPlayingCommandPublisher.observeOn(mainThread()) } data class StateWithTrack( From 46846bd5c92d3ad1b062d5c0209072d9be7d6a34 Mon Sep 17 00:00:00 2001 From: Nite Date: Sun, 24 Apr 2022 08:44:36 +0200 Subject: [PATCH 51/67] Incremented max playlist size Fixed DownloadService start Minor fixes --- .../playback/LegacyPlaylistManager.kt | 4 +- .../ultrasonic/service/DownloadService.kt | 37 ++++++++++--------- .../moire/ultrasonic/service/Downloader.kt | 2 +- .../service/MediaPlayerController.kt | 4 -- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 0afaccd2..88d5dc13 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -30,7 +30,9 @@ class LegacyPlaylistManager : KoinComponent { @JvmField var currentPlaying: DownloadFile? = null - private val mediaItemCache = LRUCache(1000) + // TODO This limits the maximum size of the playlist. + // This will be fixed when this class is refactored and removed + private val mediaItemCache = LRUCache(2000) val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() val downloader: Downloader by inject() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index 4f585402..e3c75d54 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -26,6 +26,8 @@ import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.SimpleServiceBinder import org.moire.ultrasonic.util.Util import timber.log.Timber +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit /** * Android Foreground service which is used to download tracks even when the app is not visible @@ -56,6 +58,7 @@ class DownloadService : Service() { updateNotification() instance = this + startedSemaphore.release() Timber.i("DownloadService initiated") } @@ -176,29 +179,29 @@ class DownloadService : Service() { @Volatile private var instance: DownloadService? = null private val instanceLock = Any() + private val startedSemaphore: Semaphore = Semaphore(0) @JvmStatic fun getInstance(): DownloadService? { val context = UApp.applicationContext() - // Try for twenty times to retrieve a running service, - // sleep 100 millis between each try, - // and run the block that creates a service only synchronized. - for (i in 0..19) { + if (instance != null) return instance + synchronized(instanceLock) { if (instance != null) return instance - synchronized(instanceLock) { - if (instance != null) return instance - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService( - Intent(context, DownloadService::class.java) - ) - } else { - context.startService(Intent(context, DownloadService::class.java)) - } - Timber.i("DownloadService started") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService( + Intent(context, DownloadService::class.java) + ) + } else { + context.startService(Intent(context, DownloadService::class.java)) } - Util.sleepQuietly(100L) + Timber.i("DownloadService starting...") + if (startedSemaphore.tryAcquire(10, TimeUnit.SECONDS)) { + Timber.i("DownloadService started") + return instance + } + Timber.w("DownloadService failed to start!") + return null } - return instance } @JvmStatic @@ -208,7 +211,7 @@ class DownloadService : Service() { } @JvmStatic - fun executeOnStartedMediaPlayerService( + fun executeOnStartedDownloadService( taskToExecute: (DownloadService) -> Unit ) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index f16a2d39..7532c263 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -205,7 +205,7 @@ class Downloader( private fun startDownloadOnService(file: DownloadFile) { if (file.isDownloading) return file.prepare() - DownloadService.executeOnStartedMediaPlayerService { + DownloadService.executeOnStartedDownloadService { FileUtil.createDirectoryForParent(file.pinnedFile) file.isFailed = false file.downloadTask = DownloadTask(file) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 1ff9aa0b..a712915b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -347,10 +347,6 @@ class MediaPlayerController( ) { var insertAt = 0 - if (insertionMode == InsertionMode.CLEAR) { - clear() - } - when (insertionMode) { InsertionMode.CLEAR -> clear() InsertionMode.APPEND -> insertAt = mediaItemCount From cbe3992b01524984843dc0bc7b988edf3bebda24 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 2 May 2022 20:46:01 +0200 Subject: [PATCH 52/67] Fixed Notification Ids to be different for Downloader and Player Fixed multiple start of periodic CheckDownloads --- .../playback/MediaNotificationProvider.kt | 4 +++- .../moire/ultrasonic/service/Downloader.kt | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 17d0b0eb..7bccd678 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -144,13 +144,15 @@ internal class MediaNotificationProvider(context: Context) : NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW ) + channel.setShowBadge(false) + notificationManager.createNotificationChannel(channel) } companion object { private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic" private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service" - private const val NOTIFICATION_ID = 3033 + private const val NOTIFICATION_ID = 3032 private fun getSmallIconResId(): Int { return R.drawable.ic_stat_ultrasonic } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 7532c263..b1d4b1b7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -46,6 +46,7 @@ class Downloader( var started: Boolean = false var shouldStop: Boolean = false + var isPolling: Boolean = false private val downloadQueue = PriorityQueue() private val activelyDownloading = mutableListOf() @@ -67,6 +68,7 @@ class Downloader( init { // Check downloads if the playlist changed rxBusSubscription += RxBus.playlistObservable.subscribe { + Timber.v("Playlist has changed, checking Downloads...") checkDownloads() } } @@ -79,10 +81,14 @@ class Downloader( } catch (all: Exception) { Timber.e(all, "checkDownloads() failed.") } finally { - if (!shouldStop) { - Handler(Looper.getMainLooper()).postDelayed(this, CHECK_INTERVAL) - } else { - shouldStop = false + if (!isPolling) { + isPolling = true + if (!shouldStop) { + Handler(Looper.getMainLooper()).postDelayed(this, CHECK_INTERVAL) + } else { + shouldStop = false + isPolling = false + } } } } @@ -98,6 +104,7 @@ class Downloader( @Synchronized fun start() { + if (started) return started = true // Start our loop @@ -110,6 +117,7 @@ class Downloader( } fun stop() { + if (!started) return started = false shouldStop = true wifiLock?.release() @@ -210,6 +218,7 @@ class Downloader( file.isFailed = false file.downloadTask = DownloadTask(file) file.downloadTask!!.start() + Timber.v("startDownloadOnService started downloading file ${file.completeFile}") } } @@ -313,6 +322,7 @@ class Downloader( } } + Timber.v("downloadBackground Checking Downloads") checkDownloads() } @@ -490,6 +500,7 @@ class Downloader( inputStream.safeClose() outputStream.safeClose() CacheCleaner().cleanSpace() + Timber.v("DownloadTask checking downloads") checkDownloads() } } From 34e0178db396cfe86b66606b3eb848e4eb7d6f9e Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 2 May 2022 20:47:38 +0200 Subject: [PATCH 53/67] Fixed Lint errors --- .../kotlin/org/moire/ultrasonic/service/DownloadService.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index e3c75d54..a111d29b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -18,16 +18,15 @@ import android.os.IBinder import android.support.v4.media.session.MediaSessionCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit import org.koin.android.ext.android.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.SimpleServiceBinder -import org.moire.ultrasonic.util.Util import timber.log.Timber -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit /** * Android Foreground service which is used to download tracks even when the app is not visible From 4a00494647836fa34055a7a10cf8e1cd81a89c6f Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 4 May 2022 11:24:07 +0200 Subject: [PATCH 54/67] Readd Headphone plug and Bluetooth listeners --- ultrasonic/src/main/AndroidManifest.xml | 8 ++ .../receiver/BluetoothIntentReceiver.java | 100 ++++++++++++++++++ .../ultrasonic/fragment/SettingsFragment.kt | 71 +++++++++++++ .../service/MediaPlayerLifecycleSupport.kt | 46 ++++++++ .../org/moire/ultrasonic/util/Constants.kt | 5 + .../org/moire/ultrasonic/util/Settings.kt | 14 +++ .../moire/ultrasonic/util/SettingsDelegate.kt | 4 + ultrasonic/src/main/res/values-cs/strings.xml | 7 ++ ultrasonic/src/main/res/values-de/strings.xml | 7 ++ ultrasonic/src/main/res/values-es/strings.xml | 7 ++ ultrasonic/src/main/res/values-fr/strings.xml | 7 ++ ultrasonic/src/main/res/values-hu/strings.xml | 7 ++ ultrasonic/src/main/res/values-it/strings.xml | 2 + ultrasonic/src/main/res/values-nl/strings.xml | 7 ++ ultrasonic/src/main/res/values-pl/strings.xml | 7 ++ .../src/main/res/values-pt-rBR/strings.xml | 7 ++ ultrasonic/src/main/res/values-pt/strings.xml | 7 ++ ultrasonic/src/main/res/values-ru/strings.xml | 7 ++ .../src/main/res/values-zh-rCN/strings.xml | 7 ++ .../src/main/res/values-zh-rTW/strings.xml | 1 + ultrasonic/src/main/res/values/arrays.xml | 5 + .../src/main/res/values/setting_keys.xml | 4 + ultrasonic/src/main/res/values/strings.xml | 7 ++ ultrasonic/src/main/res/xml/settings.xml | 14 +++ 24 files changed, 358 insertions(+) create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java create mode 100644 ultrasonic/src/main/res/values/setting_keys.xml diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 27946252..a421856c 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -83,6 +83,14 @@ + + + + + + + + diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java new file mode 100644 index 00000000..cf7844e7 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java @@ -0,0 +1,100 @@ +/* + 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 . + + Copyright 2010 (C) Sindre Mehus + */ +package org.moire.ultrasonic.receiver; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.moire.ultrasonic.util.Constants; +import org.moire.ultrasonic.util.Settings; + +import timber.log.Timber; + +/** + * Resume or pause playback on Bluetooth A2DP connect/disconnect. + * + * @author Sindre Mehus + */ +@SuppressLint("MissingPermission") +public class BluetoothIntentReceiver extends BroadcastReceiver +{ + @Override + public void onReceive(Context context, Intent intent) + { + int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + String action = intent.getAction(); + String name = device != null ? device.getName() : "Unknown"; + String address = device != null ? device.getAddress() : "Unknown"; + + Timber.d("A2DP State: %d; Action: %s; Device: %s; Address: %s", state, action, name, address); + + boolean actionBluetoothDeviceConnected = false; + boolean actionBluetoothDeviceDisconnected = false; + boolean actionA2dpConnected = false; + boolean actionA2dpDisconnected = false; + + if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) + { + actionBluetoothDeviceConnected = true; + } + else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action) || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED.equals(action)) + { + actionBluetoothDeviceDisconnected = true; + } + + if (state == android.bluetooth.BluetoothA2dp.STATE_CONNECTED) actionA2dpConnected = true; + else if (state == android.bluetooth.BluetoothA2dp.STATE_DISCONNECTED) actionA2dpDisconnected = true; + + boolean resume = false; + boolean pause = false; + + switch (Settings.getResumeOnBluetoothDevice()) + { + case Constants.PREFERENCE_VALUE_ALL: resume = actionA2dpConnected || actionBluetoothDeviceConnected; + break; + case Constants.PREFERENCE_VALUE_A2DP: resume = actionA2dpConnected; + break; + } + + switch (Settings.getPauseOnBluetoothDevice()) + { + case Constants.PREFERENCE_VALUE_ALL: pause = actionA2dpDisconnected || actionBluetoothDeviceDisconnected; + break; + case Constants.PREFERENCE_VALUE_A2DP: pause = actionA2dpDisconnected; + break; + } + + if (resume) + { + Timber.i("Connected to Bluetooth device %s address %s, resuming playback.", name, address); + context.sendBroadcast(new Intent(Constants.CMD_RESUME_OR_PLAY).setPackage(context.getPackageName())); + } + + if (pause) + { + Timber.i("Disconnected from Bluetooth device %s address %s, requesting pause.", name, address); + context.sendBroadcast(new Intent(Constants.CMD_PAUSE).setPackage(context.getPackageName())); + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 70374c21..70bc45d0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -12,6 +12,7 @@ import android.os.Bundle import android.provider.DocumentsContract import android.provider.SearchRecentSuggestions import android.view.View +import androidx.annotation.StringRes import androidx.fragment.app.DialogFragment import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference @@ -78,6 +79,8 @@ class SettingsFragment : private var sharingDefaultDescription: EditTextPreference? = null private var sharingDefaultGreeting: EditTextPreference? = null private var sharingDefaultExpiration: TimeSpanPreference? = null + private var resumeOnBluetoothDevice: Preference? = null + private var pauseOnBluetoothDevice: Preference? = null private var debugLogToFile: CheckBoxPreference? = null private var customCacheLocation: CheckBoxPreference? = null @@ -113,6 +116,9 @@ class SettingsFragment : sharingDefaultGreeting = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING) sharingDefaultExpiration = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION) + resumeOnBluetoothDevice = + findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE) + pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE) debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE) showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE) customCacheLocation = findPreference(Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION) @@ -120,6 +126,7 @@ class SettingsFragment : sharingDefaultGreeting?.text = shareGreeting setupClearSearchPreference() setupCacheLocationPreference() + setupBluetoothDevicePreferences() } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -253,6 +260,70 @@ class SettingsFragment : startActivityForResult(intent, SELECT_CACHE_ACTIVITY) } + private fun setupBluetoothDevicePreferences() { + val resumeSetting = Settings.resumeOnBluetoothDevice + val pauseSetting = Settings.pauseOnBluetoothDevice + resumeOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(resumeSetting) + pauseOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(pauseSetting) + resumeOnBluetoothDevice!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + showBluetoothDevicePreferenceDialog( + R.string.settings_playback_resume_on_bluetooth_device, + Settings.resumeOnBluetoothDevice + ) { choice: Int -> + Settings.resumeOnBluetoothDevice = choice + resumeOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(choice) + } + true + } + pauseOnBluetoothDevice!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + showBluetoothDevicePreferenceDialog( + R.string.settings_playback_pause_on_bluetooth_device, + Settings.pauseOnBluetoothDevice + ) { choice: Int -> + Settings.pauseOnBluetoothDevice = choice + pauseOnBluetoothDevice!!.summary = bluetoothDevicePreferenceToString(choice) + } + true + } + } + + private fun showBluetoothDevicePreferenceDialog( + @StringRes title: Int, + defaultChoice: Int, + onChosen: (Int) -> Unit + ) { + val choice = intArrayOf(defaultChoice) + AlertDialog.Builder(activity).setTitle(title) + .setSingleChoiceItems( + R.array.bluetoothDeviceSettingNames, defaultChoice + ) { _: DialogInterface?, i: Int -> choice[0] = i } + .setNegativeButton(R.string.common_cancel) { dialogInterface: DialogInterface, _: Int -> + dialogInterface.cancel() + } + .setPositiveButton(R.string.common_ok) { dialogInterface: DialogInterface, _: Int -> + onChosen(choice[0]) + dialogInterface.dismiss() + } + .create().show() + } + + private fun bluetoothDevicePreferenceToString(preferenceValue: Int): String { + return when (preferenceValue) { + Constants.PREFERENCE_VALUE_ALL -> { + getString(R.string.settings_playback_bluetooth_all) + } + Constants.PREFERENCE_VALUE_A2DP -> { + getString(R.string.settings_playback_bluetooth_a2dp) + } + Constants.PREFERENCE_VALUE_DISABLED -> { + getString(R.string.settings_playback_bluetooth_disabled) + } + else -> "" + } + } + private fun setupClearSearchPreference() { val clearSearchPreference = findPreference(Constants.PREFERENCES_KEY_CLEAR_SEARCH_HISTORY) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index dfdbeac3..68701102 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -7,12 +7,18 @@ package org.moire.ultrasonic.service +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.media.AudioManager import android.view.KeyEvent import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.ifNotNull import timber.log.Timber @@ -24,6 +30,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { private val mediaPlayerController by inject() private var created = false + private var headsetEventReceiver: BroadcastReceiver? = null fun onCreate() { onCreate(false, null) @@ -40,6 +47,8 @@ class MediaPlayerLifecycleSupport : KoinComponent { restoreLastSession(autoPlay, afterRestore) } + registerHeadsetReceiver() + CacheCleaner().clean() created = true Timber.i("LifecycleSupport created") @@ -73,6 +82,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { ) mediaPlayerController.clear(false) + applicationContext().unregisterReceiver(headsetEventReceiver) mediaPlayerController.onDestroy() created = false @@ -98,6 +108,42 @@ class MediaPlayerLifecycleSupport : KoinComponent { } } + /** + * The Headset Intent Receiver is responsible for resuming playback when a headset is inserted + * and pausing it when it is removed. + * Unfortunately this Intent can't be registered in the AndroidManifest, so it works only + * while Ultrasonic is running. + */ + private fun registerHeadsetReceiver() { + + headsetEventReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val extras = intent.extras ?: return + + Timber.i("Headset event for: %s", extras["name"]) + + val state = extras.getInt("state") + + if (state == 0) { + if (!mediaPlayerController.isJukeboxEnabled) { + mediaPlayerController.pause() + } + } else if (state == 1) { + if (!mediaPlayerController.isJukeboxEnabled && + Settings.resumePlayOnHeadphonePlug && !mediaPlayerController.isPlaying + ) { + mediaPlayerController.prepare() + mediaPlayerController.play() + } + } + } + } + + val headsetIntentFilter = IntentFilter(AudioManager.ACTION_HEADSET_PLUG) + + applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter) + } + @Suppress("MagicNumber", "ComplexMethod") private fun handleKeyEvent(event: KeyEvent) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index 687282b3..75b349b4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -100,8 +100,13 @@ object Constants { const val PREFERENCES_KEY_HARDWARE_OFFLOAD = "use_hw_offload" const val PREFERENCES_KEY_CATEGORY_NOTIFICATIONS = "notificationsCategory" const val PREFERENCES_KEY_FIRST_RUN_EXECUTED = "firstRunExecuted" + const val PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE = "resumeOnBluetoothDevice" + const val PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE = "pauseOnBluetoothDevice" const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile" const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage" + const val PREFERENCE_VALUE_ALL = 0 + const val PREFERENCE_VALUE_A2DP = 1 + const val PREFERENCE_VALUE_DISABLED = 2 const val FILENAME_PLAYLIST_SER = "downloadstate.ser" const val ALBUM_ART_FILE = "folder.jpeg" const val STARRED = "starred" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index c62e1ce5..356fca5c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -131,6 +131,20 @@ object Settings { @JvmStatic var mediaButtonsEnabled by BooleanSetting(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true) + var resumePlayOnHeadphonePlug + by BooleanSetting(R.string.setting_keys_resume_play_on_headphones_plug, true) + + @JvmStatic + var resumeOnBluetoothDevice by IntSetting( + Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE, + Constants.PREFERENCE_VALUE_DISABLED + ) + + @JvmStatic + var pauseOnBluetoothDevice by IntSetting( + Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE, + Constants.PREFERENCE_VALUE_A2DP + ) @JvmStatic var showNowPlaying diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SettingsDelegate.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SettingsDelegate.kt index 3e24864e..03cfbd31 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SettingsDelegate.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SettingsDelegate.kt @@ -76,4 +76,8 @@ class BooleanSetting(private val key: String, private val defaultValue: Boolean override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) = sharedPreferences.edit { putBoolean(key, value) } + + constructor(stringId: Int, defaultValue: Boolean = false) : this( + Util.appContext().getString(stringId), defaultValue + ) } diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 5e8dd6ef..70334b12 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -222,6 +222,8 @@ 3 skladby 5 skladeb Neomezeně + Pokračovat po připojení sluchátek + Aplikace spustí pozastavené přehrávání po připojení kabelu sluchátek do přístroje. 1 10 100 @@ -310,6 +312,11 @@ Zobrazit umělce albumArt Vícenásobné roky + Pokračovat v přehrávání po připojení bluetooth přístroje + Pozastavení přehrávání při odpojení bluetooth přístroje + Všechny bluetooth přístroje + Pouze audio (A2DP) přístroje + Vypnuto Možnosti ladění aplikace Zapisovat logy ladění do souboru Soubory logů jsou dostupné v %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index b8f35122..a859a80a 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -260,6 +260,8 @@ 3 Titel 5 Titel Unbegrenzt + Fortsetzen mit Kopfhörer + Die App setzt eine pausierte Wiedergabe beim Anschließen der Kopfhörer fort. Benutzername und Passwort des Scrobble Service(s) müssen im Server gesetzt sein Gespielte Musik scrobbeln 1 @@ -360,6 +362,11 @@ Künstler*in anzeigen Album Cover Mehrere Jahre + Wiedergabe fortsetzen, wenn ein Bluetooth Gerät verbunden wurde + Wiedergabe pausieren, wenn ein Bluetooth Gerät getrennt wurde + Alle Bluetooth Geräte + Nur Audio (A2DP) Geräte + Deaktiviert Debug Optionen Schreibe Debug Log in Datei Die Log Dateien sind unter %1$s/%2$s verfügbar diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 0a6e78d5..d01cbb81 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -260,6 +260,8 @@ 3 canciónes 5 canciónes Ilimitado + Reanudar al insertar los auriculares + La aplicación reanudará la reproducción en pausa al insertar los auriculares en el dispositivo. Recuerda configurar tu nombre de usuario y contraseña en los servicios de Scrobble en el servidor Hacer Scrobble de mis reproducciones 1 @@ -360,6 +362,11 @@ Mostrar artista Caratula del Álbum Múltiples años + Reanudar al conectar un dispositivo Bluetooth + Pausar al desconectar un dispositivo Bluetooth + Todos los dispositivos Bluetooth + Solo dispositivos de audio (A2DP) + Deshabilitado Opciones de depuración Escribir registro de depuración en un archivo Los archivos de registro están disponibles en %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index c70bd81b..309c136d 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -240,6 +240,8 @@ 3 morceaux 5 morceaux Illimité + Reprise à l\'insertion des écouteurs + L\'application reprendra la lecture lors de l\'insertion du casque dans l\'appareil. Pensez à configurer votre nom d’utilisateur et votre mot de passe dans le(s) service(s) Scrobble sur le serveur. Scrobbler mes lectures 1 @@ -337,6 +339,11 @@ Afficher l\'artiste Pochette d\'album Années multiples + Reprendre lorsqu’un appareil Bluetooth se connecte + Mettre en pause lorsqu’un appareil Bluetooth se déconnecte + Tous les appareils Bluetooth + Seulement les appareils audio (A2DP) + Désactivé Paramètres de debug Enregistrer les logs de debug dans des fichiers Les fichiers de log sont disponibles dans %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 9e508a6c..117ec7bd 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -228,6 +228,8 @@ 3 dal 5 dal Korlátlan + Folytatás a fejhallgató behelyezésekor + Az alkalmazás folytatja a szüneteltetett lejátszást a fejhallgató behelyezésekor a készülékbe. Ne felejtsd el beállítani a Scrobble szolgáltatónál használt felhasználóneved és jelszavad a szervereden Scrobble engedélyezése 1 @@ -318,6 +320,11 @@ Ugrás az előadóhoz albumArt Több év + Folytatás Bluetooth eszköz csatlakozásakor + Szünet Bluetooth eszköz kikapcsolásakor + Minden Bluetooth eszköz + Csak audio (A2DP) eszközök + Kikapcsolva Hibakeresési lehetőségek Hibakeresési napló írása fájlba A naplófájlok elérhetőek a következő helyen: %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 9253f199..b7fe167f 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -217,6 +217,7 @@ 3 canzoni 5 canzoni Illimitato + Riprendi all\'inserimento delle cuffie 1 10 100 @@ -278,6 +279,7 @@ Commenta \"%s\" è stato rimosso dalla playlist Condividi canzoni via + Disabilitato Elimina Il periodo di prova è terminato. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index e5b3e6a7..850d0a53 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -260,6 +260,8 @@ 3 nummers 5 nummers Ongelimiteerd + Hervatten bij aansluiten van hoofdtelefoon + Het afspelen wordt hervat zodra er een hoofdtelefoon wordt aangesloten. Let op: stel je gebruikersnaam en wachtwoord van je scrobble-dienst(en) in op je Subsonic-server Scrobbelen 1 @@ -360,6 +362,11 @@ Artiest tonen Albumhoes Meerdere jaren + Hervatten bij verbinding met bluetoothapparaat + Pauzeren als verbinding met bluetoothapparaat verbroken is + Alle bluetoothapparaten + Alleen audio-apparaten (AD2P) + Uitgeschakeld Foutopsporingsopties Foutopsporingslogboek bijhouden De logboeken worden opgeslagen in %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 7e5e6373..c6031dd2 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -222,6 +222,8 @@ 3 utwory 5 utworów Bez limitu + Wznawiaj po podłączeniu słuchawek + Aplikacja wznowi zatrzymane odtwarzanie po podpięciu słuchawek. 1 10 100 @@ -307,6 +309,11 @@ Wyświetlaj artystę Okładka Z różnych lat + Resume when a Bluetooth device is connected + Pause when a Bluetooth device is disconnected + All Bluetooth devices + Only audio (A2DP) devices + Wyłączone Configured servers Are you sure you want to delete the server? Editing server diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index c8201db1..4fafe9e8 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -235,6 +235,8 @@ 3 músicas 5 músicas Ilimitado + Retomar ao Inserir Fone de Ouvido + O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo Lembre-se de configurar usuário e senha nos serviços Scrobble do servidor Registrar Minhas Músicas 1 @@ -329,6 +331,11 @@ Mostrar Artista albumArt Anos Múltiplos + Retomar ao Conectar Dispositivo Bluetooth + Pausar ao Desconectar Dispositivo Bluetooth + Todos os dispositivos Bluetooth + Somente dispositivos de áudio (A2DP) + Desativado Opções de Depuração Log de Depuração em Arquivo Os arquivos com log estão disponíveis em %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 373681e2..33821f9b 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -222,6 +222,8 @@ 3 músicas 5 músicas Ilimitado + Retomar ao inserir Auscultadores + O aplicativo retomará a reprodução em pausa na inserção dos auscultadores no dispositivo. 1 10 100 @@ -307,6 +309,11 @@ Mostrar Artista albumArt Múltiplos Anos + Resume when a Bluetooth device is connected + Pause when a Bluetooth device is disconnected + All Bluetooth devices + Only audio (A2DP) devices + Disabilitando Configured servers Are you sure you want to delete the server? Editing server diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 2bbb030a..27ae0982 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -246,6 +246,8 @@ 3 песни 5 песен Неограниченный + Возобновить подключение наушников + Приложение возобновит приостановленное воспроизведение после того, как в устройство будут вставлены проводные наушники. Не забудьте установить своего пользователя и пароль в Скроббл сервисах на сервере. Скробблить мои воспроизведения 1 @@ -336,6 +338,11 @@ Показать исполнителей albumArt Несколько лет + Возобновить при подключении устройства Bluetooth + Пауза при отключении устройства Bluetooth + Все устройства Bluetooth + Только аудио (A2DP) устройства + Отключено Настройки отладки Записать журнал отладки в файл Файлы журнала доступны по адресу %1$s/%2$s diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 6f20b74f..bcb04683 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -240,6 +240,8 @@ 3 首歌 5 首歌 不限制 + 插入耳机时恢复播放 + 应用将在有线耳机插入设备时恢复已暂停的播放。 请记得在服务器上的 Scrobble 服务中设置您的用户名和密码 Scrobble我的播放列表 1 @@ -337,6 +339,11 @@ 显示艺术家 albumArt Multiple Years + 连接蓝牙设备时恢复播放 + 断开蓝牙设备时暂停播放 + 所有蓝牙设备 + 仅音频 (A2DP) 设备 + 已禁用 调试选项 将调试日志写入文件 日志文件可在 %1$s/%2$s 获取 diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index 784f9731..d12844da 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -54,5 +54,6 @@ 名稱 已停用 註記 + 已停用 刪除 diff --git a/ultrasonic/src/main/res/values/arrays.xml b/ultrasonic/src/main/res/values/arrays.xml index e645bdd6..511d7be6 100644 --- a/ultrasonic/src/main/res/values/arrays.xml +++ b/ultrasonic/src/main/res/values/arrays.xml @@ -231,6 +231,11 @@ @string/settings.share_hours @string/settings.share_days + + @string/settings.playback.bluetooth_all + @string/settings.playback.bluetooth_a2dp + @string/settings.playback.bluetooth_disabled + @string/language.default @string/language.zh_CN diff --git a/ultrasonic/src/main/res/values/setting_keys.xml b/ultrasonic/src/main/res/values/setting_keys.xml new file mode 100644 index 00000000..3378560d --- /dev/null +++ b/ultrasonic/src/main/res/values/setting_keys.xml @@ -0,0 +1,4 @@ + + + playback.resume_play_on_headphones_plug + \ No newline at end of file diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 5ca00267..4ace333f 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -256,6 +256,13 @@ Override the language You need to restart the app after changing the language Playback Control Settings + Resume when a Bluetooth device is connected + Pause when a Bluetooth device is disconnected + All Bluetooth devices + Only audio (A2DP) devices + Disabled + Resume on headphones insertion + App will resume paused playback on wired headphones insertion into device. Songs To Preload 1 song 10 songs diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 7eb7dc81..04f81008 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -90,6 +90,20 @@ a:key="incrementTime" a:title="@string/settings.increment_time" app:iconSpaceReserved="false"/> + + + Date: Wed, 4 May 2022 11:59:48 +0200 Subject: [PATCH 55/67] Add exported attributes, fix lint --- ultrasonic/lint-baseline.xml | 74 ++----------------------- ultrasonic/src/main/AndroidManifest.xml | 22 +++++--- 2 files changed, 19 insertions(+), 77 deletions(-) diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 62a8f675..521d62a7 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -55,18 +55,18 @@ errorLine2=" ~~~~~~~~"> @@ -77,7 +77,7 @@ errorLine2=" ~~~~~~~"> @@ -92,72 +92,6 @@ column="10"/> - - - - - - - - - - - - - - - - - - - - - - - - + @@ -71,7 +72,8 @@ - + @@ -83,7 +85,8 @@ - + @@ -93,7 +96,8 @@ + android:label="Ultrasonic (4x1)" + android:exported="false"> @@ -104,7 +108,8 @@ + android:label="Ultrasonic (4x2)" + android:exported="false"> @@ -115,7 +120,8 @@ + android:label="Ultrasonic (4x3)" + android:exported="false"> @@ -126,7 +132,8 @@ + android:label="Ultrasonic (4x4)" + android:exported="false"> @@ -135,7 +142,8 @@ android:name="android.appwidget.provider" android:resource="@xml/appwidget_info_4x4"/> - + From faf07f28871f8a9285309c1f7a963dce80534c8b Mon Sep 17 00:00:00 2001 From: Nite Date: Fri, 6 May 2022 18:51:12 +0200 Subject: [PATCH 56/67] Fixed not closed connection in CachedDataSource --- .../kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt index 64381403..79bae338 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt @@ -27,7 +27,7 @@ import timber.log.Timber @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) class CachedDataSource( private var upstreamDataSource: DataSource -) : BaseDataSource(false) { +) : BaseDataSource(true) { class Factory( private var upstreamDataSourceFactory: DataSource.Factory @@ -186,6 +186,8 @@ class CachedDataSource( transferEnded() responseByteStream?.close() responseByteStream = null + } else { + upstreamDataSource.close() } } From f790e29add844fd949a966530d7d259bb73fcbd4 Mon Sep 17 00:00:00 2001 From: Nite Date: Tue, 10 May 2022 18:15:37 +0200 Subject: [PATCH 57/67] Fixed AndroidManifest to receive Bluetooth events Set parallel downloads to 2 so the third thread can stream for playback --- ultrasonic/src/main/AndroidManifest.xml | 2 +- .../src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 0e955a94..8c3f2dde 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -86,7 +86,7 @@ + android:exported="true"> diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index b1d4b1b7..d36854a6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -353,7 +353,7 @@ class Downloader( } companion object { - const val PARALLEL_DOWNLOADS = 3 + const val PARALLEL_DOWNLOADS = 2 const val CHECK_INTERVAL = 5000L } From 6e1478d896fc7388ea3ab56baa5ac36e158f7858 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 30 May 2022 16:10:54 +0200 Subject: [PATCH 58/67] Pause playback when swiping away... --- .../ultrasonic/playback/PlaybackService.kt | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 17818963..3b59f42b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -40,6 +40,7 @@ import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings +import timber.log.Timber class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var player: ExoPlayer @@ -50,6 +51,8 @@ class PlaybackService : MediaLibraryService(), KoinComponent { private var rxBusSubscription = CompositeDisposable() + private var isStarted = false + /* * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, * and thereby customarily it is required to rebuild it.. @@ -70,15 +73,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent { override fun onCreate() { super.onCreate() initializeSessionAndPlayer() - - rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { - // Update the API endpoint when the active server has changed - val newClient: SubsonicAPIClient by inject() - apiDataSource.setAPIClient(newClient) - - // Set the player wake mode - player.setWakeMode(getWakeModeFlag()) - } } private fun getWakeModeFlag(): Int { @@ -86,9 +80,8 @@ class PlaybackService : MediaLibraryService(), KoinComponent { } override fun onDestroy() { - player.release() - mediaLibrarySession.release() - rxBusSubscription.dispose() + Timber.i("onDestroy called") + releasePlayerAndSession() super.onDestroy() } @@ -96,8 +89,22 @@ class PlaybackService : MediaLibraryService(), KoinComponent { return mediaLibrarySession } + override fun onTaskRemoved(rootIntent: Intent?) { + Timber.i("Pausing the playback because we were swiped away") + player.pause() + } + + private fun releasePlayerAndSession() { + player.release() + mediaLibrarySession.release() + rxBusSubscription.dispose() + isStarted = false + stopSelf() + } + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) private fun initializeSessionAndPlayer() { + if (isStarted) return setMediaNotificationProvider(MediaNotificationProvider(UApp.applicationContext())) @@ -134,6 +141,17 @@ class PlaybackService : MediaLibraryService(), KoinComponent { .setMediaItemFiller(CustomMediaItemFiller()) .setSessionActivity(getPendingIntentForContent()) .build() + + // Set a listener to update the API client when the active server has changed + rxBusSubscription += RxBus.activeServerChangeObservable.subscribe { + val newClient: SubsonicAPIClient by inject() + apiDataSource.setAPIClient(newClient) + + // Set the player wake mode + player.setWakeMode(getWakeModeFlag()) + } + + isStarted = true } private fun getPendingIntentForContent(): PendingIntent { From 608f86ac5f600b5ab942b81cd2a64f183aea1682 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 30 May 2022 16:24:24 +0200 Subject: [PATCH 59/67] Always call prepare before starting playback, otherwise resuming playback is difficult when the session was dismissed... --- .../kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index a712915b..ede9d503 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -283,6 +283,7 @@ class MediaPlayerController( if (jukeboxMediaPlayer.isEnabled) { jukeboxMediaPlayer.start() } else { + controller?.prepare() controller?.play() } } From 53ae0cd23293335f7fb186b9e48c694bc302a897 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 31 May 2022 10:18:23 +0200 Subject: [PATCH 60/67] Update copyright --- .../moire/ultrasonic/playback/PlaybackService.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 3b59f42b..ea65a16f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -1,17 +1,8 @@ /* - * Copyright 2021 The Android Open Source Project + * PlaybackService.kt + * Copyright (C) 2009-2022 Ultrasonic developers * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Distributed under terms of the GNU GPLv3 license. */ package org.moire.ultrasonic.playback @@ -71,6 +62,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { } override fun onCreate() { + Timber.i("onCreate called") super.onCreate() initializeSessionAndPlayer() } From 5e0dd14c4f87384ffd669cc6fdab919ddac0399c Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 31 May 2022 10:27:29 +0200 Subject: [PATCH 61/67] Shutdown service on exit button press --- .../org/moire/ultrasonic/activity/NavigationActivity.kt | 8 +++++--- .../org/moire/ultrasonic/playback/PlaybackService.kt | 6 ++++++ .../ultrasonic/service/MediaPlayerLifecycleSupport.kt | 2 ++ .../src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt | 5 +++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index bcd991ff..e243e704 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -1,6 +1,6 @@ /* * NavigationActivity.kt - * Copyright (C) 2009-2021 Ultrasonic developers + * Copyright (C) 2009-2022 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ @@ -42,6 +42,7 @@ import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.google.android.material.navigation.NavigationView import io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlin.system.exitProcess import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R @@ -66,7 +67,7 @@ import org.moire.ultrasonic.util.Util import timber.log.Timber /** - * The main Activity of Ultrasonic which loads all other screens as Fragments + * The main (and only) Activity of Ultrasonic which loads all other screens as Fragments */ @Suppress("TooManyFunctions") class NavigationActivity : AppCompatActivity() { @@ -366,7 +367,8 @@ class NavigationActivity : AppCompatActivity() { private fun exit() { lifecycleSupport.onDestroy() - finish() + finishAndRemoveTask() + exitProcess(0) } private fun showWelcomeDialog() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index ea65a16f..6f40a6df 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -143,6 +143,12 @@ class PlaybackService : MediaLibraryService(), KoinComponent { player.setWakeMode(getWakeModeFlag()) } + // Listen to the shutdown command + rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { + Timber.i("Received destroy command via Rx") + onDestroy() + } + isStarted = true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 68701102..2700747b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -82,6 +82,8 @@ class MediaPlayerLifecycleSupport : KoinComponent { ) mediaPlayerController.clear(false) + RxBus.shutdownCommandPublisher.onNext(Unit) + applicationContext().unregisterReceiver(headsetEventReceiver) mediaPlayerController.onDestroy() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 03859659..4bfdcdab 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -58,6 +58,11 @@ class RxBus { PublishSubject.create() val dismissNowPlayingCommandObservable: Observable = dismissNowPlayingCommandPublisher.observeOn(mainThread()) + + val shutdownCommandPublisher: PublishSubject = + PublishSubject.create() + val shutdownCommandObservable: Observable = + shutdownCommandPublisher.observeOn(mainThread()) } data class StateWithTrack( From 1e571e165cd6f6cfa87f721fd1a5bcc0998b45ea Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 6 Jun 2022 22:12:16 +0200 Subject: [PATCH 62/67] Don't ecit the process, and reinit Koin on resume --- .../ultrasonic/activity/NavigationActivity.kt | 20 +++++++++++++--- .../kotlin/org/moire/ultrasonic/app/UApp.kt | 24 +++++++++++++++---- .../moire/ultrasonic/service/Downloader.kt | 1 + .../service/MediaPlayerController.kt | 17 +++++++++++-- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index e243e704..d0499aa5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -42,10 +42,10 @@ import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.google.android.material.navigation.NavigationView import io.reactivex.rxjava3.disposables.CompositeDisposable -import kotlin.system.exitProcess import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSettingDao import org.moire.ultrasonic.fragment.OnBackPressedHandler @@ -99,6 +99,17 @@ class NavigationActivity : AppCompatActivity() { private var cachedServerCount: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { + Timber.d("onCreate called") + + // First check if Koin has been started + + if (UApp.instance != null && !UApp.instance!!.initiated) { + Timber.d("Starting Koin") + UApp.instance!!.startKoin() + } else { + Timber.d("No need to start Koin") + } + setUncaughtExceptionHandler() Util.applyTheme(this) @@ -226,6 +237,7 @@ class NavigationActivity : AppCompatActivity() { } override fun onResume() { + Timber.d("onResume called") super.onResume() Storage.reset() @@ -239,9 +251,11 @@ class NavigationActivity : AppCompatActivity() { } override fun onDestroy() { - super.onDestroy() + Timber.d("onDestroy called") rxBusSubscription.dispose() imageLoaderProvider.clearImageLoader() + UApp.instance!!.shutdownKoin() + super.onDestroy() } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { @@ -366,9 +380,9 @@ class NavigationActivity : AppCompatActivity() { } private fun exit() { + Timber.d("User choose to exit the app") lifecycleSupport.onDestroy() finishAndRemoveTask() - exitProcess(0) } private fun showWelcomeDialog() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index d5d31b6b..6d642063 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -1,13 +1,13 @@ package org.moire.ultrasonic.app import android.content.Context -import android.os.StrictMode import androidx.multidex.MultiDexApplication import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin import org.koin.core.logger.Level import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.di.appPermanentStorage @@ -31,17 +31,22 @@ class UApp : MultiDexApplication() { init { instance = this - if (BuildConfig.DEBUG) - StrictMode.enableDefaults() +// if (BuildConfig.DEBUG) +// StrictMode.enableDefaults() } + var initiated = false + override fun onCreate() { + initiated = true super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(DebugTree()) } + Timber.d("onCreate called") + // In general we should not access the settings from the main thread to avoid blocking... ioScope.launch { if (Settings.debugLogToFile) { @@ -49,8 +54,12 @@ class UApp : MultiDexApplication() { } } + startKoin() + } + + internal fun startKoin() { startKoin { - // TODO Currently there is a bug in Koin which makes necessary to set the loglevel to ERROR + // TODO Currently there is a bug in Koin which makes necessary to set the log level to ERROR logger(TimberKoinLogger(Level.ERROR)) // logger(TimberKoinLogger(Level.INFO)) @@ -67,8 +76,13 @@ class UApp : MultiDexApplication() { } } + internal fun shutdownKoin() { + stopKoin() + initiated = false + } + companion object { - private var instance: UApp? = null + var instance: UApp? = null fun applicationContext(): Context { return instance!!.applicationContext diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index d36854a6..100a8fdd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -66,6 +66,7 @@ class Downloader( private val rxBusSubscription: CompositeDisposable = CompositeDisposable() init { + Timber.i("Init called") // Check downloads if the playlist changed rxBusSubscription += RxBus.playlistObservable.subscribe { Timber.v("Playlist has changed, checking Downloads...") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index ede9d503..45a070ed 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -76,6 +76,8 @@ class MediaPlayerController( var controller: MediaController? = null + private lateinit var listeners: Player.Listener + fun onCreate(onCreated: () -> Unit) { if (created) return externalStorageMonitor.onCreate { reset() } @@ -86,7 +88,7 @@ class MediaPlayerController( Timber.i("MediaController Instance received") - controller?.addListener(object : Player.Listener { + listeners = object : Player.Listener { /* * Log all events @@ -134,7 +136,9 @@ class MediaPlayerController( playerStateChangedHandler() publishPlaybackState() } - }) + } + + controller?.addListener(listeners) onCreated() @@ -224,6 +228,12 @@ class MediaPlayerController( fun onDestroy() { if (!created) return + + // First stop listening to events + rxBusSubscription.dispose() + controller?.removeListener(listeners) + + // Shutdown the rest val context = UApp.applicationContext() externalStorageMonitor.onDestroy() context.stopService(Intent(context, DownloadService::class.java)) @@ -457,6 +467,9 @@ class MediaPlayerController( @Synchronized private fun serializeCurrentSession() { + // Don't serialize invalid sessions + if (currentMediaItemIndex == -1) return + playbackStateSerializer.serialize( legacyPlaylistManager.playlist, currentMediaItemIndex, From 59e37e62a69e8a82ad7a08129a2eb749d148a36b Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 7 Jun 2022 13:30:33 +0200 Subject: [PATCH 63/67] Call stopForeground(yes) --- .../main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 6f40a6df..b4ae1a91 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -91,6 +91,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { mediaLibrarySession.release() rxBusSubscription.dispose() isStarted = false + stopForeground(true) stopSelf() } From 147d7cd46ee3718323b50d0b1d1bc3ee2064c8e7 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 7 Jun 2022 16:00:51 +0200 Subject: [PATCH 64/67] Fix reappearing notification --- .../ultrasonic/playback/MediaNotificationProvider.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 7bccd678..5ecb4fe4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -100,19 +100,9 @@ internal class MediaNotificationProvider(context: Context) : builder.setLargeIcon(artworkBitmap) } val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle() - .setCancelButtonIntent( - actionFactory.createMediaActionPendingIntent( - ActionFactory.COMMAND_STOP - ) - ) .setShowActionsInCompactView(0, 1, 2) val notification: Notification = builder .setContentIntent(mediaController.sessionActivity) - .setDeleteIntent( - actionFactory.createMediaActionPendingIntent( - ActionFactory.COMMAND_STOP - ) - ) .setOnlyAlertOnce(true) .setSmallIcon(getSmallIconResId()) .setStyle(mediaStyle) From 70f8b7501976b298c501993bea78252867475546 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 7 Jun 2022 16:18:26 +0200 Subject: [PATCH 65/67] Completely stop and release the player on dismissing the app. --- .../kotlin/org/moire/ultrasonic/playback/PlaybackService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index b4ae1a91..23adf816 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -82,8 +82,8 @@ class PlaybackService : MediaLibraryService(), KoinComponent { } override fun onTaskRemoved(rootIntent: Intent?) { - Timber.i("Pausing the playback because we were swiped away") - player.pause() + Timber.i("Stopping the playback because we were swiped away") + releasePlayerAndSession() } private fun releasePlayerAndSession() { From 87c160610fbea166974cc7d0f4b37cd6296b6e75 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 8 Jun 2022 09:17:14 +0200 Subject: [PATCH 66/67] Clear widget when service is being shutdown --- .../org/moire/ultrasonic/activity/NavigationActivity.kt | 1 - .../kotlin/org/moire/ultrasonic/playback/PlaybackService.kt | 3 +++ .../ultrasonic/provider/UltrasonicAppWidgetProvider.kt | 1 + .../org/moire/ultrasonic/service/MediaPlayerController.kt | 5 +++++ .../src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt | 6 ++++++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index d0499aa5..2fefadc3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -102,7 +102,6 @@ class NavigationActivity : AppCompatActivity() { Timber.d("onCreate called") // First check if Koin has been started - if (UApp.instance != null && !UApp.instance!!.initiated) { Timber.d("Starting Koin") UApp.instance!!.startKoin() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 23adf816..8404d0f9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -87,6 +87,9 @@ class PlaybackService : MediaLibraryService(), KoinComponent { } private fun releasePlayerAndSession() { + // Broadcast that the service is being shutdown + RxBus.stopCommandPublisher.onNext(Unit) + player.release() mediaLibrarySession.release() rxBusSubscription.dispose() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt index ddc11e3f..e74fbbae 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt @@ -97,6 +97,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { playing: Boolean, setAlbum: Boolean ) { + Timber.d("Updating Widget") val res = context.resources val views = RemoteViews(context.packageName, layoutId) val title = currentSong?.title diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 45a070ed..749e30e0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -166,6 +166,11 @@ class MediaPlayerController( } } + rxBusSubscription += RxBus.stopCommandObservable.subscribe { + // Clear the widget when we stop the service + updateWidget(null) + } + created = true Timber.i("MediaPlayerController started") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 4bfdcdab..e472637a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -63,6 +63,12 @@ class RxBus { PublishSubject.create() val shutdownCommandObservable: Observable = shutdownCommandPublisher.observeOn(mainThread()) + + val stopCommandPublisher: PublishSubject = + PublishSubject.create() + val stopCommandObservable: Observable = + stopCommandPublisher.observeOn(mainThread()) + } data class StateWithTrack( From b6730f5a934a316b7c75c0086703ef0f8cea31c6 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 8 Jun 2022 10:02:04 +0200 Subject: [PATCH 67/67] Reset widget on manual exit as well. --- .../org/moire/ultrasonic/activity/NavigationActivity.kt | 4 ++++ .../src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 2fefadc3..7c399d6c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -380,6 +380,10 @@ class NavigationActivity : AppCompatActivity() { private fun exit() { Timber.d("User choose to exit the app") + + // Broadcast that the service is being shutdown + RxBus.stopCommandPublisher.onNext(Unit) + lifecycleSupport.onDestroy() finishAndRemoveTask() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index e472637a..8d996f8c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -68,7 +68,6 @@ class RxBus { PublishSubject.create() val stopCommandObservable: Observable = stopCommandPublisher.observeOn(mainThread()) - } data class StateWithTrack(