From 417496aacc6306cc0aaf04894a6ccbf95f54bbe4 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 17 Apr 2021 18:23:32 +0200 Subject: [PATCH 01/29] Remove unecessary null-check --- .../java/org/moire/ultrasonic/service/MediaPlayerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index c7ae0cf7..8cb422a1 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -536,7 +536,7 @@ public class MediaPlayerService extends Service { final MusicDirectory.Entry song = currentPlaying.getSong(); - if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(MediaPlayerService.this)) + if (song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(MediaPlayerService.this)) { MusicService musicService = MusicServiceFactory.getMusicService(MediaPlayerService.this); try From 26ba02200337b2f64a99fbdbe93d4ee5b66c79ea Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 12 Apr 2021 07:27:55 +0200 Subject: [PATCH 02/29] Modernize Service Notification Use the native Android way of generating playback notifications, by using a "Media Style" and creating a Media Session. --- .../service/MediaPlayerService.java | 310 +++++++++++++----- ultrasonic/src/main/res/values/strings.xml | 3 + 2 files changed, 233 insertions(+), 80 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 8cb422a1..d0ed1449 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -10,22 +10,20 @@ import android.content.Intent; import android.graphics.Bitmap; import android.os.Build; import android.os.IBinder; -import timber.log.Timber; -import android.view.View; -import android.widget.RemoteViews; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.view.KeyEvent; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import org.koin.java.KoinJavaComponent; import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.NavigationActivity; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.domain.RepeatMode; -import org.moire.ultrasonic.featureflags.Feature; -import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1; import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2; import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3; @@ -37,7 +35,10 @@ import org.moire.ultrasonic.util.ShufflePlayBuffer; import org.moire.ultrasonic.util.SimpleServiceBinder; import org.moire.ultrasonic.util.Util; +import java.util.ArrayList; + import kotlin.Lazy; +import timber.log.Timber; import static org.koin.java.KoinJavaComponent.inject; import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; @@ -70,11 +71,15 @@ public class MediaPlayerService extends Service private final Lazy downloaderLazy = inject(Downloader.class); private final Lazy localMediaPlayerLazy = inject(LocalMediaPlayer.class); private final Lazy nowPlayingEventDistributor = inject(NowPlayingEventDistributor.class); + private LocalMediaPlayer localMediaPlayer; private Downloader downloader; private ShufflePlayBuffer shufflePlayBuffer; private DownloadQueueSerializer downloadQueueSerializer; + private MediaSessionCompat mediaSession; + private MediaSessionCompat.Token mediaSessionToken; + private boolean isInForeground = false; private NotificationCompat.Builder notificationBuilder; @@ -143,6 +148,9 @@ public class MediaPlayerService extends Service shufflePlayBuffer = shufflePlayBufferLazy.getValue(); downloadQueueSerializer = downloadQueueSerializerLazy.getValue(); + initMediaSessions(); + + downloader.onCreate(); shufflePlayBuffer.onCreate(); @@ -150,6 +158,7 @@ public class MediaPlayerService extends Service setupOnCurrentPlayingChangedHandler(); setupOnPlayerStateChangedHandler(); setupOnSongCompletedHandler(); + localMediaPlayer.onPrepared = new Runnable() { @Override public void run() { @@ -174,10 +183,9 @@ public class MediaPlayerService extends Service manager.createNotificationChannel(channel); } - // We should use a single notification builder, otherwise the notification may not be updated - notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); // Update notification early. It is better to show an empty one temporarily than waiting too long and letting Android kill the app updateNotification(IDLE, null); + instance = this; Timber.i("MediaPlayerService created"); @@ -201,6 +209,7 @@ public class MediaPlayerService extends Service localMediaPlayer.release(); downloader.stop(); shufflePlayBuffer.onDestroy(); + mediaSession.release(); } catch (Throwable ignored) { } @@ -475,6 +484,9 @@ public class MediaPlayerService extends Service localMediaPlayer.onPlayerStateChanged = new BiConsumer() { @Override public void accept(PlayerState playerState, DownloadFile currentPlaying) { + // Notify MediaSession + updateMediaSession(currentPlaying, playerState); + if (playerState == PAUSED) { downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); @@ -597,6 +609,43 @@ public class MediaPlayerService extends Service } } + private void updateMediaSession(DownloadFile currentPlaying, PlayerState playerState) { + // Set Metadata + MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder(); + Context context = getApplicationContext(); + + if (currentPlaying != null) { + try { + MusicDirectory.Entry song = currentPlaying.getSong(); + + Bitmap cover = FileUtil.getAlbumArtBitmap(context, song, + Util.getMinDisplayMetric(context), true + ); + + metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtist()); + metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtist()); + metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbum()); + metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getTitle()); + metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover); + } catch (Exception e) { + Timber.e(e, "Error setting the metadata"); + } + } + + // Save the metadata + mediaSession.setMetadata(metadata.build()); + + // Create playback State + PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder(); + int state = (playerState == STARTED) ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; + + // If we set the playback position correctly, we can get a nice seek bar :) + playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0F); + + // Save the playback state + mediaSession.setPlaybackState(playbackState.build()); + } + public void updateNotification(PlayerState playerState, DownloadFile currentPlaying) { if (Util.isNotificationEnabled(this)) { @@ -604,15 +653,13 @@ public class MediaPlayerService extends Service if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); - } - else { + } else { final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); } Timber.w("--- Updated notification"); - } - else { + } else { startForeground(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); isInForeground = true; Timber.w("--- Created Foreground notification"); @@ -620,84 +667,187 @@ public class MediaPlayerService extends Service } } - @SuppressWarnings("IconColors") + + /** + * This method builds a notification, reusing the Notification Builder if possible + */ private Notification buildForegroundNotification(PlayerState playerState, DownloadFile currentPlaying) { - notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); + // Init + Context context = getApplicationContext(); - notificationBuilder.setAutoCancel(false); - notificationBuilder.setOngoing(true); - notificationBuilder.setOnlyAlertOnce(true); - notificationBuilder.setWhen(System.currentTimeMillis()); - notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - notificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); + // We should use a single notification builder, otherwise the notification may not be updated + if (notificationBuilder == null) { + notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); - RemoteViews contentView = new RemoteViews(this.getPackageName(), R.layout.notification); - Util.linkButtons(this, contentView, false); - RemoteViews bigView = new RemoteViews(this.getPackageName(), R.layout.notification_large); - Util.linkButtons(this, bigView, false); + // Set some values that never change + notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); + notificationBuilder.setAutoCancel(false); + notificationBuilder.setOngoing(true); + notificationBuilder.setOnlyAlertOnce(true); + notificationBuilder.setWhen(0); + notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + notificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); + notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + notificationBuilder.setColor(NotificationCompat.COLOR_DEFAULT); - notificationBuilder.setContent(contentView); - - Intent notificationIntent = new Intent(this, NavigationActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true); - notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)); - - if (playerState == PlayerState.PAUSED || playerState == PlayerState.IDLE) { - contentView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); - bigView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); - } else if (playerState == PlayerState.STARTED) { - contentView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); - bigView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); + // Add content intent (when user taps on notification) + notificationBuilder.setContentIntent(getPendingIntentForContent()); } + // Set song title, artist and cover if possible if (currentPlaying != null) { - final MusicDirectory.Entry song = currentPlaying.getSong(); - final String title = song.getTitle(); - final String text = song.getArtist(); - final String album = song.getAlbum(); - final int rating = song.getUserRating() == null ? 0 : song.getUserRating(); - final int imageSize = Util.getNotificationImageSize(this); - - try { - final Bitmap nowPlayingImage = FileUtil.getAlbumArtBitmap(this, currentPlaying.getSong(), imageSize, true); - if (nowPlayingImage == null) { - contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } else { - contentView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); - bigView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); - } - } catch (Exception x) { - Timber.w(x, "Failed to get notification cover art"); - contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } - - contentView.setTextViewText(R.id.trackname, title); - bigView.setTextViewText(R.id.trackname, title); - contentView.setTextViewText(R.id.artist, text); - bigView.setTextViewText(R.id.artist, text); - contentView.setTextViewText(R.id.album, album); - bigView.setTextViewText(R.id.album, album); - - boolean useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING); - if (!useFiveStarRating) - bigView.setViewVisibility(R.id.notification_rating, View.INVISIBLE); - else { - bigView.setImageViewResource(R.id.notification_five_star_1, rating > 0 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_2, rating > 1 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_3, rating > 2 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_4, rating > 3 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_5, rating > 4 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - } + MusicDirectory.Entry song = currentPlaying.getSong(); + int iconSize = (int) (256 * context.getResources().getDisplayMetrics().density); + Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, iconSize, true); + notificationBuilder.setContentTitle(song.getTitle()); + notificationBuilder.setContentText(song.getArtist()); + notificationBuilder.setLargeIcon(bitmap); } - Notification notification = notificationBuilder.build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - notification.bigContentView = bigView; + // Use the Media Style, to enable native Android support for playback notification + androidx.media.app.NotificationCompat.MediaStyle style = new androidx.media.app.NotificationCompat.MediaStyle(); + style.setMediaSession(mediaSessionToken); + + // Clear old actions + notificationBuilder.clearActions(); + + // Add actions + int[] compactActions = addActions(context, notificationBuilder, playerState); + style.setShowActionsInCompactView(compactActions); + notificationBuilder.setStyle(style); + + return notificationBuilder.build(); + } + + + private int[] addActions(Context context, NotificationCompat.Builder notificationBuilder, PlayerState playerState) { + ArrayList compactActionList = new ArrayList<>(); + int numActions = 0; // we start and 0 and then increment by 1 for each call to generateAction + + + // 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); + + int[] actionArray = new int[compactActionList.size()]; + + for (int i = 0; i < actionArray.length; i++) { + actionArray[i] = compactActionList.get(i); } - return notification; + return actionArray; + //notificationBuilder.setShowActionsInCompactView()) + } + + + private NotificationCompat.Action generateAction(Context context, int requestCode) { + int keycode; + int icon; + String label; + + // If you change the order here, also update the requestCode in updatePlayPauseAction()! + switch (requestCode) { + case 0: + keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; + label = getString(R.string.common_play_previous); + icon = R.drawable.media_backward_normal_dark; + break; + case 1: + // Is handled in generatePlayPauseAction() + return null; + case 2: + keycode = KeyEvent.KEYCODE_MEDIA_NEXT; + label = getString(R.string.common_play_next); + icon = R.drawable.media_forward_normal_dark; + break; + default: + return null; + } + + PendingIntent pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode); + + return new NotificationCompat.Action.Builder(icon, label, pendingIntent).build(); + } + + private NotificationCompat.Action generatePlayPauseAction(Context context, int requestCode, PlayerState playerState) { + + boolean isPlaying = (playerState == STARTED); + PendingIntent pendingIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, requestCode); + + String label; + int icon; + + if (isPlaying) { + label = getString(R.string.common_pause); + icon = R.drawable.media_pause_normal_dark; + } else { + label = getString(R.string.common_play); + icon = R.drawable.media_start_normal_dark; + } + + return new NotificationCompat.Action.Builder(icon, label, pendingIntent).build(); + } + + + private PendingIntent getPendingIntentForContent() { + Intent notificationIntent = new Intent(this, NavigationActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true); + return PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private PendingIntent getPendingIntentForMediaAction(Context context, int keycode, int requestCode) { + Intent intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keycode)); + + return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private void initMediaSessions() { + + mediaSession = new MediaSessionCompat(getApplicationContext(), "UltrasonicService"); + mediaSessionToken = mediaSession.getSessionToken(); + //mediaController = new MediaControllerCompat(getApplicationContext(), mediaSessionToken); + + mediaSession.setCallback(new MediaSessionCompat.Callback() { + @Override + public void onPlay() { + super.onPlay(); + play(); + Timber.w("Media Session Callback: onPlay"); + } + + @Override + public void onPause() { + super.onPause(); + pause(); + Timber.w("Media Session Callback: onPause"); + } + + @Override + public void onStop() { + super.onStop(); + stop(); + Timber.w("Media Session Callback: onStop"); + } + + @Override + public void onSeekTo(long pos) { + super.onSeekTo(pos); + } + + } + ); } } diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index aac3afd0..d4681e06 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -39,8 +39,11 @@ Name OK Pin + Pause + Play Play Last Play Next + Play Previous Play Now Play Shuffled Public From 19580cda8b4f02ed347aa5d01fe99abbc0266b5d Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 17 Apr 2021 18:39:17 +0200 Subject: [PATCH 03/29] We can now remove the deprecated Remote Control API --- .../service/MediaPlayerService.java | 2 - .../ultrasonic/service/LocalMediaPlayer.kt | 146 +----------------- 2 files changed, 1 insertion(+), 147 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index d0ed1449..bc702191 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -300,7 +300,6 @@ public class MediaPlayerService extends Service { nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); stopForeground(true); - localMediaPlayer.clearRemoteControl(); isInForeground = false; stopIfIdle(); } @@ -520,7 +519,6 @@ public class MediaPlayerService extends Service { nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); stopForeground(true); - localMediaPlayer.clearRemoteControl(); isInForeground = false; stopIfIdle(); } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 5dc20678..c21246f3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -7,17 +7,12 @@ package org.moire.ultrasonic.service -import android.app.PendingIntent -import android.content.ComponentName import android.content.Context -import android.content.Context.AUDIO_SERVICE import android.content.Context.POWER_SERVICE import android.content.Intent import android.media.AudioManager -import android.media.MediaMetadataRetriever import android.media.MediaPlayer import android.media.MediaPlayer.OnCompletionListener -import android.media.RemoteControlClient import android.media.audiofx.AudioEffect import android.os.Build import android.os.Handler @@ -35,10 +30,8 @@ import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.fragment.PlayerFragment -import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.StreamProxy import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -84,8 +77,6 @@ class LocalMediaPlayer( private var mediaPlayerHandler: Handler? = null private var cachedPosition = 0 private var proxy: StreamProxy? = null - private var audioManager: AudioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager - private var remoteControlClient: RemoteControlClient? = null private var bufferTask: CancellableTask? = null private var positionCache: PositionCache? = null private var secondaryProgress = -1 @@ -129,7 +120,6 @@ class LocalMediaPlayer( wakeLock.setReferenceCounted(false) Util.registerMediaButtonEventReceiver(context, true) - setUpRemoteControlClient() Timber.i("LocalMediaPlayer created") } @@ -156,8 +146,6 @@ class LocalMediaPlayer( if (nextPlayingTask != null) { nextPlayingTask!!.cancel() } - audioManager.unregisterRemoteControlClient(remoteControlClient) - clearRemoteControl() Util.unregisterMediaButtonEventReceiver(context, true) wakeLock.release() } catch (exception: Throwable) { @@ -173,9 +161,7 @@ class LocalMediaPlayer( if (playerState === PlayerState.STARTED) { audioFocusHandler.requestAudioFocus() } - if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) { - updateRemoteControl() - } + if (onPlayerStateChanged != null) { val mainHandler = Handler(context.mainLooper) val myRunnable = Runnable { @@ -200,7 +186,6 @@ class LocalMediaPlayer( fun setCurrentPlaying(currentPlaying: DownloadFile?) { Timber.v("setCurrentPlaying %s", currentPlaying) this.currentPlaying = currentPlaying - updateRemoteControl() if (onCurrentPlayingChanged != null) { val mainHandler = Handler(context.mainLooper) @@ -296,140 +281,11 @@ class LocalMediaPlayer( } } - /* - * The remote control API is deprecated in API 21 - */ - private fun updateRemoteControl() { - if (!Util.isLockScreenEnabled(context)) { - clearRemoteControl() - return - } - - if (remoteControlClient == null) { - remoteControlClient = createRemoteControlClient() - } else { - // This is probably needed only in API <=17 - // "You must register your RemoteControlDisplay every time when the View which - // displays metadata is shown to the user. This is because 4.2.2 and lower - // versions support only one RemoteControlDisplay, and if system will - // decide to register it's own RCD, your RCD will be - // unregistered automatically. - // https://forum.xda-developers.com/t/guide-implement-your-own-lockscreen-like-music-controls.2401597/ - audioManager.unregisterRemoteControlClient(remoteControlClient) - audioManager.registerRemoteControlClient(remoteControlClient) - } - - Timber.i( - "In updateRemoteControl, playerState: %s [%d]", - playerState, playerPosition - ) - - if (playerState === PlayerState.STARTED) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { - remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING) - } else { - remoteControlClient!!.setPlaybackState( - RemoteControlClient.PLAYSTATE_PLAYING, - playerPosition.toLong(), 1.0f - ) - } - } else { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { - remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED) - } else { - remoteControlClient!!.setPlaybackState( - RemoteControlClient.PLAYSTATE_PAUSED, - playerPosition.toLong(), 1.0f - ) - } - } - - if (currentPlaying != null) { - val currentSong = currentPlaying!!.song - val lockScreenBitmap = FileUtil.getAlbumArtBitmap( - context, currentSong, - Util.getMinDisplayMetric(context), true - ) - val artist = currentSong.artist - val album = currentSong.album - val title = currentSong.title - val currentSongDuration = currentSong.duration - var duration = 0L - if (currentSongDuration != null) duration = (currentSongDuration * 1000).toLong() - remoteControlClient!!.editMetadata(true) - .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) - .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) - .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap) - .apply() - } - } - - fun clearRemoteControl() { - if (remoteControlClient != null) { - remoteControlClient!!.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED) - audioManager.unregisterRemoteControlClient(remoteControlClient) - remoteControlClient = null - } - } - - private fun setUpRemoteControlClient() { - if (!Util.isLockScreenEnabled(context)) return - - if (remoteControlClient == null) { - remoteControlClient = createRemoteControlClient() - } - } - - private fun createRemoteControlClient(): RemoteControlClient { - val componentName = ComponentName( - context.packageName, - MediaButtonIntentReceiver::class.java.name - ) - - val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) - mediaButtonIntent.component = componentName - - val broadcast = PendingIntent.getBroadcast( - context, 0, - mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT - ) - - val remoteControlClient = RemoteControlClient(broadcast) - audioManager.registerRemoteControlClient(remoteControlClient) - - // Flags for the media transport control that this client supports. - var flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS or - RemoteControlClient.FLAG_KEY_MEDIA_NEXT or - RemoteControlClient.FLAG_KEY_MEDIA_PLAY or - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE or - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE or - RemoteControlClient.FLAG_KEY_MEDIA_STOP - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - flags = flags or RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE - remoteControlClient.setOnGetPlaybackPositionListener { - mediaPlayer.currentPosition.toLong() - } - remoteControlClient.setPlaybackPositionUpdateListener { - newPositionMs -> - seekTo(newPositionMs.toInt()) - } - } - - remoteControlClient.setTransportControlFlags(flags) - - return remoteControlClient - } - @Synchronized fun seekTo(position: Int) { try { mediaPlayer.seekTo(position) cachedPosition = position - updateRemoteControl() } catch (x: Exception) { handleError(x) } From 731447fda5687d9205443b8683d770f8390f942d Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 17 Apr 2021 19:13:19 +0200 Subject: [PATCH 04/29] Improve icon sizes --- .../org/moire/ultrasonic/service/MediaPlayerService.java | 8 ++++---- .../src/main/res/drawable/media_backward_medium_dark.xml | 5 +++++ .../src/main/res/drawable/media_forward_medium_dark.xml | 5 +++++ .../src/main/res/drawable/media_pause_large_dark.xml | 5 +++++ .../src/main/res/drawable/media_start_large_dark.xml | 5 +++++ 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml create mode 100644 ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml create mode 100644 ultrasonic/src/main/res/drawable/media_pause_large_dark.xml create mode 100644 ultrasonic/src/main/res/drawable/media_start_large_dark.xml diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index bc702191..460947b6 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -758,7 +758,7 @@ public class MediaPlayerService extends Service case 0: keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; label = getString(R.string.common_play_previous); - icon = R.drawable.media_backward_normal_dark; + icon = R.drawable.media_backward_medium_dark; break; case 1: // Is handled in generatePlayPauseAction() @@ -766,7 +766,7 @@ public class MediaPlayerService extends Service case 2: keycode = KeyEvent.KEYCODE_MEDIA_NEXT; label = getString(R.string.common_play_next); - icon = R.drawable.media_forward_normal_dark; + icon = R.drawable.media_forward_medium_dark; break; default: return null; @@ -787,10 +787,10 @@ public class MediaPlayerService extends Service if (isPlaying) { label = getString(R.string.common_pause); - icon = R.drawable.media_pause_normal_dark; + icon = R.drawable.media_pause_large_dark; } else { label = getString(R.string.common_play); - icon = R.drawable.media_start_normal_dark; + icon = R.drawable.media_start_large_dark; } return new NotificationCompat.Action.Builder(icon, label, pendingIntent).build(); diff --git a/ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml b/ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml new file mode 100644 index 00000000..79c6bfd3 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media_backward_medium_dark.xml @@ -0,0 +1,5 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml b/ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml new file mode 100644 index 00000000..dc96d2dc --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media_forward_medium_dark.xml @@ -0,0 +1,5 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media_pause_large_dark.xml b/ultrasonic/src/main/res/drawable/media_pause_large_dark.xml new file mode 100644 index 00000000..64164940 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media_pause_large_dark.xml @@ -0,0 +1,5 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/media_start_large_dark.xml b/ultrasonic/src/main/res/drawable/media_start_large_dark.xml new file mode 100644 index 00000000..0ebbb01e --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media_start_large_dark.xml @@ -0,0 +1,5 @@ + + + From 116307df5644381998aa92d80a1111444d42037f Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 17 Apr 2021 19:56:48 +0200 Subject: [PATCH 05/29] Fix a regression were we accidentally reset our current MediaPlayer Fixes #418 --- .../kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index c21246f3..d18c34bd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -438,7 +438,8 @@ class LocalMediaPlayer( try { val file = downloadFile.completeOrPartialFile - if (nextMediaPlayer != null) { + // Release the media player if it is not our active player + if (nextMediaPlayer != null && nextMediaPlayer != mediaPlayer) { nextMediaPlayer!!.setOnCompletionListener(null) nextMediaPlayer!!.release() nextMediaPlayer = null From 1ee36322db8bb9703dced3154d9bdb9cfead2063 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 17 Apr 2021 21:41:12 +0200 Subject: [PATCH 06/29] Remove unused views and util function --- .../java/org/moire/ultrasonic/util/Util.java | 74 +------ .../src/main/res/layout/notification.xml | 121 ----------- .../main/res/layout/notification_large.xml | 189 ------------------ 3 files changed, 1 insertion(+), 383 deletions(-) delete mode 100644 ultrasonic/src/main/res/layout/notification.xml delete mode 100644 ultrasonic/src/main/res/layout/notification_large.xml diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index bd4bb8bc..cf4a13f3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -21,7 +21,6 @@ package org.moire.ultrasonic.util; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; -import android.app.PendingIntent; import android.content.*; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -35,7 +34,6 @@ import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.Uri; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Environment; @@ -44,17 +42,14 @@ import android.util.DisplayMetrics; import timber.log.Timber; import android.util.TypedValue; import android.view.Gravity; -import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; -import android.widget.RemoteViews; import android.widget.Toast; import androidx.annotation.ColorInt; import androidx.preference.PreferenceManager; import org.moire.ultrasonic.R; -import org.moire.ultrasonic.activity.NavigationActivity; import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.*; import org.moire.ultrasonic.domain.MusicDirectory.Entry; @@ -852,6 +847,7 @@ public class Util return; } + // FIXME: This is probably a bug. if (currentSong != currentSong) { Util.currentSong = currentSong; @@ -1004,74 +1000,6 @@ public class Util return inSampleSize; } - public 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_EXTRA_NAME_SHOW_PLAYER, true); - - intent.setAction("android.intent.action.MAIN"); - intent.addCategory("android.intent.category.LAUNCHER"); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, 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.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - pendingIntent = PendingIntent.getBroadcast(context, 1, intent, 0); - views.setOnClickPendingIntent(R.id.control_play, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); - pendingIntent = PendingIntent.getBroadcast(context, 2, intent, 0); - views.setOnClickPendingIntent(R.id.control_next, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); - pendingIntent = PendingIntent.getBroadcast(context, 3, intent, 0); - views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); - pendingIntent = PendingIntent.getBroadcast(context, 4, intent, 0); - views.setOnClickPendingIntent(R.id.control_stop, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_1)); - pendingIntent = PendingIntent.getBroadcast(context, 5, intent, 0); - views.setOnClickPendingIntent(R.id.notification_five_star_1, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_2)); - pendingIntent = PendingIntent.getBroadcast(context, 6, intent, 0); - views.setOnClickPendingIntent(R.id.notification_five_star_2, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_3)); - pendingIntent = PendingIntent.getBroadcast(context, 7, intent, 0); - views.setOnClickPendingIntent(R.id.notification_five_star_3, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_4)); - pendingIntent = PendingIntent.getBroadcast(context, 8, intent, 0); - views.setOnClickPendingIntent(R.id.notification_five_star_4, pendingIntent); - - intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5)); - pendingIntent = PendingIntent.getBroadcast(context, 9, intent, 0); - views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent); - } - // TODO: Shouldn't this be used when making requests? public static int getNetworkTimeout(Context context) { diff --git a/ultrasonic/src/main/res/layout/notification.xml b/ultrasonic/src/main/res/layout/notification.xml deleted file mode 100644 index f101cb66..00000000 --- a/ultrasonic/src/main/res/layout/notification.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/notification_large.xml b/ultrasonic/src/main/res/layout/notification_large.xml deleted file mode 100644 index 912eef61..00000000 --- a/ultrasonic/src/main/res/layout/notification_large.xml +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From d3b94f3d4cc441d561c00e2b7a56fdab6664fb83 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 21 Apr 2021 16:41:08 +0200 Subject: [PATCH 07/29] Add Star/Unstar and close Action Also: Add album to subtitle :) --- .../service/MediaPlayerControllerImpl.java | 12 +++ .../service/MediaPlayerLifecycleSupport.java | 3 + .../service/MediaPlayerService.java | 75 ++++++++++++++----- .../res/drawable/ic_baseline_close_24.xml | 9 +++ 4 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 ultrasonic/src/main/res/drawable/ic_baseline_close_24.xml diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java index 516406d7..662b0da1 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java @@ -589,6 +589,18 @@ public class MediaPlayerControllerImpl implements MediaPlayerController if (mediaPlayerService != null) mediaPlayerService.updateNotification(localMediaPlayer.playerState, localMediaPlayer.currentPlaying); } + public void toggleSongStarred() { + if (localMediaPlayer.currentPlaying == null) + return; + + final Entry song = localMediaPlayer.currentPlaying.getSong(); + + // Trigger an update + localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying); + + song.setStarred(!song.getStarred()); + } + public void setSongRating(final int rating) { if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING)) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java index 2e4d6984..3c4c0750 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java @@ -254,6 +254,9 @@ public class MediaPlayerLifecycleSupport case KeyEvent.KEYCODE_5: mediaPlayerController.setSongRating(5); break; + case KeyEvent.KEYCODE_STAR: + mediaPlayerController.toggleSongStarred(); + break; default: break; } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 460947b6..a0046956 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -672,6 +672,7 @@ public class MediaPlayerService extends Service private Notification buildForegroundNotification(PlayerState playerState, DownloadFile currentPlaying) { // Init Context context = getApplicationContext(); + MusicDirectory.Entry song = (currentPlaying != null) ? currentPlaying.getSong() : null; // We should use a single notification builder, otherwise the notification may not be updated if (notificationBuilder == null) { @@ -682,26 +683,15 @@ public class MediaPlayerService extends Service notificationBuilder.setAutoCancel(false); notificationBuilder.setOngoing(true); notificationBuilder.setOnlyAlertOnce(true); - notificationBuilder.setWhen(0); + notificationBuilder.setWhen(System.currentTimeMillis()); + notificationBuilder.setShowWhen(false); notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); notificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - notificationBuilder.setColor(NotificationCompat.COLOR_DEFAULT); // Add content intent (when user taps on notification) notificationBuilder.setContentIntent(getPendingIntentForContent()); } - // Set song title, artist and cover if possible - if (currentPlaying != null) { - MusicDirectory.Entry song = currentPlaying.getSong(); - int iconSize = (int) (256 * context.getResources().getDisplayMetrics().density); - Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, iconSize, true); - notificationBuilder.setContentTitle(song.getTitle()); - notificationBuilder.setContentText(song.getArtist()); - notificationBuilder.setLargeIcon(bitmap); - } - // Use the Media Style, to enable native Android support for playback notification androidx.media.app.NotificationCompat.MediaStyle style = new androidx.media.app.NotificationCompat.MediaStyle(); style.setMediaSession(mediaSessionToken); @@ -710,19 +700,37 @@ public class MediaPlayerService extends Service notificationBuilder.clearActions(); // Add actions - int[] compactActions = addActions(context, notificationBuilder, playerState); + int[] compactActions = addActions(context, notificationBuilder, playerState, song); + + // Configure shortcut actions style.setShowActionsInCompactView(compactActions); notificationBuilder.setStyle(style); + // Set song title, artist and cover if possible + if (song != null) { + int iconSize = (int) (256 * context.getResources().getDisplayMetrics().density); + Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, iconSize, true); + notificationBuilder.setContentTitle(song.getTitle()); + notificationBuilder.setContentText(song.getArtist()); + notificationBuilder.setLargeIcon(bitmap); + notificationBuilder.setSubText(song.getAlbum()); + } + return notificationBuilder.build(); } - private int[] addActions(Context context, NotificationCompat.Builder notificationBuilder, PlayerState playerState) { + private int[] addActions(Context context, NotificationCompat.Builder notificationBuilder, PlayerState playerState, MusicDirectory.Entry song) { ArrayList compactActionList = new ArrayList<>(); int numActions = 0; // we start and 0 and then increment by 1 for each call to generateAction + // Star + if (song != null) { + notificationBuilder.addAction(generateStarUnstarAction(context, numActions, song.getStarred())); + } + numActions++; + // Next notificationBuilder.addAction(generateAction(context, numActions)); compactActionList.add(numActions); @@ -736,6 +744,10 @@ public class MediaPlayerService extends Service // Previous notificationBuilder.addAction(generateAction(context, numActions)); compactActionList.add(numActions); + numActions++; + + // Close + notificationBuilder.addAction(generateAction(context, numActions)); int[] actionArray = new int[compactActionList.size()]; @@ -755,19 +767,24 @@ public class MediaPlayerService extends Service // If you change the order here, also update the requestCode in updatePlayPauseAction()! switch (requestCode) { - case 0: + case 1: keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; label = getString(R.string.common_play_previous); icon = R.drawable.media_backward_medium_dark; break; - case 1: + case 2: // Is handled in generatePlayPauseAction() return null; - case 2: + case 3: keycode = KeyEvent.KEYCODE_MEDIA_NEXT; label = getString(R.string.common_play_next); icon = R.drawable.media_forward_medium_dark; break; + case 4: + keycode = KeyEvent.KEYCODE_MEDIA_STOP; + label = getString(R.string.buttons_stop); + icon = R.drawable.ic_baseline_close_24; + break; default: return null; } @@ -797,6 +814,28 @@ public class MediaPlayerService extends Service } + private NotificationCompat.Action generateStarUnstarAction(Context context, int requestCode, Boolean isStarred) { + + int keyCode; + String label; + int icon; + keyCode = 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; + } + + PendingIntent pendingIntent = getPendingIntentForMediaAction(context, keyCode, requestCode); + + return new NotificationCompat.Action.Builder(icon, label, pendingIntent).build(); + } + + private PendingIntent getPendingIntentForContent() { Intent notificationIntent = new Intent(this, NavigationActivity.class) .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_close_24.xml b/ultrasonic/src/main/res/drawable/ic_baseline_close_24.xml new file mode 100644 index 00000000..361e6899 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_baseline_close_24.xml @@ -0,0 +1,9 @@ + + + From e4bf431cfef1ce9b563e739a61d7520fefb594c2 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 21 Apr 2021 17:14:05 +0200 Subject: [PATCH 08/29] Add deleteIntent --- .../java/org/moire/ultrasonic/service/MediaPlayerService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index a0046956..cb180138 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -673,6 +673,7 @@ public class MediaPlayerService extends Service // Init Context context = getApplicationContext(); MusicDirectory.Entry song = (currentPlaying != null) ? currentPlaying.getSong() : null; + PendingIntent stopIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_STOP, 100); // We should use a single notification builder, otherwise the notification may not be updated if (notificationBuilder == null) { @@ -690,6 +691,9 @@ public class MediaPlayerService extends Service // 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 From 88f6bdb3a91c1b5dad9540bda046d76d6ef24d75 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 21 Apr 2021 17:40:51 +0200 Subject: [PATCH 09/29] setShowBadge = false --- .../service/MediaPlayerService.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index cb180138..9613d403 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -174,14 +174,7 @@ public class MediaPlayerService extends Service }; // Create Notification Channel - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - //The suggested importance of a startForeground service notification is IMPORTANCE_LOW - NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); - channel.setLightColor(android.R.color.holo_blue_dark); - channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(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(IDLE, null); @@ -644,6 +637,18 @@ public class MediaPlayerService extends Service mediaSession.setPlaybackState(playbackState.build()); } + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //The suggested importance of a startForeground service notification is IMPORTANCE_LOW + NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); + channel.setLightColor(android.R.color.holo_blue_dark); + channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); + channel.setShowBadge(false); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(channel); + } + } + public void updateNotification(PlayerState playerState, DownloadFile currentPlaying) { if (Util.isNotificationEnabled(this)) { From 8bfc5d04ef9d8212f8fc410ed3a180599c630f88 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 21 Apr 2021 19:09:57 +0200 Subject: [PATCH 10/29] Pass keyevents from the session to MediaPlayerLifecycleSupport --- .../service/MediaPlayerLifecycleSupport.java | 2 +- .../moire/ultrasonic/service/MediaPlayerService.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java index 3c4c0750..2cf3d392 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java @@ -183,7 +183,7 @@ public class MediaPlayerLifecycleSupport context.registerReceiver(headsetEventReceiver, headsetIntentFilter); } - private void handleKeyEvent(KeyEvent event) + public void handleKeyEvent(KeyEvent event) { if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 9613d403..41e42045 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -71,6 +71,7 @@ public class MediaPlayerService extends Service private final Lazy downloaderLazy = inject(Downloader.class); private final Lazy localMediaPlayerLazy = inject(LocalMediaPlayer.class); private final Lazy nowPlayingEventDistributor = inject(NowPlayingEventDistributor.class); + private final Lazy mediaPlayerLifecycleSupport = inject(MediaPlayerLifecycleSupport.class); private LocalMediaPlayer localMediaPlayer; private Downloader downloader; @@ -893,6 +894,17 @@ public class MediaPlayerService extends Service super.onSeekTo(pos); } + @Override + public boolean onMediaButtonEvent(Intent mediaButtonEvent) { + // This probably won't be necessary once we implement more + // of the modern media APIs, like the MediaController etc. + KeyEvent event = (KeyEvent) mediaButtonEvent.getExtras().get("android.intent.extra.KEY_EVENT"); + MediaPlayerLifecycleSupport lifecycleSupport = mediaPlayerLifecycleSupport.getValue(); + lifecycleSupport.handleKeyEvent(event); + + return true; + } + } ); } From 0550116b352fe33ee52936357436016bd0c940d1 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 09:18:15 +0200 Subject: [PATCH 11/29] Rename .java to .kt --- .../org/moire/ultrasonic/service/AudioFocusHandler.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ultrasonic/src/main/{java/org/moire/ultrasonic/service/AudioFocusHandler.java => kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt} (100%) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt similarity index 100% rename from ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt From 37632dd031bc24467f98564097eaf2543b911103 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 09:18:15 +0200 Subject: [PATCH 12/29] Convert AudioFocusHandler to Kotlin --- .../ultrasonic/service/AudioFocusHandler.kt | 128 +++++++----------- 1 file changed, 52 insertions(+), 76 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt index b625067b..c3641729 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt @@ -1,88 +1,64 @@ -package org.moire.ultrasonic.service; +package org.moire.ultrasonic.service -import android.content.Context; -import android.content.SharedPreferences; -import android.media.AudioManager; -import timber.log.Timber; - -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.Util; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; - -public class AudioFocusHandler -{ - private static boolean hasFocus; - private static boolean pauseFocus; - private static boolean lowerFocus; +import android.content.Context +import android.media.AudioManager +import android.media.AudioManager.OnAudioFocusChangeListener +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Util +import timber.log.Timber +class AudioFocusHandler(private val context: Context) { // TODO: This is a circular reference, try to remove it - private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private Context context; + private val mediaPlayerControllerLazy = inject(MediaPlayerController::class.java) - public AudioFocusHandler(Context context) - { - this.context = context; - } + fun requestAudioFocus() { + if (!hasFocus) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + hasFocus = true - public void requestAudioFocus() - { - if (!hasFocus) - { - final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - hasFocus = true; - audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() - { - @Override - public void onAudioFocusChange(int focusChange) - { - MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); - if ((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !mediaPlayerController.isJukeboxEnabled()) - { - Timber.v("Lost Audio Focus"); - if (mediaPlayerController.getPlayerState() == PlayerState.STARTED) - { - SharedPreferences preferences = Util.getPreferences(context); - int lossPref = Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")); - if (lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) - { - lowerFocus = true; - mediaPlayerController.setVolume(0.1f); - } - else if (lossPref == 0 || (lossPref == 1)) - { - pauseFocus = true; - mediaPlayerController.pause(); + + audioManager.requestAudioFocus(object : OnAudioFocusChangeListener { + override fun onAudioFocusChange(focusChange: Int) { + val mediaPlayerController = mediaPlayerControllerLazy.value + if ((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !mediaPlayerController.isJukeboxEnabled) { + Timber.v("Lost Audio Focus") + if (mediaPlayerController.playerState === PlayerState.STARTED) { + val preferences = Util.getPreferences(context) + val lossPref = preferences.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")!!.toInt() + if (lossPref == 2 || lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + lowerFocus = true + mediaPlayerController.setVolume(0.1f) + } else if (lossPref == 0 || lossPref == 1) { + pauseFocus = true + mediaPlayerController.pause() } } - } - else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) - { - Timber.v("Regained Audio Focus"); - if (pauseFocus) - { - pauseFocus = false; - mediaPlayerController.start(); + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + Timber.v("Regained Audio Focus") + if (pauseFocus) { + pauseFocus = false + mediaPlayerController.start() + } else if (lowerFocus) { + lowerFocus = false + mediaPlayerController.setVolume(1.0f) } - else if (lowerFocus) - { - lowerFocus = false; - mediaPlayerController.setVolume(1.0f); - } - } - else if (focusChange == AudioManager.AUDIOFOCUS_LOSS && !mediaPlayerController.isJukeboxEnabled()) - { - hasFocus = false; - mediaPlayerController.pause(); - audioManager.abandonAudioFocus(this); - Timber.v("Abandoned Audio Focus"); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS && !mediaPlayerController.isJukeboxEnabled) { + hasFocus = false + mediaPlayerController.pause() + audioManager.abandonAudioFocus(this) + Timber.v("Abandoned Audio Focus") } } - }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); - Timber.v("Got Audio Focus"); + }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) + Timber.v("Got Audio Focus") } } -} + + companion object { + private var hasFocus = false + private var pauseFocus = false + private var lowerFocus = false + } +} \ No newline at end of file From 8640d39438747030f0cbafa3d5635d470ce1b91c Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 09:31:27 +0200 Subject: [PATCH 13/29] We need androidx.media at v1.3.0. Also do the minor gradle update --- dependencies.gradle | 6 ++++-- ultrasonic/build.gradle | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 782e63eb..074b77a4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -5,13 +5,14 @@ ext.versions = [ gradle : '6.5', navigation : "2.3.2", - gradlePlugin : "4.1.2", + gradlePlugin : "4.1.3", androidxcore : "1.5.0-rc01", ktlint : "0.37.1", ktlintGradle : "9.2.1", detekt : "1.0.0.RC6-4", jacoco : "0.8.5", preferences : "1.1.1", + media : "1.3.0", androidSupport : "28.0.0", androidLegacySupport : "1.0.0", @@ -53,7 +54,7 @@ ext.gradlePlugins = [ ] ext.androidSupport = [ - core : "androidx.core:core-ktx:$versions.androidxcore", + core : "androidx.core:core-ktx:$versions.androidxcore", support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport", design : "com.google.android.material:material:$versions.androidSupportDesign", annotations : "com.android.support:support-annotations:$versions.androidSupport", @@ -69,6 +70,7 @@ ext.androidSupport = [ navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation", navigationFeature : "androidx.navigation:navigation-dynamic-features-fragment:$versions.navigation", preferences : "androidx.preference:preference:$versions.preferences", + media : "androidx.media:media:$versions.media", ] ext.other = [ diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index ec595e98..a7714400 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -80,6 +80,7 @@ dependencies { implementation androidSupport.viewModelKtx implementation androidSupport.constraintLayout implementation androidSupport.preferences + implementation androidSupport.media implementation androidSupport.navigationFragment implementation androidSupport.navigationUi From 5f8e3ce8511c1aa1883098f2610abaedc0e6901f Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 10:25:17 +0200 Subject: [PATCH 14/29] Modernize AudioFocusHandler --- .../ultrasonic/service/AudioFocusHandler.kt | 127 +++++++++++++----- 1 file changed, 94 insertions(+), 33 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt index c3641729..4d5511e5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt @@ -1,8 +1,14 @@ package org.moire.ultrasonic.service import android.content.Context +import android.media.AudioAttributes import android.media.AudioManager import android.media.AudioManager.OnAudioFocusChangeListener +import android.os.Build +import androidx.annotation.RequiresApi +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.Constants @@ -11,54 +17,109 @@ 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 preferences by lazy { + Util.getPreferences(context) + } + + private val lossPref: Int + get() = preferences.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")!!.toInt() + + 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) { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager hasFocus = true + AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) + } + } + private val listener = OnAudioFocusChangeListener { focusChange -> - audioManager.requestAudioFocus(object : OnAudioFocusChangeListener { - override fun onAudioFocusChange(focusChange: Int) { - val mediaPlayerController = mediaPlayerControllerLazy.value - if ((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !mediaPlayerController.isJukeboxEnabled) { - Timber.v("Lost Audio Focus") - if (mediaPlayerController.playerState === PlayerState.STARTED) { - val preferences = Util.getPreferences(context) - val lossPref = preferences.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")!!.toInt() - if (lossPref == 2 || lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - lowerFocus = true - mediaPlayerController.setVolume(0.1f) - } else if (lossPref == 0 || lossPref == 1) { - pauseFocus = true - mediaPlayerController.pause() - } + 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() } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - Timber.v("Regained Audio Focus") - if (pauseFocus) { - pauseFocus = false - mediaPlayerController.start() - } else if (lowerFocus) { - lowerFocus = false - mediaPlayerController.setVolume(1.0f) - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS && !mediaPlayerController.isJukeboxEnabled) { - hasFocus = false - mediaPlayerController.pause() - audioManager.abandonAudioFocus(this) - Timber.v("Abandoned Audio Focus") } } - }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) - Timber.v("Got Audio Focus") + } + 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 + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + fun getAudioAttributes(): AudioAttributes { + return AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setLegacyStreamType(AudioManager.STREAM_MUSIC) + .build() + } } -} \ No newline at end of file +} From e63f24242b0a33c17dcda470fc14f57fa99a4399 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 10:27:55 +0200 Subject: [PATCH 15/29] Change detekt config: Allow TODO comments, but continue to block FIXME comments Our code still has so many TODOs that sometimes they can't be fixed immediately because the require a much larger refactor, and are also so specific that they don't warrant an own bug. --- detekt-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detekt-config.yml b/detekt-config.yml index bc94cdbe..b39186c0 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -71,7 +71,7 @@ style: active: true ForbiddenComment: active: true - values: 'TODO:,FIXME:,STOPSHIP:' + values: 'FIXME:,STOPSHIP:' WildcardImport: active: true MaxLineLength: From f27212f26342205cb83f2474ef1b8ac3ad0fe5fc Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 10:59:05 +0200 Subject: [PATCH 16/29] Set AudioAttributes in LocalMediaPlayer --- .../ultrasonic/service/LocalMediaPlayer.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index d18c34bd..2c420d8f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -360,7 +360,7 @@ class LocalMediaPlayer( secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works setPlayerState(PlayerState.IDLE) - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC) + setAudioAttributes(mediaPlayer) var dataSource = file.path if (partial) { @@ -433,6 +433,15 @@ class LocalMediaPlayer( } } + private fun setAudioAttributes(player: MediaPlayer) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + player.setAudioAttributes(AudioFocusHandler.getAudioAttributes()) + } else { + @Suppress("DEPRECATION") + player.setAudioStreamType(AudioManager.STREAM_MUSIC) + } + } + @Synchronized private fun setupNext(downloadFile: DownloadFile) { try { @@ -446,11 +455,17 @@ class LocalMediaPlayer( } nextMediaPlayer = MediaPlayer() nextMediaPlayer!!.setWakeMode(context, PowerManager.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 (e: Throwable) { - nextMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC) } + nextMediaPlayer!!.setDataSource(file.path) setNextPlayerState(PlayerState.PREPARING) nextMediaPlayer!!.setOnPreparedListener { From 2adb9ffc7e32c204350f9cbd01ba32aa184cd6ee Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 11:00:20 +0200 Subject: [PATCH 17/29] Remove two warnings --- .../kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 2c420d8f..30c0c2d1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -87,7 +87,7 @@ class LocalMediaPlayer( Thread { Thread.currentThread().name = "MediaPlayerThread" Looper.prepare() - mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) + mediaPlayer.setWakeMode(context, PARTIAL_WAKE_LOCK) mediaPlayer.setOnErrorListener { _, what, more -> handleError( Exception( @@ -454,7 +454,7 @@ class LocalMediaPlayer( nextMediaPlayer = null } nextMediaPlayer = MediaPlayer() - nextMediaPlayer!!.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) + nextMediaPlayer!!.setWakeMode(context, PARTIAL_WAKE_LOCK) setAudioAttributes(nextMediaPlayer!!) From 205f477b43a9d548d772977ddee024e8c6231dc3 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 11:47:15 +0200 Subject: [PATCH 18/29] Refactor all event listeners to lambdas --- .../moire/ultrasonic/service/BiConsumer.java | 11 - .../moire/ultrasonic/service/Consumer.java | 2 + .../service/MediaPlayerService.java | 268 ++++++++---------- .../ultrasonic/service/LocalMediaPlayer.kt | 20 +- 4 files changed, 132 insertions(+), 169 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java deleted file mode 100644 index 8909762e..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.moire.ultrasonic.service; - -/** - * Abstract class for consumers with two parameters - * @param The type of the first object to consume - * @param The type of the second object to consume - */ -public abstract class BiConsumer -{ - public abstract void accept(T t, U u); -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java index d2b09de7..6b8ca564 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java @@ -1,9 +1,11 @@ package org.moire.ultrasonic.service; /** + * Deprecated: Should be replaced with lambdas * Abstract class for consumers with one parameter * @param The type of the object to consume */ +@Deprecated public abstract class Consumer { public abstract void accept(T t); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 41e42045..1e26ced6 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -160,20 +160,17 @@ public class MediaPlayerService extends Service setupOnPlayerStateChangedHandler(); setupOnSongCompletedHandler(); - localMediaPlayer.onPrepared = new Runnable() { - @Override - public void run() { - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, - downloader.getCurrentPlayingIndex(), getPlayerPosition()); - } - }; - localMediaPlayer.onNextSongRequested = new Runnable() { - @Override - public void run() { - setNextPlaying(); - } + localMediaPlayer.onPrepared = () -> { + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.getCurrentPlayingIndex(), + getPlayerPosition() + ); + return null; }; + localMediaPlayer.onNextSongRequested = this::setNextPlaying; + // Create Notification Channel createNotificationChannel(); @@ -259,45 +256,35 @@ public class MediaPlayerService extends Service } } - public void setupOnCurrentPlayingChangedHandler() - { - localMediaPlayer.onCurrentPlayingChanged = new Consumer() { - @Override - public void accept(DownloadFile currentPlaying) { - if (currentPlaying != null) - { - Util.broadcastNewTrackInfo(MediaPlayerService.this, currentPlaying.getSong()); - Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), currentPlaying, - downloader.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); - } - else - { - Util.broadcastNewTrackInfo(MediaPlayerService.this, null); - Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), null, - downloader.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); - } + public void setupOnCurrentPlayingChangedHandler() { + localMediaPlayer.onCurrentPlayingChanged = (DownloadFile currentPlaying) -> { - // Update widget - PlayerState playerState = localMediaPlayer.playerState; - MusicDirectory.Entry song = currentPlaying == null? null : currentPlaying.getSong(); - UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, true); - UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - - if (currentPlaying != null) - { - updateNotification(localMediaPlayer.playerState, currentPlaying); - nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent(); - } - else - { - nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); - stopForeground(true); - isInForeground = false; - stopIfIdle(); - } + if (currentPlaying != null) { + Util.broadcastNewTrackInfo(MediaPlayerService.this, currentPlaying.getSong()); + Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), currentPlaying, + downloader.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); + } else { + Util.broadcastNewTrackInfo(MediaPlayerService.this, null); + Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), null, + downloader.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); } + + // Update widget + PlayerState playerState = localMediaPlayer.playerState; + MusicDirectory.Entry song = currentPlaying == null ? null : currentPlaying.getSong(); + UpdateWidget(playerState, song); + + if (currentPlaying != null) { + updateNotification(localMediaPlayer.playerState, currentPlaying); + nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent(); + } else { + nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); + stopForeground(true); + isInForeground = false; + stopIfIdle(); + } + + return null; }; } @@ -472,118 +459,101 @@ public class MediaPlayerService extends Service localMediaPlayer.setPlayerState(STARTED); } - public void setupOnPlayerStateChangedHandler() - { - localMediaPlayer.onPlayerStateChanged = new BiConsumer() { - @Override - public void accept(PlayerState playerState, DownloadFile currentPlaying) { - // Notify MediaSession - updateMediaSession(currentPlaying, playerState); + private void UpdateWidget(PlayerState playerState, MusicDirectory.Entry song) { + UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, true); + UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + } - if (playerState == PAUSED) - { - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); - } + public void setupOnPlayerStateChangedHandler() { + localMediaPlayer.onPlayerStateChanged = (PlayerState playerState, DownloadFile currentPlaying) -> { + // Notify MediaSession + updateMediaSession(currentPlaying, playerState); - boolean showWhenPaused = (playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(MediaPlayerService.this)); - boolean show = playerState == PlayerState.STARTED || showWhenPaused; - MusicDirectory.Entry song = currentPlaying == null? null : currentPlaying.getSong(); - - Util.broadcastPlaybackStatusChange(MediaPlayerService.this, playerState); - Util.broadcastA2dpPlayStatusChange(MediaPlayerService.this, playerState, song, - downloader.downloadList.size() + downloader.backgroundDownloadList.size(), - downloader.downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); - - // Update widget - UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, true); - UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - - 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); - nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent(); - } - } - else - { - nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); - stopForeground(true); - isInForeground = false; - stopIfIdle(); - } - - if (playerState == STARTED) - { - scrobbler.scrobble(MediaPlayerService.this, currentPlaying, false); - } - else if (playerState == COMPLETED) - { - scrobbler.scrobble(MediaPlayerService.this, currentPlaying, true); - } + if (playerState == PAUSED) { + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); } + + boolean showWhenPaused = (playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(MediaPlayerService.this)); + boolean show = playerState == PlayerState.STARTED || showWhenPaused; + MusicDirectory.Entry song = currentPlaying == null ? null : currentPlaying.getSong(); + + Util.broadcastPlaybackStatusChange(MediaPlayerService.this, playerState); + Util.broadcastA2dpPlayStatusChange(MediaPlayerService.this, playerState, song, + downloader.downloadList.size() + downloader.backgroundDownloadList.size(), + downloader.downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); + + // 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); + nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent(); + } + } else { + nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); + stopForeground(true); + isInForeground = false; + stopIfIdle(); + } + + if (playerState == STARTED) { + scrobbler.scrobble(MediaPlayerService.this, currentPlaying, false); + } else if (playerState == COMPLETED) { + scrobbler.scrobble(MediaPlayerService.this, currentPlaying, true); + } + + return null; }; } - private void setupOnSongCompletedHandler() - { - localMediaPlayer.onSongCompleted = new Consumer() { - @Override - public void accept(DownloadFile currentPlaying) { - int index = downloader.getCurrentPlayingIndex(); + private void setupOnSongCompletedHandler() { + localMediaPlayer.onSongCompleted = (DownloadFile currentPlaying) -> { + int index = downloader.getCurrentPlayingIndex(); - if (currentPlaying != null) - { - final MusicDirectory.Entry song = currentPlaying.getSong(); + if (currentPlaying != null) { + final MusicDirectory.Entry song = currentPlaying.getSong(); - if (song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(MediaPlayerService.this)) - { - MusicService musicService = MusicServiceFactory.getMusicService(MediaPlayerService.this); - try - { - musicService.deleteBookmark(song.getId(), MediaPlayerService.this); - } - catch (Exception ignored) - { + if (song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(MediaPlayerService.this)) { + MusicService musicService = MusicServiceFactory.getMusicService(MediaPlayerService.this); + try { + musicService.deleteBookmark(song.getId(), MediaPlayerService.this); + } catch (Exception ignored) { - } - } - } - - if (index != -1) - { - switch (getRepeatMode()) - { - case OFF: - if (index + 1 < 0 || index + 1 >= downloader.downloadList.size()) - { - if (Util.getShouldClearPlaylist(MediaPlayerService.this)) - { - clear(true); - jukeboxMediaPlayer.getValue().updatePlaylist(); - } - - resetPlayback(); - break; - } - - play(index + 1); - break; - case ALL: - play((index + 1) % downloader.downloadList.size()); - break; - case SINGLE: - play(index); - break; - default: - break; } } } + + if (index != -1) { + switch (getRepeatMode()) { + case OFF: + if (index + 1 < 0 || index + 1 >= downloader.downloadList.size()) { + if (Util.getShouldClearPlaylist(MediaPlayerService.this)) { + clear(true); + jukeboxMediaPlayer.getValue().updatePlaylist(); + } + + resetPlayback(); + break; + } + + play(index + 1); + break; + case ALL: + play((index + 1) % downloader.downloadList.size()); + break; + case SINGLE: + play(index); + break; + default: + break; + } + } + + return null; }; } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 30c0c2d1..612383a1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -45,16 +45,16 @@ class LocalMediaPlayer( ) { @JvmField - var onCurrentPlayingChanged: Consumer? = null + var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null @JvmField - var onSongCompleted: Consumer? = null + var onSongCompleted: ((DownloadFile?) -> Unit?)? = null @JvmField - var onPlayerStateChanged: BiConsumer? = null + var onPlayerStateChanged: ((PlayerState, DownloadFile?) -> Unit?)? = null @JvmField - var onPrepared: Runnable? = null + var onPrepared: (() -> Any?)? = null @JvmField var onNextSongRequested: Runnable? = null @@ -164,8 +164,9 @@ class LocalMediaPlayer( if (onPlayerStateChanged != null) { val mainHandler = Handler(context.mainLooper) + val myRunnable = Runnable { - onPlayerStateChanged!!.accept(playerState, currentPlaying) + onPlayerStateChanged!!(playerState, currentPlaying) } mainHandler.post(myRunnable) } @@ -189,7 +190,7 @@ class LocalMediaPlayer( if (onCurrentPlayingChanged != null) { val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onCurrentPlayingChanged!!.accept(currentPlaying) } + val myRunnable = Runnable { onCurrentPlayingChanged!!(currentPlaying) } mainHandler.post(myRunnable) } } @@ -424,7 +425,9 @@ class LocalMediaPlayer( } } - postRunnable(onPrepared) + postRunnable { + onPrepared + } } attachHandlersToPlayer(mediaPlayer, downloadFile, partial) mediaPlayer.prepareAsync() @@ -458,7 +461,6 @@ class LocalMediaPlayer( setAudioAttributes(nextMediaPlayer!!) - // This has nothing to do with the MediaSession, it is used to associate // the equalizer or visualizer with the player try { @@ -536,7 +538,7 @@ class LocalMediaPlayer( } else { if (onSongCompleted != null) { val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onSongCompleted!!.accept(currentPlaying) } + val myRunnable = Runnable { onSongCompleted!!(currentPlaying) } mainHandler.post(myRunnable) } } From e6135dc21af7d8717e32a033282a22ed2bf40d35 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 11:47:35 +0200 Subject: [PATCH 19/29] Set Session to active --- .../java/org/moire/ultrasonic/service/MediaPlayerService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 1e26ced6..048f26f3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -584,6 +584,7 @@ public class MediaPlayerService extends Service Util.getMinDisplayMetric(context), true ); + metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L); metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtist()); metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtist()); metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbum()); @@ -604,6 +605,9 @@ public class MediaPlayerService extends Service // If we set the playback position correctly, we can get a nice seek bar :) playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0F); + // Set Active state + mediaSession.setActive(playerState == STARTED); + // Save the playback state mediaSession.setPlaybackState(playbackState.build()); } From df54d3450b03f678863fdbf4cf3982d39e6779e2 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 12:15:08 +0200 Subject: [PATCH 20/29] Detekt: Remove "warningThreshold and failThreshold properties are deprecated. Please use the new 'maxIssues' config property." --- detekt-config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/detekt-config.yml b/detekt-config.yml index b39186c0..4674fd6a 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -2,8 +2,7 @@ autoCorrect: true failFast: false build: - warningThreshold: 0 - failThreshold: 0 + maxIssues: 0 weights: complexity: 2 formatting: 1 From 9472aa4c22aa1f45e09bf8c47cb6413e86f0e2c4 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 19:25:50 +0200 Subject: [PATCH 21/29] Rename .java to .kt --- .../service/{MediaPlayerService.java => MediaPlayerService.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ultrasonic/src/main/java/org/moire/ultrasonic/service/{MediaPlayerService.java => MediaPlayerService.kt} (100%) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt similarity index 100% rename from ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java rename to ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt From 35e89b47c6be57fbc95aabb134cdfb8fbf2ba4cf Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 19:25:50 +0200 Subject: [PATCH 22/29] Convert MediaPlayerService to Kotlin --- .../ultrasonic/service/MediaPlayerService.kt | 1190 +++++++---------- 1 file changed, 513 insertions(+), 677 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt index 048f26f3..d7b15bae 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -1,885 +1,721 @@ -package org.moire.ultrasonic.service; +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.graphics.Bitmap; -import android.os.Build; -import android.os.IBinder; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; -import android.view.KeyEvent; - -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.activity.NavigationActivity; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.domain.RepeatMode; -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.util.Constants; -import org.moire.ultrasonic.util.FileUtil; -import org.moire.ultrasonic.util.NowPlayingEventDistributor; -import org.moire.ultrasonic.util.ShufflePlayBuffer; -import org.moire.ultrasonic.util.SimpleServiceBinder; -import org.moire.ultrasonic.util.Util; - -import java.util.ArrayList; - -import kotlin.Lazy; -import timber.log.Timber; - -import static org.koin.java.KoinJavaComponent.inject; -import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; -import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; -import static org.moire.ultrasonic.domain.PlayerState.IDLE; -import static org.moire.ultrasonic.domain.PlayerState.PAUSED; -import static org.moire.ultrasonic.domain.PlayerState.PREPARING; -import static org.moire.ultrasonic.domain.PlayerState.STARTED; -import static org.moire.ultrasonic.domain.PlayerState.STOPPED; +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.IBinder +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import android.view.KeyEvent +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.activity.NavigationActivity +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.domain.RepeatMode +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.MediaPlayerService +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.NowPlayingEventDistributor +import org.moire.ultrasonic.util.ShufflePlayBuffer +import org.moire.ultrasonic.util.SimpleServiceBinder +import org.moire.ultrasonic.util.Util +import timber.log.Timber +import java.util.ArrayList /** * Android Foreground Service for playing music * while the rest of the Ultrasonic App is in the background. */ -public class MediaPlayerService extends Service -{ - private static final String NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"; - private static final String NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"; - private static final int NOTIFICATION_ID = 3033; +class MediaPlayerService : Service() { + private val binder: IBinder = SimpleServiceBinder(this) + private val scrobbler = Scrobbler() + var jukeboxMediaPlayer = inject(JukeboxMediaPlayer::class.java) + private val downloadQueueSerializerLazy = inject(DownloadQueueSerializer::class.java) + private val shufflePlayBufferLazy = inject(ShufflePlayBuffer::class.java) + private val downloaderLazy = inject(Downloader::class.java) + private val localMediaPlayerLazy = inject(LocalMediaPlayer::class.java) + private val nowPlayingEventDistributor = inject(NowPlayingEventDistributor::class.java) + private val mediaPlayerLifecycleSupport = inject(MediaPlayerLifecycleSupport::class.java) + private var localMediaPlayer: LocalMediaPlayer? = null + private var downloader: Downloader? = null + private var shufflePlayBuffer: ShufflePlayBuffer? = null + private var downloadQueueSerializer: DownloadQueueSerializer? = null + private var mediaSession: MediaSessionCompat? = null + private var mediaSessionToken: MediaSessionCompat.Token? = null + private var isInForeground = false + private var notificationBuilder: NotificationCompat.Builder? = null + val repeatMode: RepeatMode + get() = Util.getRepeatMode(this) - private static MediaPlayerService instance = null; - private static final Object instanceLock = new Object(); + override fun onBind(intent: Intent): IBinder? { + return binder + } - private final IBinder binder = new SimpleServiceBinder<>(this); - private final Scrobbler scrobbler = new Scrobbler(); - - public Lazy jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class); - private final Lazy downloadQueueSerializerLazy = inject(DownloadQueueSerializer.class); - private final Lazy shufflePlayBufferLazy = inject(ShufflePlayBuffer.class); - private final Lazy downloaderLazy = inject(Downloader.class); - private final Lazy localMediaPlayerLazy = inject(LocalMediaPlayer.class); - private final Lazy nowPlayingEventDistributor = inject(NowPlayingEventDistributor.class); - private final Lazy mediaPlayerLifecycleSupport = inject(MediaPlayerLifecycleSupport.class); - - private LocalMediaPlayer localMediaPlayer; - private Downloader downloader; - private ShufflePlayBuffer shufflePlayBuffer; - private DownloadQueueSerializer downloadQueueSerializer; - - private MediaSessionCompat mediaSession; - private MediaSessionCompat.Token mediaSessionToken; - - private boolean isInForeground = false; - private NotificationCompat.Builder notificationBuilder; - - public RepeatMode getRepeatMode() { return Util.getRepeatMode(this); } - - public static MediaPlayerService getInstance(Context context) - { - synchronized (instanceLock) { - for (int i = 0; i < 20; i++) { - if (instance != null) return instance; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(new Intent(context, MediaPlayerService.class)); - } else { - context.startService(new Intent(context, MediaPlayerService.class)); - } - - Util.sleepQuietly(50L); - } - - return instance; + override fun onCreate() { + super.onCreate() + downloader = downloaderLazy.value + localMediaPlayer = localMediaPlayerLazy.value + shufflePlayBuffer = shufflePlayBufferLazy.value + downloadQueueSerializer = downloadQueueSerializerLazy.value + initMediaSessions() + downloader!!.onCreate() + shufflePlayBuffer!!.onCreate() + localMediaPlayer!!.init() + setupOnCurrentPlayingChangedHandler() + setupOnPlayerStateChangedHandler() + setupOnSongCompletedHandler() + localMediaPlayer!!.onPrepared = { + downloadQueueSerializer!!.serializeDownloadQueue( + downloader!!.downloadList, + downloader!!.currentPlayingIndex, + playerPosition + ) + null } - } - - public static MediaPlayerService getRunningInstance() - { - synchronized (instanceLock) - { - return instance; - } - } - - public static void executeOnStartedMediaPlayerService(final Context context, final Consumer taskToExecute) - { - Thread t = new Thread() - { - public void run() - { - MediaPlayerService instance = getInstance(context); - if (instance == null) - { - Timber.e("ExecuteOnStartedMediaPlayerService failed to get a MediaPlayerService instance!"); - return; - } - - taskToExecute.accept(instance); - } - }; - t.start(); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) - { - return binder; - } - - @Override - public void onCreate() - { - super.onCreate(); - - downloader = downloaderLazy.getValue(); - localMediaPlayer = localMediaPlayerLazy.getValue(); - shufflePlayBuffer = shufflePlayBufferLazy.getValue(); - downloadQueueSerializer = downloadQueueSerializerLazy.getValue(); - - initMediaSessions(); - - - downloader.onCreate(); - shufflePlayBuffer.onCreate(); - - localMediaPlayer.init(); - setupOnCurrentPlayingChangedHandler(); - setupOnPlayerStateChangedHandler(); - setupOnSongCompletedHandler(); - - localMediaPlayer.onPrepared = () -> { - downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, - downloader.getCurrentPlayingIndex(), - getPlayerPosition() - ); - return null; - }; - - localMediaPlayer.onNextSongRequested = this::setNextPlaying; + localMediaPlayer!!.onNextSongRequested = Runnable { setNextPlaying() } // Create Notification Channel - createNotificationChannel(); + createNotificationChannel() // Update notification early. It is better to show an empty one temporarily than waiting too long and letting Android kill the app - updateNotification(IDLE, null); - - instance = this; - - Timber.i("MediaPlayerService created"); + updateNotification(PlayerState.IDLE, null) + instance = this + Timber.i("MediaPlayerService created") } - @Override - public int onStartCommand(Intent intent, int flags, int startId) - { - super.onStartCommand(intent, flags, startId); - return START_NOT_STICKY; + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + return START_NOT_STICKY } - @Override - public void onDestroy() - { - super.onDestroy(); - - instance = null; - + override fun onDestroy() { + super.onDestroy() + instance = null try { - localMediaPlayer.release(); - downloader.stop(); - shufflePlayBuffer.onDestroy(); - mediaSession.release(); - } catch (Throwable ignored) { + localMediaPlayer!!.release() + downloader!!.stop() + shufflePlayBuffer!!.onDestroy() + mediaSession!!.release() + } catch (ignored: Throwable) { } - - Timber.i("MediaPlayerService stopped"); + Timber.i("MediaPlayerService stopped") } - private void stopIfIdle() - { - synchronized (instanceLock) - { + 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 == STOPPED) stopSelf(); + if (localMediaPlayer!!.currentPlaying == null || localMediaPlayer!!.playerState === PlayerState.STOPPED) stopSelf() } } - public synchronized void seekTo(int position) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().skip(downloader.getCurrentPlayingIndex(), position / 1000); - } - else - { - localMediaPlayer.seekTo(position); + @Synchronized + fun seekTo(position: Int) { + if (jukeboxMediaPlayer.value.isEnabled) { + jukeboxMediaPlayer.value.skip(downloader!!.currentPlayingIndex, position / 1000) + } else { + localMediaPlayer!!.seekTo(position) } } - public synchronized int getPlayerPosition() - { - if (localMediaPlayer.playerState == IDLE || localMediaPlayer.playerState == DOWNLOADING || localMediaPlayer.playerState == PREPARING) - { - return 0; + @get:Synchronized + val playerPosition: Int + get() { + if (localMediaPlayer!!.playerState === PlayerState.IDLE || localMediaPlayer!!.playerState === PlayerState.DOWNLOADING || localMediaPlayer!!.playerState === PlayerState.PREPARING) { + return 0 + } + return if (jukeboxMediaPlayer.value.isEnabled) jukeboxMediaPlayer.value.positionSeconds * 1000 else localMediaPlayer!!.playerPosition } - return jukeboxMediaPlayer.getValue().isEnabled() ? jukeboxMediaPlayer.getValue().getPositionSeconds() * 1000 : - localMediaPlayer.getPlayerPosition(); - } + @get:Synchronized + val playerDuration: Int + get() = localMediaPlayer!!.playerDuration - public synchronized int getPlayerDuration() - { - return localMediaPlayer.getPlayerDuration(); - } - - public synchronized void setCurrentPlaying(int currentPlayingIndex) - { - try - { - localMediaPlayer.setCurrentPlaying(downloader.downloadList.get(currentPlayingIndex)); - } - catch (IndexOutOfBoundsException x) - { + @Synchronized + fun setCurrentPlaying(currentPlayingIndex: Int) { + try { + localMediaPlayer!!.setCurrentPlaying(downloader!!.downloadList[currentPlayingIndex]) + } catch (x: IndexOutOfBoundsException) { // Ignored } } - public void setupOnCurrentPlayingChangedHandler() { - localMediaPlayer.onCurrentPlayingChanged = (DownloadFile currentPlaying) -> { - + fun setupOnCurrentPlayingChangedHandler() { + localMediaPlayer!!.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> if (currentPlaying != null) { - Util.broadcastNewTrackInfo(MediaPlayerService.this, currentPlaying.getSong()); - Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), currentPlaying, - downloader.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); + Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song) + Util.broadcastA2dpMetaDataChange(this@MediaPlayerService, playerPosition, currentPlaying, + downloader!!.downloads.size, downloader!!.currentPlayingIndex + 1) } else { - Util.broadcastNewTrackInfo(MediaPlayerService.this, null); - Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), null, - downloader.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); + Util.broadcastNewTrackInfo(this@MediaPlayerService, null) + Util.broadcastA2dpMetaDataChange(this@MediaPlayerService, playerPosition, null, + downloader!!.downloads.size, downloader!!.currentPlayingIndex + 1) } // Update widget - PlayerState playerState = localMediaPlayer.playerState; - MusicDirectory.Entry song = currentPlaying == null ? null : currentPlaying.getSong(); - UpdateWidget(playerState, song); - + val playerState = localMediaPlayer!!.playerState + val song = currentPlaying?.song + UpdateWidget(playerState, song) if (currentPlaying != null) { - updateNotification(localMediaPlayer.playerState, currentPlaying); - nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent(); + updateNotification(localMediaPlayer!!.playerState, currentPlaying) + nowPlayingEventDistributor.value.raiseShowNowPlayingEvent() } else { - nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); - stopForeground(true); - isInForeground = false; - stopIfIdle(); + nowPlayingEventDistributor.value.raiseHideNowPlayingEvent() + stopForeground(true) + isInForeground = false + stopIfIdle() } - - return null; - }; + null + } } - public synchronized void setNextPlaying() - { - boolean gaplessPlayback = Util.getGaplessPlaybackPreference(this); - - if (!gaplessPlayback) - { - localMediaPlayer.clearNextPlaying(true); - return; + @Synchronized + fun setNextPlaying() { + val gaplessPlayback = Util.getGaplessPlaybackPreference(this) + if (!gaplessPlayback) { + localMediaPlayer!!.clearNextPlaying(true) + return } - - int index = downloader.getCurrentPlayingIndex(); - - if (index != -1) - { - switch (getRepeatMode()) - { - case OFF: - index += 1; - break; - case ALL: - index = (index + 1) % downloader.downloadList.size(); - break; - case SINGLE: - default: - break; + var index = downloader!!.currentPlayingIndex + if (index != -1) { + when (repeatMode) { + RepeatMode.OFF -> index += 1 + RepeatMode.ALL -> index = (index + 1) % downloader!!.downloadList.size + RepeatMode.SINGLE -> { + } + else -> { + } } } - - localMediaPlayer.clearNextPlaying(false); - - if (index < downloader.downloadList.size() && index != -1) - { - localMediaPlayer.setNextPlaying(downloader.downloadList.get(index)); - } - else - { - localMediaPlayer.clearNextPlaying(true); + localMediaPlayer!!.clearNextPlaying(false) + if (index < downloader!!.downloadList.size && index != -1) { + localMediaPlayer!!.setNextPlaying(downloader!!.downloadList[index]) + } else { + localMediaPlayer!!.clearNextPlaying(true) } } - public synchronized void togglePlayPause() - { - if (localMediaPlayer.playerState == PAUSED || localMediaPlayer.playerState == COMPLETED || localMediaPlayer.playerState == STOPPED) - { - start(); - } - else if (localMediaPlayer.playerState == IDLE) - { - play(); - } - else if (localMediaPlayer.playerState == STARTED) - { - pause(); + @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() } } - public synchronized void resumeOrPlay() - { - if (localMediaPlayer.playerState == PAUSED || localMediaPlayer.playerState == COMPLETED || localMediaPlayer.playerState == STOPPED) - { - start(); - } - else if (localMediaPlayer.playerState == IDLE) - { - play(); + @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. */ - public synchronized void play() - { - int current = downloader.getCurrentPlayingIndex(); - if (current == -1) - { - play(0); - } - else - { - play(current); + @Synchronized + fun play() { + val current = downloader!!.currentPlayingIndex + if (current == -1) { + play(0) + } else { + play(current) } } - public synchronized void play(int index) - { - play(index, true); + @Synchronized + fun play(index: Int) { + play(index, true) } - public synchronized void play(int index, boolean start) - { - Timber.v("play requested for %d", index); - if (index < 0 || index >= downloader.downloadList.size()) - { - resetPlayback(); - } - else - { - setCurrentPlaying(index); - - if (start) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().skip(index, 0); - localMediaPlayer.setPlayerState(STARTED); - } - else - { - localMediaPlayer.play(downloader.downloadList.get(index)); + @Synchronized + fun play(index: Int, start: Boolean) { + Timber.v("play requested for %d", index) + if (index < 0 || index >= downloader!!.downloadList.size) { + resetPlayback() + } else { + setCurrentPlaying(index) + if (start) { + if (jukeboxMediaPlayer.value.isEnabled) { + jukeboxMediaPlayer.value.skip(index, 0) + localMediaPlayer!!.setPlayerState(PlayerState.STARTED) + } else { + localMediaPlayer!!.play(downloader!!.downloadList[index]) } } - - downloader.checkDownloads(); - setNextPlaying(); + downloader!!.checkDownloads() + setNextPlaying() } } - private synchronized void resetPlayback() - { - localMediaPlayer.reset(); - localMediaPlayer.setCurrentPlaying(null); - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, - downloader.getCurrentPlayingIndex(), getPlayerPosition()); + @Synchronized + private fun resetPlayback() { + localMediaPlayer!!.reset() + localMediaPlayer!!.setCurrentPlaying(null) + downloadQueueSerializer!!.serializeDownloadQueue(downloader!!.downloadList, + downloader!!.currentPlayingIndex, playerPosition) } - public synchronized void pause() - { - if (localMediaPlayer.playerState == STARTED) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().stop(); + @Synchronized + fun pause() { + if (localMediaPlayer!!.playerState === PlayerState.STARTED) { + if (jukeboxMediaPlayer.value.isEnabled) { + jukeboxMediaPlayer.value.stop() + } else { + localMediaPlayer!!.pause() } - else - { - localMediaPlayer.pause(); - } - localMediaPlayer.setPlayerState(PAUSED); + localMediaPlayer!!.setPlayerState(PlayerState.PAUSED) } } - public synchronized void stop() - { - if (localMediaPlayer.playerState == STARTED) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().stop(); - } - else - { - localMediaPlayer.pause(); + @Synchronized + fun stop() { + if (localMediaPlayer!!.playerState === PlayerState.STARTED) { + if (jukeboxMediaPlayer.value.isEnabled) { + jukeboxMediaPlayer.value.stop() + } else { + localMediaPlayer!!.pause() } } - localMediaPlayer.setPlayerState(STOPPED); + localMediaPlayer!!.setPlayerState(PlayerState.STOPPED) } - public synchronized void start() - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().start(); + @Synchronized + fun start() { + if (jukeboxMediaPlayer.value.isEnabled) { + jukeboxMediaPlayer.value.start() + } else { + localMediaPlayer!!.start() } - else - { - localMediaPlayer.start(); - } - localMediaPlayer.setPlayerState(STARTED); + localMediaPlayer!!.setPlayerState(PlayerState.STARTED) } - private void UpdateWidget(PlayerState playerState, MusicDirectory.Entry song) { - UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, true); - UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + private fun UpdateWidget(playerState: PlayerState, song: MusicDirectory.Entry?) { + UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, false) + UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, true) + UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, false) + UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, false) } - public void setupOnPlayerStateChangedHandler() { - localMediaPlayer.onPlayerStateChanged = (PlayerState playerState, DownloadFile currentPlaying) -> { + fun setupOnPlayerStateChangedHandler() { + localMediaPlayer!!.onPlayerStateChanged = { playerState: PlayerState, currentPlaying: DownloadFile? -> // Notify MediaSession - updateMediaSession(currentPlaying, playerState); - - if (playerState == PAUSED) { - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); + updateMediaSession(currentPlaying, playerState) + if (playerState === PlayerState.PAUSED) { + downloadQueueSerializer!!.serializeDownloadQueue(downloader!!.downloadList, downloader!!.currentPlayingIndex, playerPosition) } - - boolean showWhenPaused = (playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(MediaPlayerService.this)); - boolean show = playerState == PlayerState.STARTED || showWhenPaused; - MusicDirectory.Entry song = currentPlaying == null ? null : currentPlaying.getSong(); - - Util.broadcastPlaybackStatusChange(MediaPlayerService.this, playerState); - Util.broadcastA2dpPlayStatusChange(MediaPlayerService.this, playerState, song, - downloader.downloadList.size() + downloader.backgroundDownloadList.size(), - downloader.downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); + val showWhenPaused = playerState !== PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(this@MediaPlayerService) + val show = playerState === PlayerState.STARTED || showWhenPaused + val song = currentPlaying?.song + Util.broadcastPlaybackStatusChange(this@MediaPlayerService, playerState) + Util.broadcastA2dpPlayStatusChange(this@MediaPlayerService, playerState, song, + downloader!!.downloadList.size + downloader!!.backgroundDownloadList.size, + downloader!!.downloadList.indexOf(currentPlaying) + 1, playerPosition) // Update widget - UpdateWidget(playerState, song); - + 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); - nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent(); + if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) { + updateNotification(playerState, currentPlaying) + nowPlayingEventDistributor.value.raiseShowNowPlayingEvent() } } else { - nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent(); - stopForeground(true); - isInForeground = false; - stopIfIdle(); + nowPlayingEventDistributor.value.raiseHideNowPlayingEvent() + stopForeground(true) + isInForeground = false + stopIfIdle() } - - if (playerState == STARTED) { - scrobbler.scrobble(MediaPlayerService.this, currentPlaying, false); - } else if (playerState == COMPLETED) { - scrobbler.scrobble(MediaPlayerService.this, currentPlaying, true); + if (playerState === PlayerState.STARTED) { + scrobbler.scrobble(this@MediaPlayerService, currentPlaying, false) + } else if (playerState === PlayerState.COMPLETED) { + scrobbler.scrobble(this@MediaPlayerService, currentPlaying, true) } - - return null; - }; + null + } } - private void setupOnSongCompletedHandler() { - localMediaPlayer.onSongCompleted = (DownloadFile currentPlaying) -> { - int index = downloader.getCurrentPlayingIndex(); - + private fun setupOnSongCompletedHandler() { + localMediaPlayer!!.onSongCompleted = { currentPlaying: DownloadFile? -> + val index = downloader!!.currentPlayingIndex if (currentPlaying != null) { - final MusicDirectory.Entry song = currentPlaying.getSong(); - - if (song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(MediaPlayerService.this)) { - MusicService musicService = MusicServiceFactory.getMusicService(MediaPlayerService.this); + val song = currentPlaying.song + if (song.bookmarkPosition > 0 && Util.getShouldClearBookmark(this@MediaPlayerService)) { + val musicService = getMusicService(this@MediaPlayerService) try { - musicService.deleteBookmark(song.getId(), MediaPlayerService.this); - } catch (Exception ignored) { - + musicService.deleteBookmark(song.id, this@MediaPlayerService) + } catch (ignored: Exception) { } } } - if (index != -1) { - switch (getRepeatMode()) { - case OFF: - if (index + 1 < 0 || index + 1 >= downloader.downloadList.size()) { - if (Util.getShouldClearPlaylist(MediaPlayerService.this)) { - clear(true); - jukeboxMediaPlayer.getValue().updatePlaylist(); + when (repeatMode) { + RepeatMode.OFF -> { + if (index + 1 < 0 || index + 1 >= downloader!!.downloadList.size) { + if (Util.getShouldClearPlaylist(this@MediaPlayerService)) { + clear(true) + jukeboxMediaPlayer.value.updatePlaylist() } - - resetPlayback(); - break; + resetPlayback() + break } - - play(index + 1); - break; - case ALL: - play((index + 1) % downloader.downloadList.size()); - break; - case SINGLE: - play(index); - break; - default: - break; + play(index + 1) + } + RepeatMode.ALL -> play((index + 1) % downloader!!.downloadList.size) + RepeatMode.SINGLE -> play(index) + else -> { + } } } - - return null; - }; - } - - public synchronized void clear(boolean serialize) - { - localMediaPlayer.reset(); - downloader.clear(); - localMediaPlayer.setCurrentPlaying(null); - - setNextPlaying(); - - if (serialize) { - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, - downloader.getCurrentPlayingIndex(), getPlayerPosition()); + null } } - private void updateMediaSession(DownloadFile currentPlaying, PlayerState playerState) { - // Set Metadata - MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder(); - Context context = getApplicationContext(); + @Synchronized + fun clear(serialize: Boolean) { + localMediaPlayer!!.reset() + downloader!!.clear() + localMediaPlayer!!.setCurrentPlaying(null) + setNextPlaying() + if (serialize) { + downloadQueueSerializer!!.serializeDownloadQueue(downloader!!.downloadList, + downloader!!.currentPlayingIndex, playerPosition) + } + } + private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) { + // Set Metadata + val metadata = MediaMetadataCompat.Builder() + val context = applicationContext if (currentPlaying != null) { try { - MusicDirectory.Entry song = currentPlaying.getSong(); - - Bitmap cover = FileUtil.getAlbumArtBitmap(context, song, + val song = currentPlaying.song + val cover = FileUtil.getAlbumArtBitmap(context, song, Util.getMinDisplayMetric(context), true - ); - - metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L); - metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtist()); - metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtist()); - metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbum()); - metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getTitle()); - metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover); - } catch (Exception e) { - Timber.e(e, "Error setting the metadata"); + ) + metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L) + 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 (e: Exception) { + Timber.e(e, "Error setting the metadata") } } // Save the metadata - mediaSession.setMetadata(metadata.build()); + mediaSession!!.setMetadata(metadata.build()) // Create playback State - PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder(); - int state = (playerState == STARTED) ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; + val playbackState = PlaybackStateCompat.Builder() + val state = if (playerState === PlayerState.STARTED) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED // If we set the playback position correctly, we can get a nice seek bar :) - playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0F); + playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f) // Set Active state - mediaSession.setActive(playerState == STARTED); + mediaSession!!.isActive = playerState === PlayerState.STARTED // Save the playback state - mediaSession.setPlaybackState(playbackState.build()); + mediaSession!!.setPlaybackState(playbackState.build()) } - private void createNotificationChannel() { + private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //The suggested importance of a startForeground service notification is IMPORTANCE_LOW - NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); - channel.setLightColor(android.R.color.holo_blue_dark); - channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - channel.setShowBadge(false); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(channel); + 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) } } - public void updateNotification(PlayerState playerState, DownloadFile currentPlaying) - { + fun updateNotification(playerState: PlayerState, currentPlaying: DownloadFile?) { if (Util.isNotificationEnabled(this)) { if (isInForeground) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)) } else { - final NotificationManagerCompat notificationManager = - NotificationManagerCompat.from(this); - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); + val notificationManager = NotificationManagerCompat.from(this) + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)) } - Timber.w("--- Updated notification"); + Timber.w("--- Updated notification") } else { - startForeground(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); - isInForeground = true; - Timber.w("--- Created Foreground notification"); + startForeground(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)) + isInForeground = true + Timber.w("--- Created Foreground notification") } } } - /** * This method builds a notification, reusing the Notification Builder if possible */ - private Notification buildForegroundNotification(PlayerState playerState, DownloadFile currentPlaying) { + private fun buildForegroundNotification(playerState: PlayerState, currentPlaying: DownloadFile?): Notification { // Init - Context context = getApplicationContext(); - MusicDirectory.Entry song = (currentPlaying != null) ? currentPlaying.getSong() : null; - PendingIntent stopIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_STOP, 100); + val context = applicationContext + val song = currentPlaying?.song + val stopIntent = 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 = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); + 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.setPriority(NotificationCompat.PRIORITY_LOW); + 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()); + notificationBuilder!!.setContentIntent(pendingIntentForContent) // This intent is executed when the user closes the notification - notificationBuilder.setDeleteIntent(stopIntent); + notificationBuilder!!.setDeleteIntent(stopIntent) } // Use the Media Style, to enable native Android support for playback notification - androidx.media.app.NotificationCompat.MediaStyle style = new androidx.media.app.NotificationCompat.MediaStyle(); - style.setMediaSession(mediaSessionToken); + val style = androidx.media.app.NotificationCompat.MediaStyle() + style.setMediaSession(mediaSessionToken) // Clear old actions - notificationBuilder.clearActions(); + notificationBuilder!!.clearActions() // Add actions - int[] compactActions = addActions(context, notificationBuilder, playerState, song); + val compactActions = addActions(context, notificationBuilder!!, playerState, song) // Configure shortcut actions - style.setShowActionsInCompactView(compactActions); - notificationBuilder.setStyle(style); + style.setShowActionsInCompactView(*compactActions) + notificationBuilder!!.setStyle(style) // Set song title, artist and cover if possible if (song != null) { - int iconSize = (int) (256 * context.getResources().getDisplayMetrics().density); - Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, iconSize, true); - notificationBuilder.setContentTitle(song.getTitle()); - notificationBuilder.setContentText(song.getArtist()); - notificationBuilder.setLargeIcon(bitmap); - notificationBuilder.setSubText(song.getAlbum()); + val iconSize = (256 * context.resources.displayMetrics.density).toInt() + val bitmap = FileUtil.getAlbumArtBitmap(context, song, iconSize, true) + notificationBuilder!!.setContentTitle(song.title) + notificationBuilder!!.setContentText(song.artist) + notificationBuilder!!.setLargeIcon(bitmap) + notificationBuilder!!.setSubText(song.album) } - - return notificationBuilder.build(); + return notificationBuilder!!.build() } - - private int[] addActions(Context context, NotificationCompat.Builder notificationBuilder, PlayerState playerState, MusicDirectory.Entry song) { - ArrayList compactActionList = new ArrayList<>(); - int numActions = 0; // we start and 0 and then increment by 1 for each call to generateAction + private fun addActions(context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry?): IntArray { + 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(generateStarUnstarAction(context, numActions, song.getStarred())); + notificationBuilder.addAction(generateStarUnstarAction(context, numActions, song.starred)) } - numActions++; + numActions++ // Next - notificationBuilder.addAction(generateAction(context, numActions)); - compactActionList.add(numActions); - numActions++; + notificationBuilder.addAction(generateAction(context, numActions)) + compactActionList.add(numActions) + numActions++ // Play/Pause button - notificationBuilder.addAction(generatePlayPauseAction(context, numActions, playerState)); - compactActionList.add(numActions); - numActions++; + notificationBuilder.addAction(generatePlayPauseAction(context, numActions, playerState)) + compactActionList.add(numActions) + numActions++ // Previous - notificationBuilder.addAction(generateAction(context, numActions)); - compactActionList.add(numActions); - numActions++; + notificationBuilder.addAction(generateAction(context, numActions)) + compactActionList.add(numActions) + numActions++ // Close - notificationBuilder.addAction(generateAction(context, numActions)); - - int[] actionArray = new int[compactActionList.size()]; - - for (int i = 0; i < actionArray.length; i++) { - actionArray[i] = compactActionList.get(i); + notificationBuilder.addAction(generateAction(context, numActions)) + val actionArray = IntArray(compactActionList.size) + for (i in actionArray.indices) { + actionArray[i] = compactActionList[i] } - - return actionArray; + return actionArray //notificationBuilder.setShowActionsInCompactView()) } - - private NotificationCompat.Action generateAction(Context context, int requestCode) { - int keycode; - int icon; - String label; - - // If you change the order here, also update the requestCode in updatePlayPauseAction()! - switch (requestCode) { - case 1: - keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; - label = getString(R.string.common_play_previous); - icon = R.drawable.media_backward_medium_dark; - break; - case 2: - // Is handled in generatePlayPauseAction() - return null; - case 3: - keycode = KeyEvent.KEYCODE_MEDIA_NEXT; - label = getString(R.string.common_play_next); - icon = R.drawable.media_forward_medium_dark; - break; - case 4: - keycode = KeyEvent.KEYCODE_MEDIA_STOP; - label = getString(R.string.buttons_stop); - icon = R.drawable.ic_baseline_close_24; - break; - default: - return null; + 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_24 + } + else -> return null } - - PendingIntent pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode); - - return new NotificationCompat.Action.Builder(icon, label, pendingIntent).build(); + val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode) + return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() } - private NotificationCompat.Action generatePlayPauseAction(Context context, int requestCode, PlayerState playerState) { - - boolean isPlaying = (playerState == STARTED); - PendingIntent pendingIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, requestCode); - - String label; - int icon; - + private fun generatePlayPauseAction(context: Context, requestCode: Int, playerState: PlayerState): NotificationCompat.Action { + val isPlaying = playerState === PlayerState.STARTED + val pendingIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, requestCode) + val label: String + val icon: Int if (isPlaying) { - label = getString(R.string.common_pause); - icon = R.drawable.media_pause_large_dark; + 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; + label = getString(R.string.common_play) + icon = R.drawable.media_start_large_dark } - - return new NotificationCompat.Action.Builder(icon, label, pendingIntent).build(); + return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() } - - private NotificationCompat.Action generateStarUnstarAction(Context context, int requestCode, Boolean isStarred) { - - int keyCode; - String label; - int icon; - keyCode = KeyEvent.KEYCODE_STAR; - + private fun generateStarUnstarAction(context: Context, requestCode: Int, isStarred: Boolean): NotificationCompat.Action { + val keyCode: Int + val label: String + val icon: Int + keyCode = KeyEvent.KEYCODE_STAR if (isStarred) { - label = getString(R.string.download_menu_star); - icon = R.drawable.ic_star_full_dark; - + 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; + label = getString(R.string.download_menu_star) + icon = R.drawable.ic_star_hollow_dark + } + val pendingIntent = getPendingIntentForMediaAction(context, keyCode, requestCode) + return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() + } + + private val pendingIntentForContent: PendingIntent + private get() { + val notificationIntent = Intent(this, NavigationActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true) + return PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) } - PendingIntent pendingIntent = getPendingIntentForMediaAction(context, keyCode, requestCode); - - return new NotificationCompat.Action.Builder(icon, label, pendingIntent).build(); + private fun getPendingIntentForMediaAction(context: Context, keycode: Int, requestCode: Int): PendingIntent { + val intent = Intent(Constants.CMD_PROCESS_KEYCODE) + intent.setPackage(context.packageName) + intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode)) + return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - - private PendingIntent getPendingIntentForContent() { - Intent notificationIntent = new Intent(this, NavigationActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true); - return PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent getPendingIntentForMediaAction(Context context, int keycode, int requestCode) { - Intent intent = new Intent(Constants.CMD_PROCESS_KEYCODE); - intent.setPackage(context.getPackageName()); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keycode)); - - return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private void initMediaSessions() { - - mediaSession = new MediaSessionCompat(getApplicationContext(), "UltrasonicService"); - mediaSessionToken = mediaSession.getSessionToken(); + private fun initMediaSessions() { + mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService") + mediaSessionToken = mediaSession!!.sessionToken //mediaController = new MediaControllerCompat(getApplicationContext(), mediaSessionToken); + mediaSession!!.setCallback(object : MediaSessionCompat.Callback() { + override fun onPlay() { + super.onPlay() + play() + Timber.w("Media Session Callback: onPlay") + } - mediaSession.setCallback(new MediaSessionCompat.Callback() { - @Override - public void onPlay() { - super.onPlay(); - play(); - Timber.w("Media Session Callback: onPlay"); - } + override fun onPause() { + super.onPause() + pause() + Timber.w("Media Session Callback: onPause") + } - @Override - public void onPause() { - super.onPause(); - pause(); - Timber.w("Media Session Callback: onPause"); - } + override fun onStop() { + super.onStop() + stop() + Timber.w("Media Session Callback: onStop") + } - @Override - public void onStop() { - super.onStop(); - stop(); - Timber.w("Media Session Callback: onStop"); - } + override fun onSeekTo(pos: Long) { + super.onSeekTo(pos) + } - @Override - public void onSeekTo(long pos) { - super.onSeekTo(pos); - } - - @Override - public boolean onMediaButtonEvent(Intent mediaButtonEvent) { - // This probably won't be necessary once we implement more - // of the modern media APIs, like the MediaController etc. - KeyEvent event = (KeyEvent) mediaButtonEvent.getExtras().get("android.intent.extra.KEY_EVENT"); - MediaPlayerLifecycleSupport lifecycleSupport = mediaPlayerLifecycleSupport.getValue(); - lifecycleSupport.handleKeyEvent(event); - - return true; - } - - } - ); + 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? + val lifecycleSupport = mediaPlayerLifecycleSupport.value + lifecycleSupport.handleKeyEvent(event) + return true + } + } + ) } -} + + 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 var instance: MediaPlayerService? = null + private val instanceLock = Any() + @JvmStatic + fun getInstance(context: Context): MediaPlayerService? { + synchronized(instanceLock) { + for (i in 0..19) { + 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(50L) + } + return instance + } + } + + @JvmStatic + val runningInstance: MediaPlayerService? + get() { + synchronized(instanceLock) { return instance } + } + + @JvmStatic + fun executeOnStartedMediaPlayerService(context: Context, taskToExecute: Consumer) { + val t: Thread = object : Thread() { + override fun run() { + val instance = getInstance(context) + if (instance == null) { + Timber.e("ExecuteOnStartedMediaPlayerService failed to get a MediaPlayerService instance!") + return + } + taskToExecute.accept(instance) + } + } + t.start() + } + } +} \ No newline at end of file From 3aae91bf137a29a678f65dcc317e203b5a7b1272 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 19:47:06 +0200 Subject: [PATCH 23/29] Refactor after conversion --- .../ultrasonic/service/MediaPlayerService.kt | 479 +++++++++++------- 1 file changed, 293 insertions(+), 186 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt index d7b15bae..401a1a29 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -1,3 +1,10 @@ +/* + * 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 @@ -15,7 +22,8 @@ import android.support.v4.media.session.PlaybackStateCompat import android.view.KeyEvent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import org.koin.java.KoinJavaComponent.inject +import java.util.ArrayList +import org.koin.android.ext.android.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.domain.MusicDirectory @@ -25,7 +33,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.MediaPlayerService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil @@ -34,7 +41,6 @@ import org.moire.ultrasonic.util.ShufflePlayBuffer import org.moire.ultrasonic.util.SimpleServiceBinder import org.moire.ultrasonic.util.Util import timber.log.Timber -import java.util.ArrayList /** * Android Foreground Service for playing music @@ -43,55 +49,54 @@ import java.util.ArrayList class MediaPlayerService : Service() { private val binder: IBinder = SimpleServiceBinder(this) private val scrobbler = Scrobbler() - var jukeboxMediaPlayer = inject(JukeboxMediaPlayer::class.java) - private val downloadQueueSerializerLazy = inject(DownloadQueueSerializer::class.java) - private val shufflePlayBufferLazy = inject(ShufflePlayBuffer::class.java) - private val downloaderLazy = inject(Downloader::class.java) - private val localMediaPlayerLazy = inject(LocalMediaPlayer::class.java) - private val nowPlayingEventDistributor = inject(NowPlayingEventDistributor::class.java) - private val mediaPlayerLifecycleSupport = inject(MediaPlayerLifecycleSupport::class.java) - private var localMediaPlayer: LocalMediaPlayer? = null - private var downloader: Downloader? = null - private var shufflePlayBuffer: ShufflePlayBuffer? = null - private var downloadQueueSerializer: DownloadQueueSerializer? = null + private val jukeboxMediaPlayer by inject() + private val downloadQueueSerializer by inject() + private val shufflePlayBuffer by inject() + private val downloader by inject() + private val localMediaPlayer by inject() + private val nowPlayingEventDistributor by inject() + private val mediaPlayerLifecycleSupport by inject() + private var mediaSession: MediaSessionCompat? = null private var mediaSessionToken: MediaSessionCompat.Token? = null private var isInForeground = false private var notificationBuilder: NotificationCompat.Builder? = null + val repeatMode: RepeatMode get() = Util.getRepeatMode(this) - override fun onBind(intent: Intent): IBinder? { + override fun onBind(intent: Intent): IBinder { return binder } override fun onCreate() { super.onCreate() - downloader = downloaderLazy.value - localMediaPlayer = localMediaPlayerLazy.value - shufflePlayBuffer = shufflePlayBufferLazy.value - downloadQueueSerializer = downloadQueueSerializerLazy.value + initMediaSessions() - downloader!!.onCreate() - shufflePlayBuffer!!.onCreate() - localMediaPlayer!!.init() + downloader.onCreate() + shufflePlayBuffer.onCreate() + localMediaPlayer.init() + setupOnCurrentPlayingChangedHandler() setupOnPlayerStateChangedHandler() setupOnSongCompletedHandler() - localMediaPlayer!!.onPrepared = { - downloadQueueSerializer!!.serializeDownloadQueue( - downloader!!.downloadList, - downloader!!.currentPlayingIndex, - playerPosition + + localMediaPlayer.onPrepared = { + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, + playerPosition ) null } - localMediaPlayer!!.onNextSongRequested = Runnable { setNextPlaying() } + + 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 + // 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) instance = this Timber.i("MediaPlayerService created") @@ -106,9 +111,9 @@ class MediaPlayerService : Service() { super.onDestroy() instance = null try { - localMediaPlayer!!.release() - downloader!!.stop() - shufflePlayBuffer!!.onDestroy() + localMediaPlayer.release() + downloader.stop() + shufflePlayBuffer.onDestroy() mediaSession!!.release() } catch (ignored: Throwable) { } @@ -117,113 +122,108 @@ class MediaPlayerService : Service() { 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() + // 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() + } } } @Synchronized fun seekTo(position: Int) { - if (jukeboxMediaPlayer.value.isEnabled) { - jukeboxMediaPlayer.value.skip(downloader!!.currentPlayingIndex, position / 1000) + if (jukeboxMediaPlayer.isEnabled) { + // TODO These APIs should be more aligned + val seconds = position / 1000 + jukeboxMediaPlayer.skip(downloader.currentPlayingIndex, seconds) } else { - localMediaPlayer!!.seekTo(position) + localMediaPlayer.seekTo(position) } } @get:Synchronized val playerPosition: Int get() { - if (localMediaPlayer!!.playerState === PlayerState.IDLE || localMediaPlayer!!.playerState === PlayerState.DOWNLOADING || localMediaPlayer!!.playerState === PlayerState.PREPARING) { + if (localMediaPlayer.playerState === PlayerState.IDLE || + localMediaPlayer.playerState === PlayerState.DOWNLOADING || + localMediaPlayer.playerState === PlayerState.PREPARING + ) { return 0 } - return if (jukeboxMediaPlayer.value.isEnabled) jukeboxMediaPlayer.value.positionSeconds * 1000 else localMediaPlayer!!.playerPosition + return if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.positionSeconds * 1000 + } else { + localMediaPlayer.playerPosition + } } @get:Synchronized val playerDuration: Int - get() = localMediaPlayer!!.playerDuration + get() = localMediaPlayer.playerDuration @Synchronized fun setCurrentPlaying(currentPlayingIndex: Int) { try { - localMediaPlayer!!.setCurrentPlaying(downloader!!.downloadList[currentPlayingIndex]) + localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex]) } catch (x: IndexOutOfBoundsException) { // Ignored } } - fun setupOnCurrentPlayingChangedHandler() { - localMediaPlayer!!.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> - if (currentPlaying != null) { - Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song) - Util.broadcastA2dpMetaDataChange(this@MediaPlayerService, playerPosition, currentPlaying, - downloader!!.downloads.size, downloader!!.currentPlayingIndex + 1) - } else { - Util.broadcastNewTrackInfo(this@MediaPlayerService, null) - Util.broadcastA2dpMetaDataChange(this@MediaPlayerService, playerPosition, null, - downloader!!.downloads.size, downloader!!.currentPlayingIndex + 1) - } - - // Update widget - val playerState = localMediaPlayer!!.playerState - val song = currentPlaying?.song - UpdateWidget(playerState, song) - if (currentPlaying != null) { - updateNotification(localMediaPlayer!!.playerState, currentPlaying) - nowPlayingEventDistributor.value.raiseShowNowPlayingEvent() - } else { - nowPlayingEventDistributor.value.raiseHideNowPlayingEvent() - stopForeground(true) - isInForeground = false - stopIfIdle() - } - null - } - } - @Synchronized fun setNextPlaying() { val gaplessPlayback = Util.getGaplessPlaybackPreference(this) + if (!gaplessPlayback) { - localMediaPlayer!!.clearNextPlaying(true) + localMediaPlayer.clearNextPlaying(true) return } - var index = downloader!!.currentPlayingIndex + + var index = downloader.currentPlayingIndex + if (index != -1) { when (repeatMode) { RepeatMode.OFF -> index += 1 - RepeatMode.ALL -> index = (index + 1) % downloader!!.downloadList.size + RepeatMode.ALL -> index = (index + 1) % downloader.downloadList.size RepeatMode.SINGLE -> { } else -> { } } } - localMediaPlayer!!.clearNextPlaying(false) - if (index < downloader!!.downloadList.size && index != -1) { - localMediaPlayer!!.setNextPlaying(downloader!!.downloadList[index]) + + localMediaPlayer.clearNextPlaying(false) + if (index < downloader.downloadList.size && index != -1) { + localMediaPlayer.setNextPlaying(downloader.downloadList[index]) } else { - localMediaPlayer!!.clearNextPlaying(true) + localMediaPlayer.clearNextPlaying(true) } } @Synchronized fun togglePlayPause() { - if (localMediaPlayer!!.playerState === PlayerState.PAUSED || localMediaPlayer!!.playerState === PlayerState.COMPLETED || localMediaPlayer!!.playerState === PlayerState.STOPPED) { + if (localMediaPlayer.playerState === PlayerState.PAUSED || + localMediaPlayer.playerState === PlayerState.COMPLETED || + localMediaPlayer.playerState === PlayerState.STOPPED + ) { start() - } else if (localMediaPlayer!!.playerState === PlayerState.IDLE) { + } else if (localMediaPlayer.playerState === PlayerState.IDLE) { play() - } else if (localMediaPlayer!!.playerState === PlayerState.STARTED) { + } else if (localMediaPlayer.playerState === PlayerState.STARTED) { pause() } } @Synchronized fun resumeOrPlay() { - if (localMediaPlayer!!.playerState === PlayerState.PAUSED || localMediaPlayer!!.playerState === PlayerState.COMPLETED || localMediaPlayer!!.playerState === PlayerState.STOPPED) { + if (localMediaPlayer.playerState === PlayerState.PAUSED || + localMediaPlayer.playerState === PlayerState.COMPLETED || + localMediaPlayer.playerState === PlayerState.STOPPED + ) { start() - } else if (localMediaPlayer!!.playerState === PlayerState.IDLE) { + } else if (localMediaPlayer.playerState === PlayerState.IDLE) { play() } } @@ -233,7 +233,7 @@ class MediaPlayerService : Service() { */ @Synchronized fun play() { - val current = downloader!!.currentPlayingIndex + val current = downloader.currentPlayingIndex if (current == -1) { play(0) } else { @@ -249,119 +249,176 @@ class MediaPlayerService : Service() { @Synchronized fun play(index: Int, start: Boolean) { Timber.v("play requested for %d", index) - if (index < 0 || index >= downloader!!.downloadList.size) { + if (index < 0 || index >= downloader.downloadList.size) { resetPlayback() } else { setCurrentPlaying(index) if (start) { - if (jukeboxMediaPlayer.value.isEnabled) { - jukeboxMediaPlayer.value.skip(index, 0) - localMediaPlayer!!.setPlayerState(PlayerState.STARTED) + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.skip(index, 0) + localMediaPlayer.setPlayerState(PlayerState.STARTED) } else { - localMediaPlayer!!.play(downloader!!.downloadList[index]) + localMediaPlayer.play(downloader.downloadList[index]) } } - downloader!!.checkDownloads() + downloader.checkDownloads() setNextPlaying() } } @Synchronized private fun resetPlayback() { - localMediaPlayer!!.reset() - localMediaPlayer!!.setCurrentPlaying(null) - downloadQueueSerializer!!.serializeDownloadQueue(downloader!!.downloadList, - downloader!!.currentPlayingIndex, playerPosition) + localMediaPlayer.reset() + localMediaPlayer.setCurrentPlaying(null) + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, playerPosition + ) } @Synchronized fun pause() { - if (localMediaPlayer!!.playerState === PlayerState.STARTED) { - if (jukeboxMediaPlayer.value.isEnabled) { - jukeboxMediaPlayer.value.stop() + if (localMediaPlayer.playerState === PlayerState.STARTED) { + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.stop() } else { - localMediaPlayer!!.pause() + localMediaPlayer.pause() } - localMediaPlayer!!.setPlayerState(PlayerState.PAUSED) + localMediaPlayer.setPlayerState(PlayerState.PAUSED) } } @Synchronized fun stop() { - if (localMediaPlayer!!.playerState === PlayerState.STARTED) { - if (jukeboxMediaPlayer.value.isEnabled) { - jukeboxMediaPlayer.value.stop() + if (localMediaPlayer.playerState === PlayerState.STARTED) { + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.stop() } else { - localMediaPlayer!!.pause() + localMediaPlayer.pause() } } - localMediaPlayer!!.setPlayerState(PlayerState.STOPPED) + localMediaPlayer.setPlayerState(PlayerState.STOPPED) } @Synchronized fun start() { - if (jukeboxMediaPlayer.value.isEnabled) { - jukeboxMediaPlayer.value.start() + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.start() } else { - localMediaPlayer!!.start() + localMediaPlayer.start() } - localMediaPlayer!!.setPlayerState(PlayerState.STARTED) + localMediaPlayer.setPlayerState(PlayerState.STARTED) } - private fun UpdateWidget(playerState: PlayerState, song: MusicDirectory.Entry?) { - UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, false) - UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, true) - UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, false) - UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(this@MediaPlayerService, song, playerState === PlayerState.STARTED, false) + private fun updateWidget(playerState: PlayerState, song: MusicDirectory.Entry?) { + 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) } - fun setupOnPlayerStateChangedHandler() { - localMediaPlayer!!.onPlayerStateChanged = { playerState: PlayerState, currentPlaying: DownloadFile? -> + private fun setupOnCurrentPlayingChangedHandler() { + localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> + + if (currentPlaying != null) { + Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song) + Util.broadcastA2dpMetaDataChange( + this@MediaPlayerService, playerPosition, currentPlaying, + downloader.downloads.size, downloader.currentPlayingIndex + 1 + ) + } else { + Util.broadcastNewTrackInfo(this@MediaPlayerService, null) + Util.broadcastA2dpMetaDataChange( + this@MediaPlayerService, playerPosition, null, + downloader.downloads.size, downloader.currentPlayingIndex + 1 + ) + } + + // Update widget + val playerState = localMediaPlayer.playerState + val song = currentPlaying?.song + + updateWidget(playerState, song) + + if (currentPlaying != null) { + updateNotification(localMediaPlayer.playerState, currentPlaying) + nowPlayingEventDistributor.raiseShowNowPlayingEvent() + } else { + nowPlayingEventDistributor.raiseHideNowPlayingEvent() + stopForeground(true) + isInForeground = false + stopIfIdle() + } + null + } + } + + private fun setupOnPlayerStateChangedHandler() { + localMediaPlayer.onPlayerStateChanged = { + playerState: PlayerState, + currentPlaying: DownloadFile? + -> + + val context = this@MediaPlayerService + // Notify MediaSession updateMediaSession(currentPlaying, playerState) if (playerState === PlayerState.PAUSED) { - downloadQueueSerializer!!.serializeDownloadQueue(downloader!!.downloadList, downloader!!.currentPlayingIndex, playerPosition) + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, downloader.currentPlayingIndex, playerPosition + ) } - val showWhenPaused = playerState !== PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(this@MediaPlayerService) + + val showWhenPaused = playerState !== PlayerState.STOPPED && + Util.isNotificationAlwaysEnabled(context) + val show = playerState === PlayerState.STARTED || showWhenPaused val song = currentPlaying?.song - Util.broadcastPlaybackStatusChange(this@MediaPlayerService, playerState) - Util.broadcastA2dpPlayStatusChange(this@MediaPlayerService, playerState, song, - downloader!!.downloadList.size + downloader!!.backgroundDownloadList.size, - downloader!!.downloadList.indexOf(currentPlaying) + 1, playerPosition) + + Util.broadcastPlaybackStatusChange(context, playerState) + Util.broadcastA2dpPlayStatusChange( + context, playerState, song, + downloader.downloadList.size + downloader.backgroundDownloadList.size, + downloader.downloadList.indexOf(currentPlaying) + 1, playerPosition + ) // Update widget - UpdateWidget(playerState, song) + 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) - nowPlayingEventDistributor.value.raiseShowNowPlayingEvent() + nowPlayingEventDistributor.raiseShowNowPlayingEvent() } } else { - nowPlayingEventDistributor.value.raiseHideNowPlayingEvent() + nowPlayingEventDistributor.raiseHideNowPlayingEvent() stopForeground(true) isInForeground = false stopIfIdle() } if (playerState === PlayerState.STARTED) { - scrobbler.scrobble(this@MediaPlayerService, currentPlaying, false) + scrobbler.scrobble(context, currentPlaying, false) } else if (playerState === PlayerState.COMPLETED) { - scrobbler.scrobble(this@MediaPlayerService, currentPlaying, true) + scrobbler.scrobble(context, currentPlaying, true) } null } } private fun setupOnSongCompletedHandler() { - localMediaPlayer!!.onSongCompleted = { currentPlaying: DownloadFile? -> - val index = downloader!!.currentPlayingIndex + localMediaPlayer.onSongCompleted = { currentPlaying: DownloadFile? -> + val index = downloader.currentPlayingIndex + val context = this@MediaPlayerService + if (currentPlaying != null) { val song = currentPlaying.song - if (song.bookmarkPosition > 0 && Util.getShouldClearBookmark(this@MediaPlayerService)) { - val musicService = getMusicService(this@MediaPlayerService) + if (song.bookmarkPosition > 0 && Util.getShouldClearBookmark(context)) { + val musicService = getMusicService(context) try { - musicService.deleteBookmark(song.id, this@MediaPlayerService) + musicService.deleteBookmark(song.id, context) } catch (ignored: Exception) { } } @@ -369,17 +426,19 @@ class MediaPlayerService : Service() { if (index != -1) { when (repeatMode) { RepeatMode.OFF -> { - if (index + 1 < 0 || index + 1 >= downloader!!.downloadList.size) { - if (Util.getShouldClearPlaylist(this@MediaPlayerService)) { + if (index + 1 < 0 || index + 1 >= downloader.downloadList.size) { + if (Util.getShouldClearPlaylist(context)) { clear(true) - jukeboxMediaPlayer.value.updatePlaylist() + jukeboxMediaPlayer.updatePlaylist() } resetPlayback() - break + } else { + play(index + 1) } - play(index + 1) } - RepeatMode.ALL -> play((index + 1) % downloader!!.downloadList.size) + RepeatMode.ALL -> { + play((index + 1) % downloader.downloadList.size) + } RepeatMode.SINGLE -> play(index) else -> { } @@ -391,13 +450,15 @@ class MediaPlayerService : Service() { @Synchronized fun clear(serialize: Boolean) { - localMediaPlayer!!.reset() - downloader!!.clear() - localMediaPlayer!!.setCurrentPlaying(null) + localMediaPlayer.reset() + downloader.clear() + localMediaPlayer.setCurrentPlaying(null) setNextPlaying() if (serialize) { - downloadQueueSerializer!!.serializeDownloadQueue(downloader!!.downloadList, - downloader!!.currentPlayingIndex, playerPosition) + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, playerPosition + ) } } @@ -408,8 +469,9 @@ class MediaPlayerService : Service() { if (currentPlaying != null) { try { val song = currentPlaying.song - val cover = FileUtil.getAlbumArtBitmap(context, song, - Util.getMinDisplayMetric(context), true + val cover = FileUtil.getAlbumArtBitmap( + context, song, + Util.getMinDisplayMetric(context), true ) metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L) metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist) @@ -427,9 +489,13 @@ class MediaPlayerService : Service() { // Create playback State val playbackState = PlaybackStateCompat.Builder() - val state = if (playerState === PlayerState.STARTED) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED + val state = if (playerState === PlayerState.STARTED) { + // If we set the playback position correctly, we can get a nice seek bar :) + PlaybackStateCompat.STATE_PLAYING + } else { + PlaybackStateCompat.STATE_PAUSED + } - // If we set the playback position correctly, we can get a nice seek bar :) playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f) // Set Active state @@ -441,31 +507,40 @@ class MediaPlayerService : Service() { 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) + + // 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 (Util.isNotificationEnabled(this)) { if (isInForeground) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)) + val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + manager.notify(NOTIFICATION_ID, notification) } else { - val notificationManager = NotificationManagerCompat.from(this) - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)) + val manager = NotificationManagerCompat.from(this) + manager.notify(NOTIFICATION_ID, notification) } - Timber.w("--- Updated notification") + Timber.v("Updated notification") } else { - startForeground(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)) + startForeground(NOTIFICATION_ID, notification) isInForeground = true - Timber.w("--- Created Foreground notification") + Timber.w("Created Foreground notification") } } } @@ -473,7 +548,11 @@ class MediaPlayerService : Service() { /** * This method builds a notification, reusing the Notification Builder if possible */ - private fun buildForegroundNotification(playerState: PlayerState, currentPlaying: DownloadFile?): Notification { + private fun buildForegroundNotification( + playerState: PlayerState, + currentPlaying: DownloadFile? + ): Notification { + // Init val context = applicationContext val song = currentPlaying?.song @@ -494,7 +573,7 @@ class MediaPlayerService : Service() { notificationBuilder!!.priority = NotificationCompat.PRIORITY_LOW // Add content intent (when user taps on notification) - notificationBuilder!!.setContentIntent(pendingIntentForContent) + notificationBuilder!!.setContentIntent(getPendingIntentForContent()) // This intent is executed when the user closes the notification notificationBuilder!!.setDeleteIntent(stopIntent) @@ -526,14 +605,19 @@ class MediaPlayerService : Service() { return notificationBuilder!!.build() } - private fun addActions(context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry?): IntArray { + private fun addActions( + context: Context, + notificationBuilder: NotificationCompat.Builder, + playerState: PlayerState, + song: MusicDirectory.Entry? + ): 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(generateStarUnstarAction(context, numActions, song.starred)) + notificationBuilder.addAction(generateStarAction(context, numActions, song.starred)) } numActions++ @@ -559,20 +643,21 @@ class MediaPlayerService : Service() { actionArray[i] = compactActionList[i] } return actionArray - //notificationBuilder.setShowActionsInCompactView()) + // 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() + 2 -> // Is handled in generatePlayPauseAction() return null 3 -> { keycode = KeyEvent.KEYCODE_MEDIA_NEXT @@ -586,15 +671,22 @@ class MediaPlayerService : Service() { } else -> return null } + val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode) return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() } - private fun generatePlayPauseAction(context: Context, requestCode: Int, playerState: PlayerState): NotificationCompat.Action { + private fun generatePlayPauseAction( + context: Context, + requestCode: Int, + playerState: PlayerState + ): NotificationCompat.Action { val isPlaying = playerState === PlayerState.STARTED - val pendingIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, requestCode) + val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + val pendingIntent = 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 @@ -602,14 +694,20 @@ class MediaPlayerService : Service() { label = getString(R.string.common_play) icon = R.drawable.media_start_large_dark } + return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() } - private fun generateStarUnstarAction(context: Context, requestCode: Int, isStarred: Boolean): NotificationCompat.Action { - val keyCode: Int + private fun generateStarAction( + context: Context, + requestCode: Int, + isStarred: Boolean + ): NotificationCompat.Action { + val label: String val icon: Int - keyCode = KeyEvent.KEYCODE_STAR + val keyCode: Int = KeyEvent.KEYCODE_STAR + if (isStarred) { label = getString(R.string.download_menu_star) icon = R.drawable.ic_star_full_dark @@ -617,29 +715,36 @@ class MediaPlayerService : Service() { label = getString(R.string.download_menu_star) icon = R.drawable.ic_star_hollow_dark } + val pendingIntent = getPendingIntentForMediaAction(context, keyCode, requestCode) return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() } - private val pendingIntentForContent: PendingIntent - private get() { - val notificationIntent = Intent(this, NavigationActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true) - return PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) - } + 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_EXTRA_NAME_SHOW_PLAYER, true) + return PendingIntent.getActivity(this, 0, intent, flags) + } - private fun getPendingIntentForMediaAction(context: Context, keycode: Int, requestCode: Int): PendingIntent { + private 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, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getBroadcast(context, requestCode, intent, flags) } private fun initMediaSessions() { mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService") mediaSessionToken = mediaSession!!.sessionToken - //mediaController = new MediaControllerCompat(getApplicationContext(), mediaSessionToken); + // mediaController = new MediaControllerCompat(getApplicationContext(), mediaSessionToken); + mediaSession!!.setCallback(object : MediaSessionCompat.Callback() { override fun onPlay() { super.onPlay() @@ -659,16 +764,11 @@ class MediaPlayerService : Service() { Timber.w("Media Session Callback: onStop") } - override fun onSeekTo(pos: Long) { - super.onSeekTo(pos) - } - 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? - val lifecycleSupport = mediaPlayerLifecycleSupport.value - lifecycleSupport.handleKeyEvent(event) + mediaPlayerLifecycleSupport.handleKeyEvent(event) return true } } @@ -681,13 +781,16 @@ class MediaPlayerService : Service() { private const val NOTIFICATION_ID = 3033 private var instance: MediaPlayerService? = null private val instanceLock = Any() + @JvmStatic fun getInstance(context: Context): MediaPlayerService? { synchronized(instanceLock) { for (i in 0..19) { if (instance != null) return instance if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(Intent(context, MediaPlayerService::class.java)) + context.startForegroundService( + Intent(context, MediaPlayerService::class.java) + ) } else { context.startService(Intent(context, MediaPlayerService::class.java)) } @@ -704,12 +807,16 @@ class MediaPlayerService : Service() { } @JvmStatic - fun executeOnStartedMediaPlayerService(context: Context, taskToExecute: Consumer) { + fun executeOnStartedMediaPlayerService( + context: Context, + taskToExecute: Consumer + ) { + val t: Thread = object : Thread() { override fun run() { val instance = getInstance(context) if (instance == null) { - Timber.e("ExecuteOnStartedMediaPlayerService failed to get a MediaPlayerService instance!") + Timber.e("ExecuteOnStarted.. failed to get a MediaPlayerService instance!") return } taskToExecute.accept(instance) @@ -718,4 +825,4 @@ class MediaPlayerService : Service() { t.start() } } -} \ No newline at end of file +} From 0ad6d0b691ed12770c4123fea63fbb4b4dd0e17c Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 20:17:03 +0200 Subject: [PATCH 24/29] Move MediaPlayerService to Kotlin dir --- .../org/moire/ultrasonic/service/MediaPlayerService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename ultrasonic/src/main/{java => kotlin}/org/moire/ultrasonic/service/MediaPlayerService.kt (99%) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt similarity index 99% rename from ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 401a1a29..d87fdaee 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -49,6 +49,7 @@ import timber.log.Timber class MediaPlayerService : Service() { private val binder: IBinder = SimpleServiceBinder(this) private val scrobbler = Scrobbler() + private val jukeboxMediaPlayer by inject() private val downloadQueueSerializer by inject() private val shufflePlayBuffer by inject() @@ -62,7 +63,7 @@ class MediaPlayerService : Service() { private var isInForeground = false private var notificationBuilder: NotificationCompat.Builder? = null - val repeatMode: RepeatMode + private val repeatMode: RepeatMode get() = Util.getRepeatMode(this) override fun onBind(intent: Intent): IBinder { From 410e1df980d64c7e238379ad14dc155c59fc6c46 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 20:51:12 +0200 Subject: [PATCH 25/29] Upgrade detekt to v1.16.0 to get Type Resolution support In course I had to modify the rules (many were renamed or dropped), and create a new baseline (many new rules added errors in old files) --- .circleci/config.yml | 4 +- .../cache/serializers/IndexesSerializer.kt | 1 + .../serializers/MusicFolderSerializer.kt | 1 + dependencies.gradle | 4 +- detekt-baseline-debug.xml | 261 ++++++++++++++++++ detekt-baseline-main.xml | 55 ++++ detekt-baseline-release.xml | 261 ++++++++++++++++++ detekt-config.yml | 35 +-- gradle_scripts/code_quality.gradle | 10 +- 9 files changed, 596 insertions(+), 36 deletions(-) create mode 100644 detekt-baseline-debug.xml create mode 100644 detekt-baseline-main.xml create mode 100644 detekt-baseline-release.xml diff --git a/.circleci/config.yml b/.circleci/config.yml index c9f040ad..6ef7ac08 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -version: 2 +version: 3 jobs: build: docker: @@ -18,7 +18,7 @@ jobs: command: ./gradlew -Pqc ktlintCheck - run: name: static analysis - command: ./gradlew -Pqc detektCheck + command: ./gradlew -Pqc detektMain - run: name: build command: ./gradlew assembleDebug diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt index b8c2e312..9683bd25 100644 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt +++ b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt @@ -25,6 +25,7 @@ private val indexesSerializer get() = object : ObjectSerializer(SERIALI .writeObject>(context, item.artists, artistListSerializer) } + @Suppress("ReturnCount") override fun deserializeObject( context: SerializationContext, input: SerializerInput, diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt index d818aed0..0af30335 100644 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt +++ b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt @@ -22,6 +22,7 @@ private val musicFolderSerializer = object : ObjectSerializer(SERIA output.writeString(item.id).writeString(item.name) } + @Suppress("ReturnCount") override fun deserializeObject( context: SerializationContext, input: SerializerInput, diff --git a/dependencies.gradle b/dependencies.gradle index 074b77a4..e99ed16e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -9,7 +9,7 @@ ext.versions = [ androidxcore : "1.5.0-rc01", ktlint : "0.37.1", ktlintGradle : "9.2.1", - detekt : "1.0.0.RC6-4", + detekt : "1.16.0", jacoco : "0.8.5", preferences : "1.1.1", media : "1.3.0", @@ -49,7 +49,7 @@ ext.gradlePlugins = [ gradle : "com.android.tools.build:gradle:$versions.gradlePlugin", kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin", ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle", - detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt", + detekt : "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt", jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco", ] diff --git a/detekt-baseline-debug.xml b/detekt-baseline-debug.xml new file mode 100644 index 00000000..fff5d6ce --- /dev/null +++ b/detekt-baseline-debug.xml @@ -0,0 +1,261 @@ + + + + + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun finishActivity() + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun setFields() + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun testConnection() + CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() + CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNumberedFile(next: Boolean) + CommentOverPrivateFunction:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification + CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun search2( criteria: SearchCriteria ): SearchResult + CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun searchOld( criteria: SearchCriteria ): SearchResult + CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int) + CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun editServer(index: Int) + CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun onServerDeleted(index: Int) + CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun setActiveServer(index: Int) + CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting? + CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun areIndexesMissing(): Boolean + CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun reindexSettings() + ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background + ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!isOffline(activity) && isArtist && Util.getShouldUseId3Tags(activity) + ComplexCondition:EditServerFragment.kt$EditServerFragment$urlString != urlString.trim(' ') || urlString.contains("@") || url.host.isNullOrBlank() + ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt" + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED ) + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$playerState !== PlayerState.IDLE && playerState !== PlayerState.DOWNLOADING && playerState !== PlayerState.PREPARING + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000 + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState === PlayerState.PREPARED + ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.IDLE || localMediaPlayer.playerState === PlayerState.DOWNLOADING || localMediaPlayer.playerState === PlayerState.PREPARING + ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.PAUSED || localMediaPlayer.playerState === PlayerState.COMPLETED || localMediaPlayer.playerState === PlayerState.STOPPED + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !deleteEnabled && !isOffline(context) + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !isOffline(context) && selection.size > pinnedCount + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$entry != null && !entry.isDirectory && !entry.isVideo + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$Util.getShouldShowAllSongsByArtist(context) && musicDirectory.findChild(allSongsId) == null && musicDirectory.getChildren(true, false).size == musicDirectory.getChildren(true, true).size + ComplexCondition:ServerSettingsModel.kt$ServerSettingsModel$url.isNullOrEmpty() || userName.isNullOrEmpty() || isMigrated + ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo && Util.getVideoPlayerType(this.context) !== VideoPlayerType.FLASH + ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$id != null && view != null && view is ImageView + ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$username != null && view != null && view is ImageView + ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String + ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() + ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean + ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean + ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) + ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) + ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) + ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() + ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>) + ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? + ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) + ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) + ComplexMethod:SongView.kt$SongView$public override fun update() + EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ } + EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$() + EmptyFunctionBlock:SongView.kt$SongView${} + FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent() + ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song) + ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song) + ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song) + 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) + 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) + ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$String.format("%s\n\n%s", Util.getShareGreeting(context), result.url) + ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber) + ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate) + ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix) + LargeClass:DownloadFile.kt$DownloadFile + LargeClass:DownloadHandler.kt$DownloadHandler + LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler + LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter + LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer + LargeClass:MediaPlayerService.kt$MediaPlayerService : Service + LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity + LargeClass:RESTMusicService.kt$RESTMusicService : MusicService + LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment + LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment$LoadTask : FragmentBackgroundTask + LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment + LargeClass:ServerSettingsModel.kt$ServerSettingsModel : ViewModel + LargeClass:SongView.kt$SongView : UpdateViewCheckable + LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry + LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting + LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) + LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) + LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String + LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File) + LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() + LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsForArtist( id: String, songs: MutableCollection<MusicDirectory.Entry> ) + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList<MusicDirectory.Entry> ) + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): List<MusicDirectory.Entry> + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$override fun done(songs: List<MusicDirectory.Entry>) + LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity() + LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean + LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle) + LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?) + LongMethod:EditServerFragment.kt$EditServerFragment.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): Boolean + LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) + LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder() + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List<File>): LinkedList<FileListItem> + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList<FileListItem> + LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context) + LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState) + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init() + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release() + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$override fun onCompletion(mediaPlayer: MediaPlayer) + LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification + LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying() + LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) + LongMethod:MediaStoreService.kt$MediaStoreService$fun saveInMediaStore(downloadFile: DownloadFile) + LongMethod:NavigationActivity.kt$NavigationActivity$// TODO Test if this works with external Intents // android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here override fun onNewIntent(intent: Intent?) + LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) + LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying() + LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, context: Context, playlist: MusicDirectory ) + LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun done(result: Pair<MusicDirectory, Boolean>) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun load(service: MusicService): MusicDirectory + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected fun createHeader( entries: List<MusicDirectory.Entry>, name: CharSequence?, songCount: Int ): View? + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>) + LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean + LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? + LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int) + LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean + LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting? + LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) + LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry) + LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) + LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) + LongMethod:SongView.kt$SongView$public override fun update() + LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable) + LongMethod:UApp.kt$UApp$override fun onCreate() + LongParameterList:ArtistRowAdapter.kt$ArtistRowAdapter$( private var artistList: List<Artist>, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -> Unit, val onContextMenuClick: (MenuItem, Artist) -> Boolean, private val imageLoader: ImageLoader ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( 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 ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String?, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean ) + 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:AudioFocusHandler.kt$AudioFocusHandler$0.1f + MagicNumber:DownloadFile.kt$DownloadFile$100 + MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10 + MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$1000L + MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60 + MagicNumber:DownloadHandler.kt$DownloadHandler$500 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$100 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$3 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$4 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$5 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$6 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$7 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$100 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$1000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$1000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1000L + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L + 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:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$50L + MagicNumber:MediaPlayerService.kt$MediaPlayerService$100 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$1000 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$256 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$3 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$4 + MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$19 + MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L + MagicNumber:RESTMusicService.kt$RESTMusicService$206 + MagicNumber:RESTMusicService.kt$RESTMusicService$5 + MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10 + MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$10 + MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10 + MagicNumber:SongView.kt$SongView$3 + MagicNumber:SongView.kt$SongView$4 + MagicNumber:SongView.kt$SongView$60 + 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() + NestedBlockDepth:SelectAlbumFragment.kt$SelectAlbumFragment$private fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?) + ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting + ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String + ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() + ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? + ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean + ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions) + SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) } + SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) } + SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower } + SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { } + SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored } + SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() } + ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>) + TooGenericExceptionCaught:ArtistListModel.kt$ArtistListModel$exception: Exception + TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception + TooGenericExceptionCaught:DownloadFile.kt$DownloadFile.DownloadTask$x: Exception + TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable + TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$e: 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 + TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception + TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException + TooGenericExceptionCaught:SelectAlbumFragment.kt$SelectAlbumFragment$exception: Exception + TooGenericExceptionCaught:SongView.kt$SongView$e: Exception + TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable + TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception + TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song)) + TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer + TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service + TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService + TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment + TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt" + UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder" + UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty() + UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree.Companion$fileList.isNullOrEmpty() + UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler + UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle + VariableNaming:SelectMusicFolderView.kt$SelectMusicFolderView$private val MENU_GROUP_MUSIC_FOLDER = 10 + + diff --git a/detekt-baseline-main.xml b/detekt-baseline-main.xml new file mode 100644 index 00000000..5d93fbb0 --- /dev/null +++ b/detekt-baseline-main.xml @@ -0,0 +1,55 @@ + + + + + ComplexCondition:SubsonicAPIClient.kt$SubsonicAPIClient$contentType != null && contentType.type().equals("application", true) && contentType.subtype().equals("json", true) + ComplexMethod:AlbumListType.kt$AlbumListType.Companion$@JvmStatic fun fromName(typeName: String): AlbumListType + ComplexMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions + ComplexMethod:SubsonicError.kt$SubsonicError.Companion$fun getError(code: Int, message: String) + EmptyFunctionBlock:SubsonicAPIClient.kt$SubsonicAPIClient.<no name provided>${} + LargeClass:ApiVersionCheckWrapper.kt$ApiVersionCheckWrapper : SubsonicAPIDefinition + LargeClass:SubsonicAPIDefinition.kt$SubsonicAPIDefinition + LongMethod:SubsonicAPIClient.kt$SubsonicAPIClient$private inline fun handleStreamResponse(apiCall: () -> Response<ResponseBody>): StreamResponse + LongMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions + MagicNumber:PasswordExt.kt$0xFF + MagicNumber:PasswordExt.kt$4 + MagicNumber:PasswordMD5Interceptor.kt$PasswordMD5Interceptor$16 + MagicNumber:StreamResponse.kt$StreamResponse$200 + MagicNumber:StreamResponse.kt$StreamResponse$300 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$10 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$11 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$12 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$13 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$14 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$15 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$16 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$3 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$4 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$5 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$6 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$7 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$8 + MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$9 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$10 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$20 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$30 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$40 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$41 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$50 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$60 + MagicNumber:SubsonicError.kt$SubsonicError.Companion$70 + MagicNumber:SubsonicError.kt$SubsonicError.IncompatibleClientProtocolVersion$20 + MagicNumber:SubsonicError.kt$SubsonicError.IncompatibleServerProtocolVersion$30 + MagicNumber:SubsonicError.kt$SubsonicError.RequestedDataWasNotFound$70 + MagicNumber:SubsonicError.kt$SubsonicError.RequiredParamMissing$10 + MagicNumber:SubsonicError.kt$SubsonicError.TokenAuthNotSupportedForLDAP$41 + MagicNumber:SubsonicError.kt$SubsonicError.TrialPeriodIsOver$60 + MagicNumber:SubsonicError.kt$SubsonicError.UserNotAuthorizedForOperation$50 + MagicNumber:SubsonicError.kt$SubsonicError.WrongUsernameOrPassword$40 + ReturnCount:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions + SwallowedException:VersionAwareJacksonConverterFactory.kt$VersionAwareJacksonConverterFactory.VersionAwareResponseBodyConverter$catch (e: IllegalArgumentException) { // no-op } + ThrowsCount:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions + TooManyFunctions:ApiVersionCheckWrapper.kt$ApiVersionCheckWrapper : SubsonicAPIDefinition + UnusedPrivateMember:AlbumListType.kt$AlbumListType.Companion$private operator fun String.contains(other: String) + + diff --git a/detekt-baseline-release.xml b/detekt-baseline-release.xml new file mode 100644 index 00000000..fff5d6ce --- /dev/null +++ b/detekt-baseline-release.xml @@ -0,0 +1,261 @@ + + + + + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun finishActivity() + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun setFields() + CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun testConnection() + CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() + CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNumberedFile(next: Boolean) + CommentOverPrivateFunction:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification + CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun search2( criteria: SearchCriteria ): SearchResult + CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun searchOld( criteria: SearchCriteria ): SearchResult + CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int) + CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun editServer(index: Int) + CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun onServerDeleted(index: Int) + CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun setActiveServer(index: Int) + CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting? + CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun areIndexesMissing(): Boolean + CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun reindexSettings() + ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background + ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!isOffline(activity) && isArtist && Util.getShouldUseId3Tags(activity) + ComplexCondition:EditServerFragment.kt$EditServerFragment$urlString != urlString.trim(' ') || urlString.contains("@") || url.host.isNullOrBlank() + ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt" + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED ) + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$playerState !== PlayerState.IDLE && playerState !== PlayerState.DOWNLOADING && playerState !== PlayerState.PREPARING + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000 + ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState === PlayerState.PREPARED + ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.IDLE || localMediaPlayer.playerState === PlayerState.DOWNLOADING || localMediaPlayer.playerState === PlayerState.PREPARING + ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.PAUSED || localMediaPlayer.playerState === PlayerState.COMPLETED || localMediaPlayer.playerState === PlayerState.STOPPED + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !deleteEnabled && !isOffline(context) + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !isOffline(context) && selection.size > pinnedCount + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$entry != null && !entry.isDirectory && !entry.isVideo + ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$Util.getShouldShowAllSongsByArtist(context) && musicDirectory.findChild(allSongsId) == null && musicDirectory.getChildren(true, false).size == musicDirectory.getChildren(true, true).size + ComplexCondition:ServerSettingsModel.kt$ServerSettingsModel$url.isNullOrEmpty() || userName.isNullOrEmpty() || isMigrated + ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo && Util.getVideoPlayerType(this.context) !== VideoPlayerType.FLASH + ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$id != null && view != null && view is ImageView + ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$username != null && view != null && view is ImageView + ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String + ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() + ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean + ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean + ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) + ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) + ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) + ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() + ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) + ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>) + ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? + ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) + ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) + ComplexMethod:SongView.kt$SongView$public override fun update() + EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ } + EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$() + EmptyFunctionBlock:SongView.kt$SongView${} + FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent() + ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song) + ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song) + ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song) + 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) + 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) + ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$String.format("%s\n\n%s", Util.getShareGreeting(context), result.url) + ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber) + ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate) + ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix) + LargeClass:DownloadFile.kt$DownloadFile + LargeClass:DownloadHandler.kt$DownloadHandler + LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler + LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter + LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer + LargeClass:MediaPlayerService.kt$MediaPlayerService : Service + LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity + LargeClass:RESTMusicService.kt$RESTMusicService : MusicService + LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment + LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment$LoadTask : FragmentBackgroundTask + LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment + LargeClass:ServerSettingsModel.kt$ServerSettingsModel : ViewModel + LargeClass:SongView.kt$SongView : UpdateViewCheckable + LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry + LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting + LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) + LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) + LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String + LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File) + LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() + LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsForArtist( id: String, songs: MutableCollection<MusicDirectory.Entry> ) + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList<MusicDirectory.Entry> ) + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): List<MusicDirectory.Entry> + LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$override fun done(songs: List<MusicDirectory.Entry>) + LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity() + LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean + LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle) + LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?) + LongMethod:EditServerFragment.kt$EditServerFragment.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): Boolean + LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) + LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder() + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List<File>): LinkedList<FileListItem> + LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList<FileListItem> + LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context) + LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState) + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init() + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release() + LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$override fun onCompletion(mediaPlayer: MediaPlayer) + LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification + LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying() + LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() + LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) + LongMethod:MediaStoreService.kt$MediaStoreService$fun saveInMediaStore(downloadFile: DownloadFile) + LongMethod:NavigationActivity.kt$NavigationActivity$// TODO Test if this works with external Intents // android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here override fun onNewIntent(intent: Intent?) + LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) + LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying() + LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, context: Context, playlist: MusicDirectory ) + LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun done(result: Pair<MusicDirectory, Boolean>) + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun load(service: MusicService): MusicDirectory + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected fun createHeader( entries: List<MusicDirectory.Entry>, name: CharSequence?, songCount: Int ): View? + LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>) + LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean + LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? + LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int) + LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean + LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting? + LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) + LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry) + LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) + LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) + LongMethod:SongView.kt$SongView$public override fun update() + LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable) + LongMethod:UApp.kt$UApp$override fun onCreate() + LongParameterList:ArtistRowAdapter.kt$ArtistRowAdapter$( private var artistList: List<Artist>, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -> Unit, val onContextMenuClick: (MenuItem, Artist) -> Boolean, private val imageLoader: ImageLoader ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( 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 ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean ) + LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String?, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean ) + 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:AudioFocusHandler.kt$AudioFocusHandler$0.1f + MagicNumber:DownloadFile.kt$DownloadFile$100 + MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10 + MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$1000L + MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60 + MagicNumber:DownloadHandler.kt$DownloadHandler$500 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$100 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$3 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$4 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$5 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$6 + MagicNumber:FileLoggerTree.kt$FileLoggerTree$7 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$100 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$1000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$1000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000 + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1000L + MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L + 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:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$50L + MagicNumber:MediaPlayerService.kt$MediaPlayerService$100 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$1000 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$256 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$3 + MagicNumber:MediaPlayerService.kt$MediaPlayerService$4 + MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$19 + MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L + MagicNumber:RESTMusicService.kt$RESTMusicService$206 + MagicNumber:RESTMusicService.kt$RESTMusicService$5 + MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10 + MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$10 + MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10 + MagicNumber:SongView.kt$SongView$3 + MagicNumber:SongView.kt$SongView$4 + MagicNumber:SongView.kt$SongView$60 + 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() + NestedBlockDepth:SelectAlbumFragment.kt$SelectAlbumFragment$private fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?) + ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting + ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String + ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() + ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? + ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? + ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean + ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions) + SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) } + SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) } + SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower } + SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { } + SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored } + SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() } + ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>) + TooGenericExceptionCaught:ArtistListModel.kt$ArtistListModel$exception: Exception + TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception + TooGenericExceptionCaught:DownloadFile.kt$DownloadFile.DownloadTask$x: Exception + TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable + TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$e: 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 + TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception + TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException + TooGenericExceptionCaught:SelectAlbumFragment.kt$SelectAlbumFragment$exception: Exception + TooGenericExceptionCaught:SongView.kt$SongView$e: Exception + TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable + TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception + TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song)) + TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer + TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service + TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService + TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment + TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt" + UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder" + UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty() + UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree.Companion$fileList.isNullOrEmpty() + UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler + UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle + VariableNaming:SelectMusicFolderView.kt$SelectMusicFolderView$private val MENU_GROUP_MUSIC_FOLDER = 10 + + diff --git a/detekt-config.yml b/detekt-config.yml index 4674fd6a..34c3446c 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -1,6 +1,3 @@ -autoCorrect: true -failFast: false - build: maxIssues: 0 weights: @@ -42,26 +39,24 @@ complexity: LongMethod: threshold: 20 LongParameterList: - threshold: 5 + functionThreshold: 5 + constructorThreshold: 5 LargeClass: threshold: 150 ComplexMethod: threshold: 10 TooManyFunctions: - threshold: 20 + thresholdInFiles: 20 + thresholdInClasses: 20 + thresholdInInterfaces: 20 ComplexCondition: threshold: 3 LabeledExpression: active: false -code-smell: - active: true - FeatureEnvy: - threshold: 0.5 - weight: 0.45 - base: 0.5 formatting: + autoCorrect: true active: false style: @@ -78,17 +73,10 @@ style: maxLineLength: 120 excludePackageStatements: false excludeImportStatements: false - NamingConventionViolation: - active: true - variablePattern: '^(_)?[a-z$][a-zA-Z$0-9]*$' - constantPattern: '^([A-Z_]*|serialVersionUID)$' - methodPattern: '^[a-z\s`$][a-zA-Z\s$0-9`]*$' - classPattern: '[A-Z$][a-zA-Z$]*' - enumEntryPattern: '^[A-Z$][a-zA-Z_$0-9]*$' comments: active: true - CommentOverPrivateMethod: + CommentOverPrivateFunction: active: true CommentOverPrivateProperty: active: true @@ -99,12 +87,3 @@ comments: searchInInnerInterface: true UndocumentedPublicFunction: active: false - -# *experimental feature* -# Migration rules can be defined in the same config file or a new one -migration: - active: false - imports: - # your.package.Class: new.package.or.Class - # for example: - # io.gitlab.arturbosch.detekt.api.Rule: io.gitlab.arturbosch.detekt.rule.Rule diff --git a/gradle_scripts/code_quality.gradle b/gradle_scripts/code_quality.gradle index 9b9a5308..b4c49e1b 100644 --- a/gradle_scripts/code_quality.gradle +++ b/gradle_scripts/code_quality.gradle @@ -20,11 +20,13 @@ if (isCodeQualityEnabled) { apply plugin: "io.gitlab.arturbosch.detekt" detekt { - version = versions.detekt - profile("main") { - config = "${rootProject.projectDir}/detekt-config.yml" - } + buildUponDefaultConfig = true + toolVersion = versions.detekt + baseline = file("${rootProject.projectDir}/detekt-baseline.xml") + config = files("${rootProject.projectDir}/detekt-config.yml") } } + tasks.detekt.jvmTarget = "1.8" } + } From 8004bc31fef66c23cece3114ec2065cb02719a6e Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 22 Apr 2021 20:58:15 +0200 Subject: [PATCH 26/29] Add PlaybackState actions .. and map PlayerStates more precisely. --- .../ultrasonic/service/MediaPlayerService.kt | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index d87fdaee..522a8264 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -323,6 +323,7 @@ class MediaPlayerService : Service() { private fun setupOnCurrentPlayingChangedHandler() { localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> + Timber.w("Calling onCurrentPlayingChanged") if (currentPlaying != null) { Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song) @@ -363,10 +364,13 @@ class MediaPlayerService : Service() { currentPlaying: DownloadFile? -> + Timber.w("Calling onPlayerStateChanged") + val context = this@MediaPlayerService // Notify MediaSession updateMediaSession(currentPlaying, playerState) + if (playerState === PlayerState.PAUSED) { downloadQueueSerializer.serializeDownloadQueue( downloader.downloadList, downloader.currentPlayingIndex, playerPosition @@ -388,6 +392,7 @@ class MediaPlayerService : Service() { // 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) { @@ -400,17 +405,20 @@ class MediaPlayerService : Service() { isInForeground = false stopIfIdle() } + if (playerState === PlayerState.STARTED) { scrobbler.scrobble(context, currentPlaying, false) } else if (playerState === PlayerState.COMPLETED) { scrobbler.scrobble(context, currentPlaying, true) } + null } } private fun setupOnSongCompletedHandler() { localMediaPlayer.onSongCompleted = { currentPlaying: DownloadFile? -> + Timber.w("Calling onSongCompleted") val index = downloader.currentPlayingIndex val context = this@MediaPlayerService @@ -464,6 +472,7 @@ class MediaPlayerService : Service() { } private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) { + Timber.w("Updating the MediaSession") // Set Metadata val metadata = MediaMetadataCompat.Builder() val context = applicationContext @@ -490,20 +499,48 @@ class MediaPlayerService : Service() { // Create playback State val playbackState = PlaybackStateCompat.Builder() - val state = if (playerState === PlayerState.STARTED) { - // If we set the playback position correctly, we can get a nice seek bar :) - PlaybackStateCompat.STATE_PLAYING - } else { - PlaybackStateCompat.STATE_PAUSED + val state: Int + val isPlaying = (playerState === PlayerState.STARTED) + + var actions: Long = PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + + // Map our playerState to native PlaybackState + // TODO: Synchronize these APIs + when (playerState) { + PlayerState.STARTED -> { + state = PlaybackStateCompat.STATE_PLAYING + actions = actions or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_STOP + } + PlayerState.COMPLETED, + PlayerState.STOPPED -> { + state = PlaybackStateCompat.STATE_STOPPED + } + PlayerState.IDLE -> { + state = PlaybackStateCompat.STATE_NONE + actions = 0L + } + PlayerState.PAUSED -> { + state = PlaybackStateCompat.STATE_PAUSED + actions = actions or PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_STOP + } + else -> state = PlaybackStateCompat.STATE_PAUSED } playbackState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f) - // Set Active state - mediaSession!!.isActive = playerState === PlayerState.STARTED + // Set actions + playbackState.setActions(actions) // Save the playback state mediaSession!!.setPlaybackState(playbackState.build()) + + // Set Active state + mediaSession!!.isActive = isPlaying + + Timber.w("Current controller: %s", mediaSession!!.remoteControlClient) + Timber.w("Setting the MediaSession to active = %s", isPlaying) } private fun createNotificationChannel() { From cd27734c04924f21cb64901c76e5fef8a9130601 Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 23 Apr 2021 11:52:08 +0200 Subject: [PATCH 27/29] Emulate keycode for the Session callbacks --- .../ultrasonic/service/MediaPlayerService.kt | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 522a8264..e19f3bad 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -73,7 +73,6 @@ class MediaPlayerService : Service() { override fun onCreate() { super.onCreate() - initMediaSessions() downloader.onCreate() shufflePlayBuffer.onCreate() localMediaPlayer.init() @@ -115,7 +114,8 @@ class MediaPlayerService : Service() { localMediaPlayer.release() downloader.stop() shufflePlayBuffer.onDestroy() - mediaSession!!.release() + mediaSession?.release() + mediaSession == null } catch (ignored: Throwable) { } Timber.i("MediaPlayerService stopped") @@ -473,6 +473,9 @@ class MediaPlayerService : Service() { private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) { Timber.w("Updating the MediaSession") + + if (mediaSession == null) initMediaSessions() + // Set Metadata val metadata = MediaMetadataCompat.Builder() val context = applicationContext @@ -502,16 +505,19 @@ class MediaPlayerService : Service() { val state: Int val isPlaying = (playerState === PlayerState.STARTED) - var actions: Long = PlaybackStateCompat.ACTION_PLAY_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + var actions: Long = PlaybackStateCompat.ACTION_PLAY_PAUSE +// or +// PlaybackStateCompat.ACTION_SKIP_TO_NEXT or +// PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS // Map our playerState to native PlaybackState // TODO: Synchronize these APIs when (playerState) { PlayerState.STARTED -> { state = PlaybackStateCompat.STATE_PLAYING - actions = actions or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_STOP + actions = actions or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_STOP } PlayerState.COMPLETED, PlayerState.STOPPED -> { @@ -523,7 +529,9 @@ class MediaPlayerService : Service() { } PlayerState.PAUSED -> { state = PlaybackStateCompat.STATE_PAUSED - actions = actions or PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_STOP + actions = actions or + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_STOP } else -> state = PlaybackStateCompat.STATE_PAUSED } @@ -619,7 +627,10 @@ class MediaPlayerService : Service() { // Use the Media Style, to enable native Android support for playback notification val style = androidx.media.app.NotificationCompat.MediaStyle() - style.setMediaSession(mediaSessionToken) + + if (mediaSessionToken != null) { + style.setMediaSession(mediaSessionToken) + } // Clear old actions notificationBuilder!!.clearActions() @@ -779,6 +790,11 @@ class MediaPlayerService : Service() { } private fun initMediaSessions() { + @Suppress("MagicNumber") + val keycode = 110 + + Timber.w("Creating media session") + mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService") mediaSessionToken = mediaSession!!.sessionToken // mediaController = new MediaControllerCompat(getApplicationContext(), mediaSessionToken); @@ -786,19 +802,33 @@ class MediaPlayerService : Service() { mediaSession!!.setCallback(object : MediaSessionCompat.Callback() { override fun onPlay() { super.onPlay() - play() + + getPendingIntentForMediaAction( + applicationContext, + KeyEvent.KEYCODE_MEDIA_PLAY, + keycode + ).send() + Timber.w("Media Session Callback: onPlay") } override fun onPause() { super.onPause() - pause() + getPendingIntentForMediaAction( + applicationContext, + KeyEvent.KEYCODE_MEDIA_PAUSE, + keycode + ).send() Timber.w("Media Session Callback: onPause") } override fun onStop() { super.onStop() - stop() + getPendingIntentForMediaAction( + applicationContext, + KeyEvent.KEYCODE_MEDIA_STOP, + keycode + ).send() Timber.w("Media Session Callback: onStop") } From 0a170918c83fa13ddb780f9ba4acf7ec4b6f6532 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 25 Apr 2021 12:24:35 +0200 Subject: [PATCH 28/29] Adjust Timber debug level --- .../ultrasonic/service/MediaPlayerService.kt | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index e19f3bad..9f8d8578 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -323,7 +323,6 @@ class MediaPlayerService : Service() { private fun setupOnCurrentPlayingChangedHandler() { localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? -> - Timber.w("Calling onCurrentPlayingChanged") if (currentPlaying != null) { Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song) @@ -364,8 +363,6 @@ class MediaPlayerService : Service() { currentPlaying: DownloadFile? -> - Timber.w("Calling onPlayerStateChanged") - val context = this@MediaPlayerService // Notify MediaSession @@ -418,7 +415,6 @@ class MediaPlayerService : Service() { private fun setupOnSongCompletedHandler() { localMediaPlayer.onSongCompleted = { currentPlaying: DownloadFile? -> - Timber.w("Calling onSongCompleted") val index = downloader.currentPlayingIndex val context = this@MediaPlayerService @@ -472,7 +468,7 @@ class MediaPlayerService : Service() { } private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) { - Timber.w("Updating the MediaSession") + Timber.d("Updating the MediaSession") if (mediaSession == null) initMediaSessions() @@ -547,8 +543,7 @@ class MediaPlayerService : Service() { // Set Active state mediaSession!!.isActive = isPlaying - Timber.w("Current controller: %s", mediaSession!!.remoteControlClient) - Timber.w("Setting the MediaSession to active = %s", isPlaying) + Timber.d("Setting the MediaSession to active = %s", isPlaying) } private fun createNotificationChannel() { @@ -586,7 +581,7 @@ class MediaPlayerService : Service() { } else { startForeground(NOTIFICATION_ID, notification) isInForeground = true - Timber.w("Created Foreground notification") + Timber.v("Created Foreground notification") } } } @@ -809,7 +804,7 @@ class MediaPlayerService : Service() { keycode ).send() - Timber.w("Media Session Callback: onPlay") + Timber.v("Media Session Callback: onPlay") } override fun onPause() { @@ -819,7 +814,7 @@ class MediaPlayerService : Service() { KeyEvent.KEYCODE_MEDIA_PAUSE, keycode ).send() - Timber.w("Media Session Callback: onPause") + Timber.v("Media Session Callback: onPause") } override fun onStop() { @@ -829,7 +824,7 @@ class MediaPlayerService : Service() { KeyEvent.KEYCODE_MEDIA_STOP, keycode ).send() - Timber.w("Media Session Callback: onStop") + Timber.v("Media Session Callback: onStop") } override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { From 4f70c61592943ef12a979947912a54d35a3658ee Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 27 Apr 2021 10:04:35 +0200 Subject: [PATCH 29/29] Fix Magic Number problem in DownloadFile (caused by #431). --- .../kotlin/org/moire/ultrasonic/service/DownloadFile.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 2f486196..a55caa0d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -45,7 +45,7 @@ class DownloadFile( private val mediaStoreService: MediaStoreService private var downloadTask: CancellableTask? = null var isFailed = false - private var retryCount = 5 + private var retryCount = MAX_RETRIES private val desiredBitRate: Int = Util.getMaxBitRate(context) @@ -382,4 +382,8 @@ class DownloadFile( } } } + + companion object { + const val MAX_RETRIES = 5 + } }