Merge pull request #421 from tzugen/modern-notification-2

Modernize Service Notification
This commit is contained in:
Nite 2021-04-27 10:58:50 +02:00 committed by GitHub
commit 817cc14ed9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1707 additions and 1386 deletions

View File

@ -1,4 +1,4 @@
version: 2 version: 3
jobs: jobs:
build: build:
docker: docker:
@ -18,7 +18,7 @@ jobs:
command: ./gradlew -Pqc ktlintCheck command: ./gradlew -Pqc ktlintCheck
- run: - run:
name: static analysis name: static analysis
command: ./gradlew -Pqc detektCheck command: ./gradlew -Pqc detektMain
- run: - run:
name: build name: build
command: ./gradlew assembleDebug command: ./gradlew assembleDebug

View File

@ -25,6 +25,7 @@ private val indexesSerializer get() = object : ObjectSerializer<Indexes>(SERIALI
.writeObject<MutableList<Artist>>(context, item.artists, artistListSerializer) .writeObject<MutableList<Artist>>(context, item.artists, artistListSerializer)
} }
@Suppress("ReturnCount")
override fun deserializeObject( override fun deserializeObject(
context: SerializationContext, context: SerializationContext,
input: SerializerInput, input: SerializerInput,

View File

@ -22,6 +22,7 @@ private val musicFolderSerializer = object : ObjectSerializer<MusicFolder>(SERIA
output.writeString(item.id).writeString(item.name) output.writeString(item.id).writeString(item.name)
} }
@Suppress("ReturnCount")
override fun deserializeObject( override fun deserializeObject(
context: SerializationContext, context: SerializationContext,
input: SerializerInput, input: SerializerInput,

View File

@ -5,13 +5,14 @@ ext.versions = [
gradle : '6.5', gradle : '6.5',
navigation : "2.3.2", navigation : "2.3.2",
gradlePlugin : "4.1.2", gradlePlugin : "4.1.3",
androidxcore : "1.5.0-rc01", androidxcore : "1.5.0-rc01",
ktlint : "0.37.1", ktlint : "0.37.1",
ktlintGradle : "9.2.1", ktlintGradle : "9.2.1",
detekt : "1.0.0.RC6-4", detekt : "1.16.0",
jacoco : "0.8.5", jacoco : "0.8.5",
preferences : "1.1.1", preferences : "1.1.1",
media : "1.3.0",
androidSupport : "28.0.0", androidSupport : "28.0.0",
androidLegacySupport : "1.0.0", androidLegacySupport : "1.0.0",
@ -48,12 +49,12 @@ ext.gradlePlugins = [
gradle : "com.android.tools.build:gradle:$versions.gradlePlugin", gradle : "com.android.tools.build:gradle:$versions.gradlePlugin",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin", kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle", 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", jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco",
] ]
ext.androidSupport = [ ext.androidSupport = [
core : "androidx.core:core-ktx:$versions.androidxcore", core : "androidx.core:core-ktx:$versions.androidxcore",
support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport", support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport",
design : "com.google.android.material:material:$versions.androidSupportDesign", design : "com.google.android.material:material:$versions.androidSupportDesign",
annotations : "com.android.support:support-annotations:$versions.androidSupport", annotations : "com.android.support:support-annotations:$versions.androidSupport",
@ -69,6 +70,7 @@ ext.androidSupport = [
navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation", navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation",
navigationFeature : "androidx.navigation:navigation-dynamic-features-fragment:$versions.navigation", navigationFeature : "androidx.navigation:navigation-dynamic-features-fragment:$versions.navigation",
preferences : "androidx.preference:preference:$versions.preferences", preferences : "androidx.preference:preference:$versions.preferences",
media : "androidx.media:media:$versions.media",
] ]
ext.other = [ ext.other = [

261
detekt-baseline-debug.xml Normal file
View 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.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!isOffline(activity) &amp;&amp; isArtist &amp;&amp; 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) &amp;&amp; Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.JELLY_BEAN &amp;&amp; ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$playerState !== PlayerState.IDLE &amp;&amp; playerState !== PlayerState.DOWNLOADING &amp;&amp; playerState !== PlayerState.PREPARING</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$!isPartial || downloadFile.isWorkDone &amp;&amp; abs(duration - pos) &lt; 1000</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$Util.getGaplessPlaybackPreference(context) &amp;&amp; nextPlaying != null &amp;&amp; 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 &amp;&amp; !deleteEnabled &amp;&amp; !isOffline(context)</ID>
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled &amp;&amp; !isOffline(context) &amp;&amp; selection.size &gt; pinnedCount</ID>
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$entry != null &amp;&amp; !entry.isDirectory &amp;&amp; !entry.isVideo</ID>
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment.&lt;no name provided&gt;$Util.getShouldShowAllSongsByArtist(context) &amp;&amp; musicDirectory.findChild(allSongsId) == null &amp;&amp; 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 &amp;&amp; Util.getVideoPlayerType(this.context) !== VideoPlayerType.FLASH</ID>
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$id != null &amp;&amp; view != null &amp;&amp; view is ImageView</ID>
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$username != null &amp;&amp; view != null &amp;&amp; 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&lt;MusicDirectory, Boolean&gt;)</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.&lt;no name provided&gt;$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.&lt;no name provided&gt;$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 &gt; %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&lt;MusicDirectory.Entry?&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Exception::class) private fun getSongsForArtist( id: String, songs: MutableCollection&lt;MusicDirectory.Entry&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList&lt;MusicDirectory.Entry&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Throwable::class) override fun doInBackground(): List&lt;MusicDirectory.Entry&gt;</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$override fun done(songs: List&lt;MusicDirectory.Entry&gt;)</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.&lt;no name provided&gt;$@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&lt;File&gt;): LinkedList&lt;FileListItem&gt;</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList&lt;FileListItem&gt;</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.&lt;no name provided&gt;$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&lt;MusicDirectory.Entry?&gt;)</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.&lt;no name provided&gt;$override fun done(result: Pair&lt;MusicDirectory, Boolean&gt;)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.&lt;no name provided&gt;$override fun load(service: MusicService): MusicDirectory</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected fun createHeader( entries: List&lt;MusicDirectory.Entry&gt;, name: CharSequence?, songCount: Int ): View?</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair&lt;MusicDirectory, Boolean&gt;)</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&lt;Artist&gt;, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -&gt; Unit, val onContextMenuClick: (MenuItem, Artist) -&gt; 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&lt;MusicDirectory.Entry?&gt; )</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&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; 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.&lt;no name provided&gt;$1000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$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&lt;out SubsonicResponse&gt;)</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
View File

