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 782e63eb..e99ed16e 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", + detekt : "1.16.0", jacoco : "0.8.5", preferences : "1.1.1", + media : "1.3.0", androidSupport : "28.0.0", androidLegacySupport : "1.0.0", @@ -48,12 +49,12 @@ 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", ] 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/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 bc94cdbe..34c3446c 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -1,9 +1,5 @@ -autoCorrect: true -failFast: false - build: - warningThreshold: 0 - failThreshold: 0 + maxIssues: 0 weights: complexity: 2 formatting: 1 @@ -43,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: @@ -71,7 +65,7 @@ style: active: true ForbiddenComment: active: true - values: 'TODO:,FIXME:,STOPSHIP:' + values: 'FIXME:,STOPSHIP:' WildcardImport: active: true MaxLineLength: @@ -79,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 @@ -100,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" } + } 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 diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java deleted file mode 100644 index b625067b..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java +++ /dev/null @@ -1,88 +0,0 @@ -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; - - // TODO: This is a circular reference, try to remove it - private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private Context context; - - public AudioFocusHandler(Context context) - { - this.context = context; - } - - 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(); - } - } - } - 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"); - } - } -} 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/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..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) { @@ -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 deleted file mode 100644 index c7ae0cf7..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ /dev/null @@ -1,703 +0,0 @@ -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 timber.log.Timber; -import android.view.View; -import android.widget.RemoteViews; - -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; -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 kotlin.Lazy; - -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; - -/** - * 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; - - private static MediaPlayerService instance = null; - private static final Object instanceLock = new Object(); - - 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 LocalMediaPlayer localMediaPlayer; - private Downloader downloader; - private ShufflePlayBuffer shufflePlayBuffer; - private DownloadQueueSerializer downloadQueueSerializer; - - 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; - } - } - - 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(); - - downloader.onCreate(); - shufflePlayBuffer.onCreate(); - - localMediaPlayer.init(); - setupOnCurrentPlayingChangedHandler(); - 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(); - } - }; - - // 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); - } - - // 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"); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) - { - super.onStartCommand(intent, flags, startId); - return START_NOT_STICKY; - } - - @Override - public void onDestroy() - { - super.onDestroy(); - - instance = null; - - try { - localMediaPlayer.release(); - downloader.stop(); - shufflePlayBuffer.onDestroy(); - } catch (Throwable ignored) { - } - - Timber.i("MediaPlayerService stopped"); - } - - private void 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(); - } - } - - public synchronized void seekTo(int position) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().skip(downloader.getCurrentPlayingIndex(), position / 1000); - } - else - { - localMediaPlayer.seekTo(position); - } - } - - public synchronized int getPlayerPosition() - { - if (localMediaPlayer.playerState == IDLE || localMediaPlayer.playerState == DOWNLOADING || localMediaPlayer.playerState == PREPARING) - { - return 0; - } - - return jukeboxMediaPlayer.getValue().isEnabled() ? jukeboxMediaPlayer.getValue().getPositionSeconds() * 1000 : - localMediaPlayer.getPlayerPosition(); - } - - public synchronized int getPlayerDuration() - { - return localMediaPlayer.getPlayerDuration(); - } - - public synchronized void setCurrentPlaying(int currentPlayingIndex) - { - try - { - localMediaPlayer.setCurrentPlaying(downloader.downloadList.get(currentPlayingIndex)); - } - catch (IndexOutOfBoundsException x) - { - // Ignored - } - } - - 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); - } - - // 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); - localMediaPlayer.clearRemoteControl(); - isInForeground = false; - stopIfIdle(); - } - } - }; - } - - public synchronized void setNextPlaying() - { - boolean 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; - } - } - - localMediaPlayer.clearNextPlaying(false); - - if (index < downloader.downloadList.size() && index != -1) - { - localMediaPlayer.setNextPlaying(downloader.downloadList.get(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(); - } - } - - public synchronized void resumeOrPlay() - { - if (localMediaPlayer.playerState == PAUSED || localMediaPlayer.playerState == COMPLETED || localMediaPlayer.playerState == STOPPED) - { - start(); - } - else if (localMediaPlayer.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); - } - } - - public synchronized void play(int index) - { - 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)); - } - } - - downloader.checkDownloads(); - setNextPlaying(); - } - } - - private synchronized void resetPlayback() - { - localMediaPlayer.reset(); - localMediaPlayer.setCurrentPlaying(null); - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, - downloader.getCurrentPlayingIndex(), getPlayerPosition()); - } - - public synchronized void pause() - { - if (localMediaPlayer.playerState == STARTED) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().stop(); - } - else - { - localMediaPlayer.pause(); - } - localMediaPlayer.setPlayerState(PAUSED); - } - } - - public synchronized void stop() - { - if (localMediaPlayer.playerState == STARTED) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().stop(); - } - else - { - localMediaPlayer.pause(); - } - } - localMediaPlayer.setPlayerState(STOPPED); - } - - public synchronized void start() - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().start(); - } - else - { - localMediaPlayer.start(); - } - localMediaPlayer.setPlayerState(STARTED); - } - - public void setupOnPlayerStateChangedHandler() - { - localMediaPlayer.onPlayerStateChanged = new BiConsumer() { - @Override - public void accept(PlayerState playerState, DownloadFile currentPlaying) { - 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 - 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); - localMediaPlayer.clearRemoteControl(); - isInForeground = false; - stopIfIdle(); - } - - if (playerState == STARTED) - { - scrobbler.scrobble(MediaPlayerService.this, currentPlaying, false); - } - else if (playerState == COMPLETED) - { - scrobbler.scrobble(MediaPlayerService.this, currentPlaying, true); - } - } - }; - } - - private void setupOnSongCompletedHandler() - { - localMediaPlayer.onSongCompleted = new Consumer() { - @Override - public void accept(DownloadFile currentPlaying) { - int index = downloader.getCurrentPlayingIndex(); - - if (currentPlaying != null) - { - final MusicDirectory.Entry song = currentPlaying.getSong(); - - if (song != null && 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; - } - } - } - }; - } - - public synchronized void clear(boolean serialize) - { - localMediaPlayer.reset(); - downloader.clear(); - localMediaPlayer.setCurrentPlaying(null); - - setNextPlaying(); - - if (serialize) { - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, - downloader.getCurrentPlayingIndex(), getPlayerPosition()); - } - } - - public void updateNotification(PlayerState playerState, DownloadFile currentPlaying) - { - 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)); - } - else { - final NotificationManagerCompat notificationManager = - NotificationManagerCompat.from(this); - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); - } - Timber.w("--- Updated notification"); - } - else { - startForeground(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); - isInForeground = true; - Timber.w("--- Created Foreground notification"); - } - } - } - - @SuppressWarnings("IconColors") - private Notification buildForegroundNotification(PlayerState playerState, DownloadFile currentPlaying) { - notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); - - notificationBuilder.setAutoCancel(false); - notificationBuilder.setOngoing(true); - notificationBuilder.setOnlyAlertOnce(true); - notificationBuilder.setWhen(System.currentTimeMillis()); - notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - notificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - - 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); - - 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); - } - - 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); - } - } - - Notification notification = notificationBuilder.build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - notification.bigContentView = bigView; - } - - return notification; - } -} 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/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt new file mode 100644 index 00000000..4d5511e5 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt @@ -0,0 +1,125 @@ +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 +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 + // 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) { + hasFocus = true + AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) + } + } + + private val listener = OnAudioFocusChangeListener { focusChange -> + + val mediaPlayerController = mediaPlayerControllerLazy.value + + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> { + Timber.v("Regained Audio Focus") + if (pauseFocus) { + pauseFocus = false + mediaPlayerController.start() + } else if (lowerFocus) { + lowerFocus = false + mediaPlayerController.setVolume(1.0f) + } + } + AudioManager.AUDIOFOCUS_LOSS -> { + if (!mediaPlayerController.isJukeboxEnabled) { + hasFocus = false + mediaPlayerController.pause() + AudioManagerCompat.abandonAudioFocusRequest(audioManager, focusRequest) + Timber.v("Abandoned Audio Focus") + } + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + if (!mediaPlayerController.isJukeboxEnabled) { + Timber.v("Lost Audio Focus") + + if (mediaPlayerController.playerState === PlayerState.STARTED) { + if (lossPref == 0 || lossPref == 1) { + pauseFocus = true + mediaPlayerController.pause() + } + } + } + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + if (!mediaPlayerController.isJukeboxEnabled) { + Timber.v("Lost Audio Focus") + + if (mediaPlayerController.playerState === PlayerState.STARTED) { + if (lossPref == 2 || lossPref == 1) { + lowerFocus = true + mediaPlayerController.setVolume(0.1f) + } else if (lossPref == 0 || lossPref == 1) { + pauseFocus = true + mediaPlayerController.pause() + } + } + } + } + } + } + + private val focusRequest: AudioFocusRequestCompat by lazy { + AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributesCompat) + .setWillPauseWhenDucked(true) + .setOnAudioFocusChangeListener(listener) + .build() + } + + companion object { + private var hasFocus = false + private var pauseFocus = false + private var lowerFocus = false + + // TODO: This can be removed if we switch to androidx.media2.player + @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() + } + } +} 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 + } } 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..612383a1 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 @@ -52,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 @@ -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 @@ -96,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( @@ -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,13 +161,12 @@ 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 { - onPlayerStateChanged!!.accept(playerState, currentPlaying) + onPlayerStateChanged!!(playerState, currentPlaying) } mainHandler.post(myRunnable) } @@ -200,11 +187,10 @@ class LocalMediaPlayer( fun setCurrentPlaying(currentPlaying: DownloadFile?) { Timber.v("setCurrentPlaying %s", currentPlaying) this.currentPlaying = currentPlaying - updateRemoteControl() if (onCurrentPlayingChanged != null) { val mainHandler = Handler(context.mainLooper) - val myRunnable = Runnable { onCurrentPlayingChanged!!.accept(currentPlaying) } + val myRunnable = Runnable { onCurrentPlayingChanged!!(currentPlaying) } mainHandler.post(myRunnable) } } @@ -296,140 +282,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) } @@ -504,7 +361,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) { @@ -568,7 +425,9 @@ class LocalMediaPlayer( } } - postRunnable(onPrepared) + postRunnable { + onPrepared + } } attachHandlersToPlayer(mediaPlayer, downloadFile, partial) mediaPlayer.prepareAsync() @@ -577,23 +436,38 @@ 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 { 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 } nextMediaPlayer = MediaPlayer() - nextMediaPlayer!!.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) + nextMediaPlayer!!.setWakeMode(context, PARTIAL_WAKE_LOCK) + + setAudioAttributes(nextMediaPlayer!!) + + // This has nothing to do with the MediaSession, it is used to associate + // the equalizer or visualizer with the player try { nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId } catch (e: Throwable) { - nextMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC) } + nextMediaPlayer!!.setDataSource(file.path) setNextPlayerState(PlayerState.PREPARING) nextMediaPlayer!!.setOnPreparedListener { @@ -664,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) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt new file mode 100644 index 00000000..9f8d8578 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -0,0 +1,891 @@ +/* + * MediaPlayerService.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.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 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 +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.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 + +/** + * Android Foreground Service for playing music + * while the rest of the Ultrasonic App is in the background. + */ +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() + 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 + + private val repeatMode: RepeatMode + get() = Util.getRepeatMode(this) + + override fun onBind(intent: Intent): IBinder { + return binder + } + + override fun onCreate() { + super.onCreate() + + downloader.onCreate() + shufflePlayBuffer.onCreate() + localMediaPlayer.init() + + setupOnCurrentPlayingChangedHandler() + setupOnPlayerStateChangedHandler() + setupOnSongCompletedHandler() + + localMediaPlayer.onPrepared = { + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, + playerPosition + ) + null + } + + localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() } + + // Create Notification Channel + createNotificationChannel() + + // Update notification early. It is better to show an empty one temporarily + // than waiting too long and letting Android kill the app + updateNotification(PlayerState.IDLE, null) + instance = this + Timber.i("MediaPlayerService created") + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + return START_NOT_STICKY + } + + override fun onDestroy() { + super.onDestroy() + instance = null + try { + localMediaPlayer.release() + downloader.stop() + shufflePlayBuffer.onDestroy() + mediaSession?.release() + mediaSession == null + } catch (ignored: Throwable) { + } + Timber.i("MediaPlayerService stopped") + } + + private fun stopIfIdle() { + synchronized(instanceLock) { + // currentPlaying could be changed from another thread in the meantime, + // so check again before stopping for good + if (localMediaPlayer.currentPlaying == null || + localMediaPlayer.playerState === PlayerState.STOPPED + ) { + stopSelf() + } + } + } + + @Synchronized + fun seekTo(position: Int) { + if (jukeboxMediaPlayer.isEnabled) { + // TODO These APIs should be more aligned + val seconds = position / 1000 + jukeboxMediaPlayer.skip(downloader.currentPlayingIndex, seconds) + } else { + localMediaPlayer.seekTo(position) + } + } + + @get:Synchronized + val playerPosition: Int + get() { + if (localMediaPlayer.playerState === PlayerState.IDLE || + localMediaPlayer.playerState === PlayerState.DOWNLOADING || + localMediaPlayer.playerState === PlayerState.PREPARING + ) { + return 0 + } + return if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.positionSeconds * 1000 + } else { + localMediaPlayer.playerPosition + } + } + + @get:Synchronized + val playerDuration: Int + get() = localMediaPlayer.playerDuration + + @Synchronized + fun setCurrentPlaying(currentPlayingIndex: Int) { + try { + localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex]) + } catch (x: IndexOutOfBoundsException) { + // Ignored + } + } + + @Synchronized + fun setNextPlaying() { + val gaplessPlayback = Util.getGaplessPlaybackPreference(this) + + if (!gaplessPlayback) { + localMediaPlayer.clearNextPlaying(true) + return + } + + 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[index]) + } else { + localMediaPlayer.clearNextPlaying(true) + } + } + + @Synchronized + fun togglePlayPause() { + if (localMediaPlayer.playerState === PlayerState.PAUSED || + localMediaPlayer.playerState === PlayerState.COMPLETED || + localMediaPlayer.playerState === PlayerState.STOPPED + ) { + start() + } else if (localMediaPlayer.playerState === PlayerState.IDLE) { + play() + } else if (localMediaPlayer.playerState === PlayerState.STARTED) { + pause() + } + } + + @Synchronized + fun resumeOrPlay() { + if (localMediaPlayer.playerState === PlayerState.PAUSED || + localMediaPlayer.playerState === PlayerState.COMPLETED || + localMediaPlayer.playerState === PlayerState.STOPPED + ) { + start() + } else if (localMediaPlayer.playerState === PlayerState.IDLE) { + play() + } + } + + /** + * Plays either the current song (resume) or the first/next one in queue. + */ + @Synchronized + fun play() { + val current = downloader.currentPlayingIndex + if (current == -1) { + play(0) + } else { + play(current) + } + } + + @Synchronized + fun play(index: Int) { + play(index, true) + } + + @Synchronized + fun play(index: Int, start: Boolean) { + Timber.v("play requested for %d", index) + if (index < 0 || index >= downloader.downloadList.size) { + resetPlayback() + } else { + setCurrentPlaying(index) + if (start) { + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.skip(index, 0) + localMediaPlayer.setPlayerState(PlayerState.STARTED) + } else { + localMediaPlayer.play(downloader.downloadList[index]) + } + } + downloader.checkDownloads() + setNextPlaying() + } + } + + @Synchronized + private fun resetPlayback() { + localMediaPlayer.reset() + localMediaPlayer.setCurrentPlaying(null) + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, playerPosition + ) + } + + @Synchronized + fun pause() { + if (localMediaPlayer.playerState === PlayerState.STARTED) { + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.stop() + } else { + localMediaPlayer.pause() + } + localMediaPlayer.setPlayerState(PlayerState.PAUSED) + } + } + + @Synchronized + fun stop() { + if (localMediaPlayer.playerState === PlayerState.STARTED) { + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.stop() + } else { + localMediaPlayer.pause() + } + } + localMediaPlayer.setPlayerState(PlayerState.STOPPED) + } + + @Synchronized + fun start() { + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.start() + } else { + localMediaPlayer.start() + } + localMediaPlayer.setPlayerState(PlayerState.STARTED) + } + + 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) + } + + 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 + ) + } + + val showWhenPaused = playerState !== PlayerState.STOPPED && + Util.isNotificationAlwaysEnabled(context) + + val show = playerState === PlayerState.STARTED || showWhenPaused + val song = currentPlaying?.song + + 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) + + 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.raiseShowNowPlayingEvent() + } + } else { + nowPlayingEventDistributor.raiseHideNowPlayingEvent() + stopForeground(true) + 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? -> + val index = downloader.currentPlayingIndex + val context = this@MediaPlayerService + + if (currentPlaying != null) { + val song = currentPlaying.song + if (song.bookmarkPosition > 0 && Util.getShouldClearBookmark(context)) { + val musicService = getMusicService(context) + try { + musicService.deleteBookmark(song.id, context) + } catch (ignored: Exception) { + } + } + } + if (index != -1) { + when (repeatMode) { + RepeatMode.OFF -> { + if (index + 1 < 0 || index + 1 >= downloader.downloadList.size) { + if (Util.getShouldClearPlaylist(context)) { + clear(true) + jukeboxMediaPlayer.updatePlaylist() + } + resetPlayback() + } else { + play(index + 1) + } + } + RepeatMode.ALL -> { + play((index + 1) % downloader.downloadList.size) + } + RepeatMode.SINGLE -> play(index) + else -> { + } + } + } + null + } + } + + @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) { + Timber.d("Updating the MediaSession") + + if (mediaSession == null) initMediaSessions() + + // Set Metadata + val metadata = MediaMetadataCompat.Builder() + val context = applicationContext + if (currentPlaying != null) { + try { + 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.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()) + + // Create playback State + val playbackState = PlaybackStateCompat.Builder() + 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 actions + playbackState.setActions(actions) + + // Save the playback state + mediaSession!!.setPlaybackState(playbackState.build()) + + // Set Active state + mediaSession!!.isActive = isPlaying + + Timber.d("Setting the MediaSession to active = %s", isPlaying) + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + // The suggested importance of a startForeground service notification is IMPORTANCE_LOW + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW + ) + + channel.lightColor = android.R.color.holo_blue_dark + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + channel.setShowBadge(false) + + val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + manager.createNotificationChannel(channel) + } + } + + fun updateNotification(playerState: PlayerState, currentPlaying: DownloadFile?) { + val notification = buildForegroundNotification(playerState, currentPlaying) + + if (Util.isNotificationEnabled(this)) { + if (isInForeground) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + manager.notify(NOTIFICATION_ID, notification) + } else { + val manager = NotificationManagerCompat.from(this) + manager.notify(NOTIFICATION_ID, notification) + } + Timber.v("Updated notification") + } else { + startForeground(NOTIFICATION_ID, notification) + isInForeground = true + Timber.v("Created Foreground notification") + } + } + } + + /** + * This method builds a notification, reusing the Notification Builder if possible + */ + private fun buildForegroundNotification( + playerState: PlayerState, + currentPlaying: DownloadFile? + ): Notification { + + // Init + 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 = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + + // Set some values that never change + notificationBuilder!!.setSmallIcon(R.drawable.ic_stat_ultrasonic) + notificationBuilder!!.setAutoCancel(false) + notificationBuilder!!.setOngoing(true) + notificationBuilder!!.setOnlyAlertOnce(true) + notificationBuilder!!.setWhen(System.currentTimeMillis()) + notificationBuilder!!.setShowWhen(false) + notificationBuilder!!.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + notificationBuilder!!.priority = NotificationCompat.PRIORITY_LOW + + // Add content intent (when user taps on notification) + notificationBuilder!!.setContentIntent(getPendingIntentForContent()) + + // This intent is executed when the user closes the notification + notificationBuilder!!.setDeleteIntent(stopIntent) + } + + // Use the Media Style, to enable native Android support for playback notification + val style = androidx.media.app.NotificationCompat.MediaStyle() + + if (mediaSessionToken != null) { + style.setMediaSession(mediaSessionToken) + } + + // Clear old actions + notificationBuilder!!.clearActions() + + // Add actions + val 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) { + 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() + } + + 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(generateStarAction(context, numActions, song.starred)) + } + numActions++ + + // Next + notificationBuilder.addAction(generateAction(context, numActions)) + compactActionList.add(numActions) + numActions++ + + // Play/Pause button + notificationBuilder.addAction(generatePlayPauseAction(context, numActions, playerState)) + compactActionList.add(numActions) + numActions++ + + // Previous + notificationBuilder.addAction(generateAction(context, numActions)) + compactActionList.add(numActions) + numActions++ + + // Close + notificationBuilder.addAction(generateAction(context, numActions)) + val actionArray = IntArray(compactActionList.size) + for (i in actionArray.indices) { + actionArray[i] = compactActionList[i] + } + return actionArray + // notificationBuilder.setShowActionsInCompactView()) + } + + private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? { + val keycode: Int + val icon: Int + val label: String + + when (requestCode) { + 1 -> { + keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS + label = getString(R.string.common_play_previous) + icon = R.drawable.media_backward_medium_dark + } + 2 -> // Is handled in generatePlayPauseAction() + return null + 3 -> { + keycode = KeyEvent.KEYCODE_MEDIA_NEXT + label = getString(R.string.common_play_next) + icon = R.drawable.media_forward_medium_dark + } + 4 -> { + keycode = KeyEvent.KEYCODE_MEDIA_STOP + label = getString(R.string.buttons_stop) + icon = R.drawable.ic_baseline_close_24 + } + 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 { + val isPlaying = playerState === PlayerState.STARTED + 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 + } else { + label = getString(R.string.common_play) + icon = R.drawable.media_start_large_dark + } + + return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() + } + + private fun generateStarAction( + context: Context, + requestCode: Int, + isStarred: Boolean + ): NotificationCompat.Action { + + val label: String + val icon: Int + val keyCode: Int = KeyEvent.KEYCODE_STAR + + if (isStarred) { + label = getString(R.string.download_menu_star) + icon = R.drawable.ic_star_full_dark + } else { + label = getString(R.string.download_menu_star) + icon = R.drawable.ic_star_hollow_dark + } + + val pendingIntent = getPendingIntentForMediaAction(context, keyCode, requestCode) + return NotificationCompat.Action.Builder(icon, label, pendingIntent).build() + } + + private fun getPendingIntentForContent(): PendingIntent { + val intent = Intent(this, NavigationActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + val flags = PendingIntent.FLAG_UPDATE_CURRENT + intent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true) + return PendingIntent.getActivity(this, 0, intent, flags) + } + + 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, flags) + } + + 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); + + mediaSession!!.setCallback(object : MediaSessionCompat.Callback() { + override fun onPlay() { + super.onPlay() + + getPendingIntentForMediaAction( + applicationContext, + KeyEvent.KEYCODE_MEDIA_PLAY, + keycode + ).send() + + Timber.v("Media Session Callback: onPlay") + } + + override fun onPause() { + super.onPause() + getPendingIntentForMediaAction( + applicationContext, + KeyEvent.KEYCODE_MEDIA_PAUSE, + keycode + ).send() + Timber.v("Media Session Callback: onPause") + } + + override fun onStop() { + super.onStop() + getPendingIntentForMediaAction( + applicationContext, + KeyEvent.KEYCODE_MEDIA_STOP, + keycode + ).send() + Timber.v("Media Session Callback: onStop") + } + + 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? + mediaPlayerLifecycleSupport.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("ExecuteOnStarted.. failed to get a MediaPlayerService instance!") + return + } + taskToExecute.accept(instance) + } + } + t.start() + } + } +} 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 @@ + + + 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 @@ + + + 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 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