mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-27 00:47:48 +01:00
Merge pull request #421 from tzugen/modern-notification-2
Modernize Service Notification
This commit is contained in:
commit
817cc14ed9
@ -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
|
||||
|
@ -25,6 +25,7 @@ private val indexesSerializer get() = object : ObjectSerializer<Indexes>(SERIALI
|
||||
.writeObject<MutableList<Artist>>(context, item.artists, artistListSerializer)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun deserializeObject(
|
||||
context: SerializationContext,
|
||||
input: SerializerInput,
|
||||
|
@ -22,6 +22,7 @@ private val musicFolderSerializer = object : ObjectSerializer<MusicFolder>(SERIA
|
||||
output.writeString(item.id).writeString(item.name)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun deserializeObject(
|
||||
context: SerializationContext,
|
||||
input: SerializerInput,
|
||||
|
@ -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 = [
|
||||
|
261
detekt-baseline-debug.xml
Normal file
261
detekt-baseline-debug.xml
Normal file
@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||
<CurrentIssues>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun finishActivity()</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun setFields()</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun testConnection()</ID>
|
||||
<ID>CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
||||
<ID>CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNumberedFile(next: Boolean)</ID>
|
||||
<ID>CommentOverPrivateFunction:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification</ID>
|
||||
<ID>CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun search2( criteria: SearchCriteria ): SearchResult</ID>
|
||||
<ID>CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun searchOld( criteria: SearchCriteria ): SearchResult</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun editServer(index: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun onServerDeleted(index: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun setActiveServer(index: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting?</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun areIndexesMissing(): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun reindexSettings()</ID>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</ID>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!isOffline(activity) && isArtist && Util.getShouldUseId3Tags(activity)</ID>
|
||||
<ID>ComplexCondition:EditServerFragment.kt$EditServerFragment$urlString != urlString.trim(' ') || urlString.contains("@") || url.host.isNullOrBlank()</ID>
|
||||
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$playerState !== PlayerState.IDLE && playerState !== PlayerState.DOWNLOADING && playerState !== PlayerState.PREPARING</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState === PlayerState.PREPARED</ID>
|
||||
<ID>ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.IDLE || localMediaPlayer.playerState === PlayerState.DOWNLOADING || localMediaPlayer.playerState === PlayerState.PREPARING</ID>
|
||||
<ID>ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.PAUSED || localMediaPlayer.playerState === PlayerState.COMPLETED || localMediaPlayer.playerState === PlayerState.STOPPED</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !deleteEnabled && !isOffline(context)</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !isOffline(context) && selection.size > pinnedCount</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$entry != null && !entry.isDirectory && !entry.isVideo</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$Util.getShouldShowAllSongsByArtist(context) && musicDirectory.findChild(allSongsId) == null && musicDirectory.getChildren(true, false).size == musicDirectory.getChildren(true, true).size</ID>
|
||||
<ID>ComplexCondition:ServerSettingsModel.kt$ServerSettingsModel$url.isNullOrEmpty() || userName.isNullOrEmpty() || isMigrated</ID>
|
||||
<ID>ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo && Util.getVideoPlayerType(this.context) !== VideoPlayerType.FLASH</ID>
|
||||
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$id != null && view != null && view is ImageView</ID>
|
||||
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$username != null && view != null && view is ImageView</ID>
|
||||
<ID>ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean</ID>
|
||||
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
|
||||
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
|
||||
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
|
||||
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
|
||||
<ID>ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>)</ID>
|
||||
<ID>ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
|
||||
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
||||
<ID>ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
|
||||
<ID>ComplexMethod:SongView.kt$SongView$public override fun update()</ID>
|
||||
<ID>EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ }</ID>
|
||||
<ID>EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$()</ID>
|
||||
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</ID>
|
||||
<ID>FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
|
||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$String.format("%s\n\n%s", Util.getShareGreeting(context), result.url)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)</ID>
|
||||
<ID>LargeClass:DownloadFile.kt$DownloadFile</ID>
|
||||
<ID>LargeClass:DownloadHandler.kt$DownloadHandler</ID>
|
||||
<ID>LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler</ID>
|
||||
<ID>LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter</ID>
|
||||
<ID>LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
|
||||
<ID>LargeClass:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||
<ID>LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity</ID>
|
||||
<ID>LargeClass:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
|
||||
<ID>LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment$LoadTask : FragmentBackgroundTask</ID>
|
||||
<ID>LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment</ID>
|
||||
<ID>LargeClass:ServerSettingsModel.kt$ServerSettingsModel : ViewModel</ID>
|
||||
<ID>LargeClass:SongView.kt$SongView : UpdateViewCheckable</ID>
|
||||
<ID>LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry</ID>
|
||||
<ID>LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
|
||||
<ID>LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout)</ID>
|
||||
<ID>LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)</ID>
|
||||
<ID>LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||
<ID>LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File)</ID>
|
||||
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> )</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsForArtist( id: String, songs: MutableCollection<MusicDirectory.Entry> )</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList<MusicDirectory.Entry> )</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): List<MusicDirectory.Entry></ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$override fun done(songs: List<MusicDirectory.Entry>)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity()</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): Boolean</ID>
|
||||
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?)</ID>
|
||||
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder()</ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List<File>): LinkedList<FileListItem></ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList<FileListItem></ID>
|
||||
<ID>LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context)</ID>
|
||||
<ID>LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState)</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init()</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release()</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$override fun onCompletion(mediaPlayer: MediaPlayer)</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState)</ID>
|
||||
<ID>LongMethod:MediaStoreService.kt$MediaStoreService$fun saveInMediaStore(downloadFile: DownloadFile)</ID>
|
||||
<ID>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?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying()</ID>
|
||||
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, context: Context, playlist: MusicDirectory )</ID>
|
||||
<ID>LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun done(result: Pair<MusicDirectory, Boolean>)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun load(service: MusicService): MusicDirectory</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected fun createHeader( entries: List<MusicDirectory.Entry>, name: CharSequence?, songCount: Int ): View?</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>)</ID>
|
||||
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean</ID>
|
||||
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
|
||||
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int)</ID>
|
||||
<ID>LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean</ID>
|
||||
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting?</ID>
|
||||
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry)</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$public override fun update()</ID>
|
||||
<ID>LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable)</ID>
|
||||
<ID>LongMethod:UApp.kt$UApp$override fun onCreate()</ID>
|
||||
<ID>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 )</ID>
|
||||
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> )</ID>
|
||||
<ID>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 )</ID>
|
||||
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean )</ID>
|
||||
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String?, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||
<ID>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) )</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:AudioFocusHandler.kt$AudioFocusHandler$0.1f</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile$100</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$1000L</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60</ID>
|
||||
<ID>MagicNumber:DownloadHandler.kt$DownloadHandler$500</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$100</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$3</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$4</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$5</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$6</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$7</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$100</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$1000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$1000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1000L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$50L</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$100</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$1000</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$256</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$19</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$5</ID>
|
||||
<ID>MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10</ID>
|
||||
<ID>MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$10</ID>
|
||||
<ID>MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$3</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$4</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$60</ID>
|
||||
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>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 )</ID>
|
||||
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>NestedBlockDepth:SelectAlbumFragment.kt$SelectAlbumFragment$private fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?)</ID>
|
||||
<ID>ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
|
||||
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||
<ID>ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
||||
<ID>ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
|
||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
|
||||
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||
<ID>SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions)</ID>
|
||||
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) }</ID>
|
||||
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) }</ID>
|
||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower }</ID>
|
||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { }</ID>
|
||||
<ID>SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored }</ID>
|
||||
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
|
||||
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>)</ID>
|
||||
<ID>TooGenericExceptionCaught:ArtistListModel.kt$ArtistListModel$exception: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile.DownloadTask$x: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$e: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException</ID>
|
||||
<ID>TooGenericExceptionCaught:SelectAlbumFragment.kt$SelectAlbumFragment$exception: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
||||
<ID>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))</ID>
|
||||
<ID>TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
|
||||
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
|
||||
<ID>TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt"</ID>
|
||||
<ID>UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"</ID>
|
||||
<ID>UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty()</ID>
|
||||
<ID>UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree.Companion$fileList.isNullOrEmpty()</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
<ID>VariableNaming:SelectMusicFolderView.kt$SelectMusicFolderView$private val MENU_GROUP_MUSIC_FOLDER = 10</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
55
detekt-baseline-main.xml
Normal file
55
detekt-baseline-main.xml
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||
<CurrentIssues>
|
||||
<ID>ComplexCondition:SubsonicAPIClient.kt$SubsonicAPIClient$contentType != null && contentType.type().equals("application", true) && contentType.subtype().equals("json", true)</ID>
|
||||
<ID>ComplexMethod:AlbumListType.kt$AlbumListType.Companion$@JvmStatic fun fromName(typeName: String): AlbumListType</ID>
|
||||
<ID>ComplexMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions</ID>
|
||||
<ID>ComplexMethod:SubsonicError.kt$SubsonicError.Companion$fun getError(code: Int, message: String)</ID>
|
||||
<ID>EmptyFunctionBlock:SubsonicAPIClient.kt$SubsonicAPIClient.<no name provided>${}</ID>
|
||||
<ID>LargeClass:ApiVersionCheckWrapper.kt$ApiVersionCheckWrapper : SubsonicAPIDefinition</ID>
|
||||
<ID>LargeClass:SubsonicAPIDefinition.kt$SubsonicAPIDefinition</ID>
|
||||
<ID>LongMethod:SubsonicAPIClient.kt$SubsonicAPIClient$private inline fun handleStreamResponse(apiCall: () -> Response<ResponseBody>): StreamResponse</ID>
|
||||
<ID>LongMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions</ID>
|
||||
<ID>MagicNumber:PasswordExt.kt$0xFF</ID>
|
||||
<ID>MagicNumber:PasswordExt.kt$4</ID>
|
||||
<ID>MagicNumber:PasswordMD5Interceptor.kt$PasswordMD5Interceptor$16</ID>
|
||||
<ID>MagicNumber:StreamResponse.kt$StreamResponse$200</ID>
|
||||
<ID>MagicNumber:StreamResponse.kt$StreamResponse$300</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$10</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$11</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$12</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$13</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$14</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$15</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$16</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$3</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$4</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$5</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$6</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$7</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$8</ID>
|
||||
<ID>MagicNumber:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$9</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$10</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$20</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$30</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$40</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$41</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$50</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$60</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.Companion$70</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.IncompatibleClientProtocolVersion$20</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.IncompatibleServerProtocolVersion$30</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.RequestedDataWasNotFound$70</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.RequiredParamMissing$10</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.TokenAuthNotSupportedForLDAP$41</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.TrialPeriodIsOver$60</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.UserNotAuthorizedForOperation$50</ID>
|
||||
<ID>MagicNumber:SubsonicError.kt$SubsonicError.WrongUsernameOrPassword$40</ID>
|
||||
<ID>ReturnCount:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions</ID>
|
||||
<ID>SwallowedException:VersionAwareJacksonConverterFactory.kt$VersionAwareJacksonConverterFactory.VersionAwareResponseBodyConverter$catch (e: IllegalArgumentException) { // no-op }</ID>
|
||||
<ID>ThrowsCount:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions</ID>
|
||||
<ID>TooManyFunctions:ApiVersionCheckWrapper.kt$ApiVersionCheckWrapper : SubsonicAPIDefinition</ID>
|
||||
<ID>UnusedPrivateMember:AlbumListType.kt$AlbumListType.Companion$private operator fun String.contains(other: String)</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
261
detekt-baseline-release.xml
Normal file
261
detekt-baseline-release.xml
Normal file
@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||
<CurrentIssues>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun finishActivity()</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun setFields()</ID>
|
||||
<ID>CommentOverPrivateFunction:EditServerFragment.kt$EditServerFragment$ private fun testConnection()</ID>
|
||||
<ID>CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
||||
<ID>CommentOverPrivateFunction:FileLoggerTree.kt$FileLoggerTree$ private fun getNumberedFile(next: Boolean)</ID>
|
||||
<ID>CommentOverPrivateFunction:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification</ID>
|
||||
<ID>CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun search2( criteria: SearchCriteria ): SearchResult</ID>
|
||||
<ID>CommentOverPrivateFunction:RESTMusicService.kt$RESTMusicService$ @Throws(Exception::class) private fun searchOld( criteria: SearchCriteria ): SearchResult</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun editServer(index: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun onServerDeleted(index: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSelectorFragment.kt$ServerSelectorFragment$ private fun setActiveServer(index: Int)</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting?</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun areIndexesMissing(): Boolean</ID>
|
||||
<ID>CommentOverPrivateFunction:ServerSettingsModel.kt$ServerSettingsModel$ private suspend fun reindexSettings()</ID>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</ID>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!isOffline(activity) && isArtist && Util.getShouldUseId3Tags(activity)</ID>
|
||||
<ID>ComplexCondition:EditServerFragment.kt$EditServerFragment$urlString != urlString.trim(' ') || urlString.contains("@") || url.host.isNullOrBlank()</ID>
|
||||
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$playerState !== PlayerState.IDLE && playerState !== PlayerState.DOWNLOADING && playerState !== PlayerState.PREPARING</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState === PlayerState.PREPARED</ID>
|
||||
<ID>ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.IDLE || localMediaPlayer.playerState === PlayerState.DOWNLOADING || localMediaPlayer.playerState === PlayerState.PREPARING</ID>
|
||||
<ID>ComplexCondition:MediaPlayerService.kt$MediaPlayerService$localMediaPlayer.playerState === PlayerState.PAUSED || localMediaPlayer.playerState === PlayerState.COMPLETED || localMediaPlayer.playerState === PlayerState.STOPPED</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !deleteEnabled && !isOffline(context)</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled && !isOffline(context) && selection.size > pinnedCount</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$entry != null && !entry.isDirectory && !entry.isVideo</ID>
|
||||
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$Util.getShouldShowAllSongsByArtist(context) && musicDirectory.findChild(allSongsId) == null && musicDirectory.getChildren(true, false).size == musicDirectory.getChildren(true, true).size</ID>
|
||||
<ID>ComplexCondition:ServerSettingsModel.kt$ServerSettingsModel$url.isNullOrEmpty() || userName.isNullOrEmpty() || isMigrated</ID>
|
||||
<ID>ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo && Util.getVideoPlayerType(this.context) !== VideoPlayerType.FLASH</ID>
|
||||
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$id != null && view != null && view is ImageView</ID>
|
||||
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$username != null && view != null && view is ImageView</ID>
|
||||
<ID>ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean</ID>
|
||||
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
|
||||
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
|
||||
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
|
||||
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
|
||||
<ID>ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
|
||||
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>)</ID>
|
||||
<ID>ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
|
||||
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
||||
<ID>ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
|
||||
<ID>ComplexMethod:SongView.kt$SongView$public override fun update()</ID>
|
||||
<ID>EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ }</ID>
|
||||
<ID>EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$()</ID>
|
||||
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</ID>
|
||||
<ID>FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
|
||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$String.format("%s\n\n%s", Util.getShareGreeting(context), result.url)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)</ID>
|
||||
<ID>LargeClass:DownloadFile.kt$DownloadFile</ID>
|
||||
<ID>LargeClass:DownloadHandler.kt$DownloadHandler</ID>
|
||||
<ID>LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler</ID>
|
||||
<ID>LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter</ID>
|
||||
<ID>LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
|
||||
<ID>LargeClass:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||
<ID>LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity</ID>
|
||||
<ID>LargeClass:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
|
||||
<ID>LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment$LoadTask : FragmentBackgroundTask</ID>
|
||||
<ID>LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment</ID>
|
||||
<ID>LargeClass:ServerSettingsModel.kt$ServerSettingsModel : ViewModel</ID>
|
||||
<ID>LargeClass:SongView.kt$SongView : UpdateViewCheckable</ID>
|
||||
<ID>LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry</ID>
|
||||
<ID>LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
|
||||
<ID>LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout)</ID>
|
||||
<ID>LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)</ID>
|
||||
<ID>LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||
<ID>LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File)</ID>
|
||||
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> )</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsForArtist( id: String, songs: MutableCollection<MusicDirectory.Entry> )</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList<MusicDirectory.Entry> )</ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): List<MusicDirectory.Entry></ID>
|
||||
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$override fun done(songs: List<MusicDirectory.Entry>)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity()</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): Boolean</ID>
|
||||
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?)</ID>
|
||||
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder()</ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List<File>): LinkedList<FileListItem></ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList<FileListItem></ID>
|
||||
<ID>LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context)</ID>
|
||||
<ID>LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState)</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init()</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release()</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$override fun onCompletion(mediaPlayer: MediaPlayer)</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState)</ID>
|
||||
<ID>LongMethod:MediaStoreService.kt$MediaStoreService$fun saveInMediaStore(downloadFile: DownloadFile)</ID>
|
||||
<ID>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?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying()</ID>
|
||||
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, context: Context, playlist: MusicDirectory )</ID>
|
||||
<ID>LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun done(result: Pair<MusicDirectory, Boolean>)</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.<no name provided>$override fun load(service: MusicService): MusicDirectory</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected fun createHeader( entries: List<MusicDirectory.Entry>, name: CharSequence?, songCount: Int ): View?</ID>
|
||||
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair<MusicDirectory, Boolean>)</ID>
|
||||
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean</ID>
|
||||
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
|
||||
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int)</ID>
|
||||
<ID>LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean</ID>
|
||||
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting?</ID>
|
||||
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry)</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$public override fun update()</ID>
|
||||
<ID>LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable)</ID>
|
||||
<ID>LongMethod:UApp.kt$UApp$override fun onCreate()</ID>
|
||||
<ID>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 )</ID>
|
||||
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> )</ID>
|
||||
<ID>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 )</ID>
|
||||
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean )</ID>
|
||||
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String?, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||
<ID>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) )</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:AudioFocusHandler.kt$AudioFocusHandler$0.1f</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile$100</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$1000L</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60</ID>
|
||||
<ID>MagicNumber:DownloadHandler.kt$DownloadHandler$500</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$100</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$3</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$4</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$5</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$6</ID>
|
||||
<ID>MagicNumber:FileLoggerTree.kt$FileLoggerTree$7</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$100</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer$1000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$1000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1000L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$50L</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$100</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$1000</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$256</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$19</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$5</ID>
|
||||
<ID>MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10</ID>
|
||||
<ID>MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$10</ID>
|
||||
<ID>MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$3</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$4</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$60</ID>
|
||||
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>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 )</ID>
|
||||
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>NestedBlockDepth:SelectAlbumFragment.kt$SelectAlbumFragment$private fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?)</ID>
|
||||
<ID>ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
|
||||
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||
<ID>ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
||||
<ID>ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
|
||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( context: Context, username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( context: Context, entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
|
||||
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||
<ID>SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions)</ID>
|
||||
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) }</ID>
|
||||
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) }</ID>
|
||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower }</ID>
|
||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { }</ID>
|
||||
<ID>SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored }</ID>
|
||||
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
|
||||
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>)</ID>
|
||||
<ID>TooGenericExceptionCaught:ArtistListModel.kt$ArtistListModel$exception: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile.DownloadTask$x: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$e: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException</ID>
|
||||
<ID>TooGenericExceptionCaught:SelectAlbumFragment.kt$SelectAlbumFragment$exception: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
||||
<ID>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))</ID>
|
||||
<ID>TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
|
||||
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
|
||||
<ID>TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt"</ID>
|
||||
<ID>UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"</ID>
|
||||
<ID>UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty()</ID>
|
||||
<ID>UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree.Companion$fileList.isNullOrEmpty()</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
<ID>VariableNaming:SelectMusicFolderView.kt$SelectMusicFolderView$private val MENU_GROUP_MUSIC_FOLDER = 10</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ dependencies {
|
||||
implementation androidSupport.viewModelKtx
|
||||
implementation androidSupport.constraintLayout
|
||||
implementation androidSupport.preferences
|
||||
implementation androidSupport.media
|
||||
|
||||
implementation androidSupport.navigationFragment
|
||||
implementation androidSupport.navigationUi
|
||||
|
@ -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<MediaPlayerController> 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
/**
|
||||
* Abstract class for consumers with two parameters
|
||||
* @param <T> The type of the first object to consume
|
||||
* @param <U> The type of the second object to consume
|
||||
*/
|
||||
public abstract class BiConsumer<T, U>
|
||||
{
|
||||
public abstract void accept(T t, U u);
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
/**
|
||||
* Deprecated: Should be replaced with lambdas
|
||||
* Abstract class for consumers with one parameter
|
||||
* @param <T> The type of the object to consume
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class Consumer<T>
|
||||
{
|
||||
public abstract void accept(T t);
|
||||
|
@ -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))
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
|
||||
private final Lazy<DownloadQueueSerializer> downloadQueueSerializerLazy = inject(DownloadQueueSerializer.class);
|
||||
private final Lazy<ShufflePlayBuffer> shufflePlayBufferLazy = inject(ShufflePlayBuffer.class);
|
||||
private final Lazy<Downloader> downloaderLazy = inject(Downloader.class);
|
||||
private final Lazy<LocalMediaPlayer> localMediaPlayerLazy = inject(LocalMediaPlayer.class);
|
||||
private final Lazy<NowPlayingEventDistributor> 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<MediaPlayerService> 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<DownloadFile>() {
|
||||
@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<PlayerState, DownloadFile>() {
|
||||
@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<DownloadFile>() {
|
||||
@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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<DownloadFile?>? = null
|
||||
var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null
|
||||
|
||||
@JvmField
|
||||
var onSongCompleted: Consumer<DownloadFile?>? = null
|
||||
var onSongCompleted: ((DownloadFile?) -> Unit?)? = null
|
||||
|
||||
@JvmField
|
||||
var onPlayerStateChanged: BiConsumer<PlayerState, DownloadFile?>? = 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)
|
||||
}
|
||||
}
|
||||
|
@ -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<JukeboxMediaPlayer>()
|
||||
private val downloadQueueSerializer by inject<DownloadQueueSerializer>()
|
||||
private val shufflePlayBuffer by inject<ShufflePlayBuffer>()
|
||||
private val downloader by inject<Downloader>()
|
||||
private val localMediaPlayer by inject<LocalMediaPlayer>()
|
||||
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
|
||||
private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
||||
|
||||
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<Int>()
|
||||
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<MediaPlayerService?>
|
||||
) {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="32dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="32dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="48dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="48dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M8,5v14l11,-7z"/>
|
||||
</vector>
|
@ -1,121 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/statusbar"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="64dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/background_color_dark"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_image"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:gravity="center"
|
||||
tools:background="#FF00FF"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="12dip"
|
||||
android:paddingStart="12dp"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trackname"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:ellipsize="marquee"
|
||||
android:focusable="true"
|
||||
android:maxLines="1"
|
||||
tools:text="Track name"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/artist"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:ellipsize="end"
|
||||
android:scrollHorizontally="true"
|
||||
android:maxLines="1"
|
||||
tools:text="Artist"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:ellipsize="end"
|
||||
android:scrollHorizontally="true"
|
||||
android:maxLines="1"
|
||||
tools:text="Album"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal"
|
||||
android:paddingRight="8dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_previous"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_margin="2dp"
|
||||
android:src="@drawable/media_backward_normal_dark" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_play"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="2dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/media_pause_normal_dark" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_next"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_margin="2dp"
|
||||
android:src="@drawable/media_forward_normal_dark" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_stop"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="70"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_menu_close_dark"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -1,189 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/statusbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="150dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/background_color_dark" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_image"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:gravity="center"
|
||||
tools:background="#ff00ff"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="8dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="8dip"
|
||||
android:paddingBottom="8dip"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingRight="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trackname"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center|start"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="marquee"
|
||||
android:focusable="true"
|
||||
android:singleLine="true"
|
||||
tools:text="Track name" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_stop"
|
||||
android:layout_width="32dip"
|
||||
android:layout_height="32dip"
|
||||
android:layout_gravity="center|end"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:gravity="center_vertical"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_menu_close_dark" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/artist"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:ellipsize="end"
|
||||
android:scrollHorizontally="true"
|
||||
android:maxLines="1"
|
||||
tools:text="Artist"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:ellipsize="end"
|
||||
android:scrollHorizontally="true"
|
||||
android:maxLines="1"
|
||||
tools:text="Album"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/notification_rating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dip"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_five_star_1"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:focusable="false"
|
||||
android:gravity="center_vertical"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_five_star_2"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:focusable="false"
|
||||
android:gravity="center_vertical"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_five_star_3"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:focusable="false"
|
||||
android:gravity="center_vertical"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_five_star_4"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:focusable="false"
|
||||
android:gravity="center_vertical"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_five_star_5"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:focusable="false"
|
||||
android:gravity="center_vertical"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="5dip"
|
||||
android:layout_marginBottom="10dip"
|
||||
android:background="#DD696969"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center|bottom"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_previous"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:src="@drawable/media_backward_normal_dark" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_play"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:src="@drawable/media_pause_normal_dark" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_next"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/btn_bg"
|
||||
android:src="@drawable/media_forward_normal_dark" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
@ -39,8 +39,11 @@
|
||||
<string name="common.name">Name</string>
|
||||
<string name="common.ok">OK</string>
|
||||
<string name="common.pin">Pin</string>
|
||||
<string name="common.pause">Pause</string>
|
||||
<string name="common.play">Play</string>
|
||||
<string name="common.play_last">Play Last</string>
|
||||
<string name="common.play_next">Play Next</string>
|
||||
<string name="common.play_previous">Play Previous</string>
|
||||
<string name="common.play_now">Play Now</string>
|
||||
<string name="common.play_shuffled">Play Shuffled</string>
|
||||
<string name="common.public">Public</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user