@ -0,0 +1,55 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>ComplexCondition:SubsonicAPIClient.kt$SubsonicAPIClient$contentType != null &amp;&amp; contentType.type().equals("application", true) &amp;&amp; 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.&lt;no name provided&gt;${}</ID>
<ID>LargeClass:ApiVersionCheckWrapper.kt$ApiVersionCheckWrapper : SubsonicAPIDefinition</ID>
<ID>LargeClass:SubsonicAPIDefinition.kt$SubsonicAPIDefinition</ID>
<ID>LongMethod:SubsonicAPIClient.kt$SubsonicAPIClient$private inline fun handleStreamResponse(apiCall: () -&gt; Response&lt;ResponseBody&gt;): 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
View 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.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!isOffline(activity) &amp;&amp; isArtist &amp;&amp; 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) &amp;&amp; Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.JELLY_BEAN &amp;&amp; ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$playerState !== PlayerState.IDLE &amp;&amp; playerState !== PlayerState.DOWNLOADING &amp;&amp; playerState !== PlayerState.PREPARING</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$!isPartial || downloadFile.isWorkDone &amp;&amp; abs(duration - pos) &lt; 1000</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$Util.getGaplessPlaybackPreference(context) &amp;&amp; nextPlaying != null &amp;&amp; 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 &amp;&amp; !deleteEnabled &amp;&amp; !isOffline(context)</ID>
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$enabled &amp;&amp; !isOffline(context) &amp;&amp; selection.size &gt; pinnedCount</ID>
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment$entry != null &amp;&amp; !entry.isDirectory &amp;&amp; !entry.isVideo</ID>
<ID>ComplexCondition:SelectAlbumFragment.kt$SelectAlbumFragment.&lt;no name provided&gt;$Util.getShouldShowAllSongsByArtist(context) &amp;&amp; musicDirectory.findChild(allSongsId) == null &amp;&amp; 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 &amp;&amp; Util.getVideoPlayerType(this.context) !== VideoPlayerType.FLASH</ID>
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$id != null &amp;&amp; view != null &amp;&amp; view is ImageView</ID>
<ID>ComplexCondition:SubsonicImageLoaderProxy.kt$SubsonicImageLoaderProxy$username != null &amp;&amp; view != null &amp;&amp; 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&lt;MusicDirectory, Boolean&gt;)</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.&lt;no name provided&gt;$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.&lt;no name provided&gt;$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 &gt; %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&lt;MusicDirectory.Entry?&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Exception::class) private fun getSongsForArtist( id: String, songs: MutableCollection&lt;MusicDirectory.Entry&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList&lt;MusicDirectory.Entry&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Throwable::class) override fun doInBackground(): List&lt;MusicDirectory.Entry&gt;</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$override fun done(songs: List&lt;MusicDirectory.Entry&gt;)</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.&lt;no name provided&gt;$@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&lt;File&gt;): LinkedList&lt;FileListItem&gt;</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList&lt;FileListItem&gt;</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.&lt;no name provided&gt;$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&lt;MusicDirectory.Entry?&gt;)</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.&lt;no name provided&gt;$override fun done(result: Pair&lt;MusicDirectory, Boolean&gt;)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.&lt;no name provided&gt;$override fun load(service: MusicService): MusicDirectory</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected fun createHeader( entries: List&lt;MusicDirectory.Entry&gt;, name: CharSequence?, songCount: Int ): View?</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment.LoadTask$protected override fun done(result: Pair&lt;MusicDirectory, Boolean&gt;)</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&lt;Artist&gt;, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -&gt; Unit, val onContextMenuClick: (MenuItem, Artist) -&gt; 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&lt;MusicDirectory.Entry?&gt; )</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&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; 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.&lt;no name provided&gt;$1000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$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&lt;out SubsonicResponse&gt;)</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>

View File

@ -1,9 +1,5 @@
autoCorrect: true
failFast: false
build: build:
warningThreshold: 0 maxIssues: 0
failThreshold: 0
weights: weights:
complexity: 2 complexity: 2
formatting: 1 formatting: 1
@ -43,26 +39,24 @@ complexity:
LongMethod: LongMethod:
threshold: 20 threshold: 20
LongParameterList: LongParameterList:
threshold: 5 functionThreshold: 5
constructorThreshold: 5
LargeClass: LargeClass:
threshold: 150 threshold: 150
ComplexMethod: ComplexMethod:
threshold: 10 threshold: 10
TooManyFunctions: TooManyFunctions:
threshold: 20 thresholdInFiles: 20
thresholdInClasses: 20
thresholdInInterfaces: 20
ComplexCondition: ComplexCondition:
threshold: 3 threshold: 3
LabeledExpression: LabeledExpression:
active: false active: false
code-smell:
active: true
FeatureEnvy:
threshold: 0.5
weight: 0.45
base: 0.5
formatting: formatting:
autoCorrect: true
active: false active: false
style: style:
@ -71,7 +65,7 @@ style:
active: true active: true
ForbiddenComment: ForbiddenComment:
active: true active: true
values: 'TODO:,FIXME:,STOPSHIP:' values: 'FIXME:,STOPSHIP:'
WildcardImport: WildcardImport:
active: true active: true
MaxLineLength: MaxLineLength:
@ -79,17 +73,10 @@ style:
maxLineLength: 120 maxLineLength: 120
excludePackageStatements: false excludePackageStatements: false
excludeImportStatements: 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: comments:
active: true active: true
CommentOverPrivateMethod: CommentOverPrivateFunction:
active: true active: true
CommentOverPrivateProperty: CommentOverPrivateProperty:
active: true active: true
@ -100,12 +87,3 @@ comments:
searchInInnerInterface: true searchInInnerInterface: true
UndocumentedPublicFunction: UndocumentedPublicFunction:
active: false 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

View File

@ -20,11 +20,13 @@ if (isCodeQualityEnabled) {
apply plugin: "io.gitlab.arturbosch.detekt" apply plugin: "io.gitlab.arturbosch.detekt"
detekt { detekt {
version = versions.detekt buildUponDefaultConfig = true
profile("main") { toolVersion = versions.detekt
config = "${rootProject.projectDir}/detekt-config.yml" baseline = file("${rootProject.projectDir}/detekt-baseline.xml")
} config = files("${rootProject.projectDir}/detekt-config.yml")
} }
} }
tasks.detekt.jvmTarget = "1.8"
} }
} }

View File

@ -80,6 +80,7 @@ dependencies {
implementation androidSupport.viewModelKtx implementation androidSupport.viewModelKtx
implementation androidSupport.constraintLayout implementation androidSupport.constraintLayout
implementation androidSupport.preferences implementation androidSupport.preferences
implementation androidSupport.media
implementation androidSupport.navigationFragment implementation androidSupport.navigationFragment
implementation androidSupport.navigationUi implementation androidSupport.navigationUi

View File

@ -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");
}
}
}

View File

@ -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);
}

View File

@ -1,9 +1,11 @@
package org.moire.ultrasonic.service; package org.moire.ultrasonic.service;
/** /**
* Deprecated: Should be replaced with lambdas
* Abstract class for consumers with one parameter * Abstract class for consumers with one parameter
* @param <T> The type of the object to consume * @param <T> The type of the object to consume
*/ */
@Deprecated
public abstract class Consumer<T> public abstract class Consumer<T>
{ {
public abstract void accept(T t); public abstract void accept(T t);

View File

@ -589,6 +589,18 @@ public class MediaPlayerControllerImpl implements MediaPlayerController
if (mediaPlayerService != null) mediaPlayerService.updateNotification(localMediaPlayer.playerState, localMediaPlayer.currentPlaying); 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) public void setSongRating(final int rating)
{ {
if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING)) if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING))

View File

@ -183,7 +183,7 @@ public class MediaPlayerLifecycleSupport
context.registerReceiver(headsetEventReceiver, headsetIntentFilter); context.registerReceiver(headsetEventReceiver, headsetIntentFilter);
} }
private void handleKeyEvent(KeyEvent event) public void handleKeyEvent(KeyEvent event)
{ {
if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0) if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0)
{ {
@ -254,6 +254,9 @@ public class MediaPlayerLifecycleSupport
case KeyEvent.KEYCODE_5: case KeyEvent.KEYCODE_5:
mediaPlayerController.setSongRating(5); mediaPlayerController.setSongRating(5);
break; break;
case KeyEvent.KEYCODE_STAR:
mediaPlayerController.toggleSongStarred();
break;
default: default:
break; break;
} }

View File

@ -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;
}
}

View File

@ -21,7 +21,6 @@ package org.moire.ultrasonic.util;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.*; import android.content.*;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@ -35,7 +34,6 @@ import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
@ -44,17 +42,14 @@ import android.util.DisplayMetrics;
import timber.log.Timber; import timber.log.Timber;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.RemoteViews;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.*; import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -852,6 +847,7 @@ public class Util
return; return;
} }
// FIXME: This is probably a bug.
if (currentSong != currentSong) if (currentSong != currentSong)
{ {
Util.currentSong = currentSong; Util.currentSong = currentSong;
@ -1004,74 +1000,6 @@ public class Util
return inSampleSize; 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? // TODO: Shouldn't this be used when making requests?
public static int getNetworkTimeout(Context context) public static int getNetworkTimeout(Context context)
{ {

View File

@ -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()
}
}
}

View File

@ -45,7 +45,7 @@ class DownloadFile(
private val mediaStoreService: MediaStoreService private val mediaStoreService: MediaStoreService
private var downloadTask: CancellableTask? = null private var downloadTask: CancellableTask? = null
var isFailed = false var isFailed = false
private var retryCount = 5 private var retryCount = MAX_RETRIES
private val desiredBitRate: Int = Util.getMaxBitRate(context) private val desiredBitRate: Int = Util.getMaxBitRate(context)
@ -382,4 +382,8 @@ class DownloadFile(
} }
} }
} }
companion object {
const val MAX_RETRIES = 5
}
} }

View File

@ -7,17 +7,12 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Context.AUDIO_SERVICE
import android.content.Context.POWER_SERVICE import android.content.Context.POWER_SERVICE
import android.content.Intent import android.content.Intent
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaMetadataRetriever
import android.media.MediaPlayer import android.media.MediaPlayer
import android.media.MediaPlayer.OnCompletionListener import android.media.MediaPlayer.OnCompletionListener
import android.media.RemoteControlClient
import android.media.audiofx.AudioEffect import android.media.audiofx.AudioEffect
import android.os.Build import android.os.Build
import android.os.Handler 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.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.fragment.PlayerFragment import org.moire.ultrasonic.fragment.PlayerFragment
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.StreamProxy import org.moire.ultrasonic.util.StreamProxy
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -52,16 +45,16 @@ class LocalMediaPlayer(
) { ) {
@JvmField @JvmField
var onCurrentPlayingChanged: Consumer<DownloadFile?>? = null var onCurrentPlayingChanged: ((DownloadFile?) -> Unit?)? = null
@JvmField @JvmField
var onSongCompleted: Consumer<DownloadFile?>? = null var onSongCompleted: ((DownloadFile?) -> Unit?)? = null
@JvmField @JvmField
var onPlayerStateChanged: BiConsumer<PlayerState, DownloadFile?>? = null var onPlayerStateChanged: ((PlayerState, DownloadFile?) -> Unit?)? = null
@JvmField @JvmField
var onPrepared: Runnable? = null var onPrepared: (() -> Any?)? = null
@JvmField @JvmField
var onNextSongRequested: Runnable? = null var onNextSongRequested: Runnable? = null
@ -84,8 +77,6 @@ class LocalMediaPlayer(
private var mediaPlayerHandler: Handler? = null private var mediaPlayerHandler: Handler? = null
private var cachedPosition = 0 private var cachedPosition = 0
private var proxy: StreamProxy? = null 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 bufferTask: CancellableTask? = null
private var positionCache: PositionCache? = null private var positionCache: PositionCache? = null
private var secondaryProgress = -1 private var secondaryProgress = -1
@ -96,7 +87,7 @@ class LocalMediaPlayer(
Thread { Thread {
Thread.currentThread().name = "MediaPlayerThread" Thread.currentThread().name = "MediaPlayerThread"
Looper.prepare() Looper.prepare()
mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) mediaPlayer.setWakeMode(context, PARTIAL_WAKE_LOCK)
mediaPlayer.setOnErrorListener { _, what, more -> mediaPlayer.setOnErrorListener { _, what, more ->
handleError( handleError(
Exception( Exception(
@ -129,7 +120,6 @@ class LocalMediaPlayer(
wakeLock.setReferenceCounted(false) wakeLock.setReferenceCounted(false)
Util.registerMediaButtonEventReceiver(context, true) Util.registerMediaButtonEventReceiver(context, true)
setUpRemoteControlClient()
Timber.i("LocalMediaPlayer created") Timber.i("LocalMediaPlayer created")
} }
@ -156,8 +146,6 @@ class LocalMediaPlayer(
if (nextPlayingTask != null) { if (nextPlayingTask != null) {
nextPlayingTask!!.cancel() nextPlayingTask!!.cancel()
} }
audioManager.unregisterRemoteControlClient(remoteControlClient)
clearRemoteControl()
Util.unregisterMediaButtonEventReceiver(context, true) Util.unregisterMediaButtonEventReceiver(context, true)
wakeLock.release() wakeLock.release()
} catch (exception: Throwable) { } catch (exception: Throwable) {
@ -173,13 +161,12 @@ class LocalMediaPlayer(
if (playerState === PlayerState.STARTED) { if (playerState === PlayerState.STARTED) {
audioFocusHandler.requestAudioFocus() audioFocusHandler.requestAudioFocus()
} }
if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) {
updateRemoteControl()
}
if (onPlayerStateChanged != null) { if (onPlayerStateChanged != null) {
val mainHandler = Handler(context.mainLooper) val mainHandler = Handler(context.mainLooper)
val myRunnable = Runnable { val myRunnable = Runnable {
onPlayerStateChanged!!.accept(playerState, currentPlaying) onPlayerStateChanged!!(playerState, currentPlaying)
} }
mainHandler.post(myRunnable) mainHandler.post(myRunnable)
} }
@ -200,11 +187,10 @@ class LocalMediaPlayer(
fun setCurrentPlaying(currentPlaying: DownloadFile?) { fun setCurrentPlaying(currentPlaying: DownloadFile?) {
Timber.v("setCurrentPlaying %s", currentPlaying) Timber.v("setCurrentPlaying %s", currentPlaying)
this.currentPlaying = currentPlaying this.currentPlaying = currentPlaying
updateRemoteControl()
if (onCurrentPlayingChanged != null) { if (onCurrentPlayingChanged != null) {
val mainHandler = Handler(context.mainLooper) val mainHandler = Handler(context.mainLooper)
val myRunnable = Runnable { onCurrentPlayingChanged!!.accept(currentPlaying) } val myRunnable = Runnable { onCurrentPlayingChanged!!(currentPlaying) }
mainHandler.post(myRunnable) 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 @Synchronized
fun seekTo(position: Int) { fun seekTo(position: Int) {
try { try {
mediaPlayer.seekTo(position) mediaPlayer.seekTo(position)
cachedPosition = position cachedPosition = position
updateRemoteControl()
} catch (x: Exception) { } catch (x: Exception) {
handleError(x) handleError(x)
} }
@ -504,7 +361,7 @@ class LocalMediaPlayer(
secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works
setPlayerState(PlayerState.IDLE) setPlayerState(PlayerState.IDLE)
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC) setAudioAttributes(mediaPlayer)
var dataSource = file.path var dataSource = file.path
if (partial) { if (partial) {
@ -568,7 +425,9 @@ class LocalMediaPlayer(
} }
} }
postRunnable(onPrepared) postRunnable {
onPrepared
}
} }
attachHandlersToPlayer(mediaPlayer, downloadFile, partial) attachHandlersToPlayer(mediaPlayer, downloadFile, partial)
mediaPlayer.prepareAsync() 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 @Synchronized
private fun setupNext(downloadFile: DownloadFile) { private fun setupNext(downloadFile: DownloadFile) {
try { try {
val file = downloadFile.completeOrPartialFile 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!!.setOnCompletionListener(null)
nextMediaPlayer!!.release() nextMediaPlayer!!.release()
nextMediaPlayer = null nextMediaPlayer = null
} }
nextMediaPlayer = MediaPlayer() 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 { try {
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
} catch (e: Throwable) { } catch (e: Throwable) {
nextMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
} }
nextMediaPlayer!!.setDataSource(file.path) nextMediaPlayer!!.setDataSource(file.path)
setNextPlayerState(PlayerState.PREPARING) setNextPlayerState(PlayerState.PREPARING)
nextMediaPlayer!!.setOnPreparedListener { nextMediaPlayer!!.setOnPreparedListener {
@ -664,7 +538,7 @@ class LocalMediaPlayer(
} else { } else {
if (onSongCompleted != null) { if (onSongCompleted != null) {
val mainHandler = Handler(context.mainLooper) val mainHandler = Handler(context.mainLooper)
val myRunnable = Runnable { onSongCompleted!!.accept(currentPlaying) } val myRunnable = Runnable { onSongCompleted!!(currentPlaying) }
mainHandler.post(myRunnable) mainHandler.post(myRunnable)
} }
} }

View File

@ -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()
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -39,8 +39,11 @@
<string name="common.name">Name</string> <string name="common.name">Name</string>
<string name="common.ok">OK</string> <string name="common.ok">OK</string>
<string name="common.pin">Pin</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_last">Play Last</string>
<string name="common.play_next">Play Next</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_now">Play Now</string>
<string name="common.play_shuffled">Play Shuffled</string> <string name="common.play_shuffled">Play Shuffled</string>
<string name="common.public">Public</string> <string name="common.public">Public</string>