Compare commits

..

No commits in common. "2df8d049d0a1e913e3a97cee129e8c05ed98b2b3" and "71168983b64d289e3a5b34c820863067193e86e2" have entirely different histories.

10 changed files with 205 additions and 197 deletions

View File

@ -22,7 +22,6 @@
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:hasFragileUserData="true" tools:targetApi="q"
android:dataExtractionRules="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"

View File

@ -0,0 +1,39 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.util;
import android.os.Binder;
/**
* @author Sindre Mehus
*/
public class SimpleServiceBinder<S> extends Binder
{
private final S service;
public SimpleServiceBinder(S service)
{
this.service = service;
}
public S getService()
{
return service;
}
}

View File

@ -11,7 +11,6 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData
import androidx.media3.common.HeartRating
import androidx.media3.common.StarRating
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.CircularProgressIndicator
import io.reactivex.rxjava3.disposables.CompositeDisposable
@ -140,17 +139,7 @@ class TrackViewHolder(val view: View) :
updateStatus(it.state, it.progress)
}
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
// Ignore updates which are not for the current song
if (it.id != song.id) return@subscribe
if (it.rating is HeartRating) {
updateSingleStar(it.rating.isHeart)
} else if (it.rating is StarRating) {
updateFiveStars(it.rating.starRating.toInt())
}
}
// Timber.v("Setting song done")
}
// This is called when the Holder is recycled and receives a new Song

View File

@ -225,7 +225,7 @@ class PlayerFragment :
fiveStar5ImageView = view.findViewById(R.id.song_five_star_5)
}
@Suppress("LongMethod")
@Suppress("LongMethod", "DEPRECATION")
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
cancellationToken = CancellationToken()
@ -235,7 +235,6 @@ class PlayerFragment :
val width: Int
val height: Int
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val bounds = windowManager.currentWindowMetrics.bounds
width = bounds.width()

View File

@ -15,11 +15,8 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -58,7 +55,8 @@ import timber.log.Timber
/**
* Initiates a search on the media library and displays the results
* TODO: Switch to material3 class
*
* TODO: Implement the search field without using the deprecated OptionsMenu calls
*/
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
private var searchResult: SearchResult? = null
@ -82,13 +80,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
super.onViewCreated(view, savedInstanceState)
cancellationToken = CancellationToken()
setTitle(this, R.string.search_title)
// Register our options menu
(requireActivity() as MenuHost).addMenuProvider(
menuProvider,
viewLifecycleOwner,
Lifecycle.State.RESUMED
)
setHasOptionsMenu(true)
listModel.searchResult.observe(
viewLifecycleOwner
@ -149,24 +141,12 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
}
/**
* This provide creates the search bar above the recycler view
* This method creates the search bar above the recycler view
*/
private val menuProvider: MenuProvider = object : MenuProvider {
override fun onPrepareMenu(menu: Menu) {
setupOptionsMenu(menu)
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.search, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return true
}
}
fun setupOptionsMenu(menu: Menu) {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val activity = activity ?: return
val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager
inflater.inflate(R.menu.search, menu)
val searchItem = menu.findItem(R.id.search_item)
searchView = searchItem.actionView as SearchView
val searchableInfo = searchManager.getSearchableInfo(requireActivity().componentName)
@ -295,7 +275,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
id = item.id,
name = item.name,
parentId = item.id,
isArtist = false
isArtist = (item is Artist)
)
} else {
SearchFragmentDirections.searchToAlbumsList(

View File

@ -12,11 +12,8 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
@ -117,13 +114,7 @@ open class TrackCollectionFragment(
setupButtons(view)
registerForContextMenu(listView!!)
// Register our options menu
(requireActivity() as MenuHost).addMenuProvider(
menuProvider,
viewLifecycleOwner,
Lifecycle.State.RESUMED
)
setHasOptionsMenu(true)
// Create a View Manager
viewManager = LinearLayoutManager(this.context)
@ -266,39 +257,41 @@ open class TrackCollectionFragment(
}
}
private val menuProvider: MenuProvider = object : MenuProvider {
override fun onPrepareMenu(menu: Menu) {
playAllButton = menu.findItem(R.id.select_album_play_all)
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
playAllButton = menu.findItem(R.id.select_album_play_all)
if (playAllButton != null) {
playAllButton!!.isVisible = playAllButtonVisible
}
shareButton = menu.findItem(R.id.menu_item_share)
if (shareButton != null) {
shareButton!!.isVisible = shareButtonVisible
}
if (playAllButton != null) {
playAllButton!!.isVisible = playAllButtonVisible
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.select_album, menu)
shareButton = menu.findItem(R.id.menu_item_share)
if (shareButton != null) {
shareButton!!.isVisible = shareButtonVisible
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.select_album, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val itemId = item.itemId
if (itemId == R.id.select_album_play_all) {
playAll()
return true
} else if (itemId == R.id.menu_item_share) {
shareHandler.createShare(
this, getSelectedSongs(),
refreshListView, cancellationToken!!,
navArgs.id
)
return true
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.select_album_play_all) {
playAll()
return true
} else if (item.itemId == R.id.menu_item_share) {
shareHandler.createShare(
this@TrackCollectionFragment, getSelectedSongs(),
refreshListView, cancellationToken!!,
navArgs.id
)
return true
}
return false
}
return false
}
override fun onDestroyView() {
@ -386,17 +379,20 @@ open class TrackCollectionFragment(
private fun selectAllOrNone() {
val someUnselected = viewAdapter.selectedSet.size < childCount
selectAll(someUnselected)
selectAll(someUnselected, true)
}
private fun selectAll(selected: Boolean) {
private fun selectAll(selected: Boolean, toast: Boolean) {
var selectedCount = viewAdapter.selectedSet.size * -1
selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
// Display toast: N tracks selected
val toastResId = R.string.select_album_n_selected
Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0)))
if (toast) {
val toastResId = R.string.select_album_n_selected
Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0)))
}
}
@Synchronized
@ -579,7 +575,7 @@ open class TrackCollectionFragment(
setTitle(R.string.main_videos)
listModel.getVideos(refresh2)
} else if (id == null || getRandomTracks) {
// There seems to be a bug in ViewPager when resuming the Activity that sub-fragments
// There seems to be a bug in ViewPager when resuming the Actitivy that subfragments
// arguments are empty. If we have no id, just show some random tracks
setTitle(R.string.main_songs_random)
listModel.getRandom(size, append)
@ -640,6 +636,10 @@ open class TrackCollectionFragment(
R.id.song_menu_download -> {
downloadBackground(false, songs)
}
R.id.select_album_play_all -> {
// TODO: Why is this being handled here?!
playAll()
}
R.id.song_menu_share -> {
if (item is Track) {
shareHandler.createShare(

View File

@ -9,6 +9,8 @@ package org.moire.ultrasonic.playback
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
@ -18,7 +20,6 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
import androidx.media3.common.Rating
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
@ -26,6 +27,7 @@ import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionResult.RESULT_SUCCESS
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.CoroutineScope
@ -47,6 +49,7 @@ import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.RatingManager
import org.moire.ultrasonic.util.MainThreadExecutor
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.buildMediaItem
import org.moire.ultrasonic.util.toMediaItem
@ -89,6 +92,7 @@ private const val DISPLAY_LIMIT = 100
private const val SEARCH_LIMIT = 10
// List of available custom SessionCommands
const val SESSION_CUSTOM_SET_RATING = "SESSION_CUSTOM_SET_RATING"
const val PLAY_COMMAND = "play "
/**
@ -115,24 +119,6 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
private val isOffline get() = ActiveServerProvider.isOffline()
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
private var customCommands: List<CommandButton>
internal var customLayout = ImmutableList.of<CommandButton>()
init {
customCommands =
listOf(
// This button is used for an unstarred track, and its action will star the track
getHeartCommandButton(
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY)
),
// This button is used for an starred track, and its action will unstar the track
getHeartCommandButton(
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY)
)
)
customLayout = ImmutableList.of(customCommands[0])
}
/**
* Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link
* MediaBrowser#getLibraryRoot(LibraryParams)}.
@ -190,10 +176,11 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
for (commandButton in customCommands) {
// Add custom command to available session commands.
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
/*
* TODO: Currently we need to create a custom session command, see https://github.com/androidx/media/issues/107
* When this issue is fixed we should be able to remove this method again
*/
availableSessionCommands.add(SessionCommand(SESSION_CUSTOM_SET_RATING, Bundle()))
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
@ -201,28 +188,6 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
)
}
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
// Let Media3 controller (for instance the MediaNotificationProvider)
// know about the custom layout right after it connected.
session.setCustomLayout(customLayout)
}
}
private fun getHeartCommandButton(sessionCommand: SessionCommand): CommandButton {
val willHeart =
(sessionCommand.customAction == PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON)
return CommandButton.Builder()
.setDisplayName("Love")
.setIconResId(
if (willHeart) R.drawable.ic_star_hollow
else R.drawable.ic_star_full
)
.setSessionCommand(sessionCommand)
.setEnabled(true)
.build()
}
override fun onGetItem(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
@ -236,12 +201,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
// Create LRU Cache of MediaItems, fill it in the other calls
// and retrieve it here.
return if (mediaItem != null) {
Futures.immediateFuture(
if (mediaItem != null) {
return Futures.immediateFuture(
LibraryResult.ofItem(mediaItem, null)
)
} else {
Futures.immediateFuture(
return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
}
@ -269,13 +234,40 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
var customCommandFuture: ListenableFuture<SessionResult>? = null
when (customCommand.customAction) {
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> {
customCommandFuture = onSetRating(session, controller, HeartRating(true))
updateCustomHeartButton(session, true)
}
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> {
customCommandFuture = onSetRating(session, controller, HeartRating(false))
updateCustomHeartButton(session, false)
SESSION_CUSTOM_SET_RATING -> {
/*
* It is currently not possible to edit a MediaItem after creation so the isRated value
* is stored in the track.starred value
* See https://github.com/androidx/media/issues/33
*/
val track = mediaPlayerManager.currentMediaItem?.toTrack()
if (track != null) {
customCommandFuture = onSetRating(
session,
controller,
HeartRating(!track.starred)
)
Futures.addCallback(
customCommandFuture,
object : FutureCallback<SessionResult> {
override fun onSuccess(result: SessionResult) {
track.starred = !track.starred
// This needs to be called on the main Thread
// TODO: This is a looping reference
libraryService.onUpdateNotification(session)
}
override fun onFailure(t: Throwable) {
Toast.makeText(
mediaPlayerManager.context,
"There was an error updating the rating",
LENGTH_SHORT
).show()
}
},
MainThreadExecutor()
)
}
}
else -> {
Timber.d(
@ -289,21 +281,19 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
return customCommandFuture
return super.onCustomCommand(session, controller, customCommand, args)
}
override fun onSetRating(
session: MediaSession,
controller: MediaSession.ControllerInfo,
rating: Rating
): ListenableFuture<SessionResult> {
val mediaItem = session.player.currentMediaItem
if (mediaItem != null) {
mediaItem.toTrack().starred = (rating as HeartRating).isHeart
if (session.player.currentMediaItem != null)
return onSetRating(
session,
controller,
mediaItem.mediaId,
session.player.currentMediaItem!!.mediaId,
rating
)
}
return super.onSetRating(session, controller, rating)
}
@ -313,9 +303,6 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
mediaId: String,
rating: Rating
): ListenableFuture<SessionResult> {
// TODO: Through this methods it is possible to set a rating on an arbitrary MediaItem.
// Right now the ratings are submitted, yet the underlying track is only updated when
// coming from the other onSetRating(session, controller, rating)
return serviceScope.future {
Timber.i(controller.packageName)
// This function even though its declared in AutoMediaBrowserCallback.kt is
@ -337,6 +324,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
* and thereby customarily it is required to rebuild it..
* See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error
*/
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
@ -1288,15 +1276,4 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
null
}
}
fun updateCustomHeartButton(
session: MediaSession,
isHeart: Boolean
) {
val command = if (isHeart) customCommands[1] else customCommands[0]
// Change the custom layout to contain the right heart button
customLayout = ImmutableList.of(command)
// Send the updated custom layout to controllers.
session.setCustomLayout(customLayout)
}
}

View File

@ -7,22 +7,79 @@
package org.moire.ultrasonic.playback
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.media3.common.HeartRating
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaNotification
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import com.google.common.collect.ImmutableList
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.util.toTrack
@UnstableApi
class CustomNotificationProvider(ctx: Context) :
DefaultMediaNotificationProvider(ctx),
KoinComponent {
// By default the skip buttons are not shown in compact view.
// We add the COMMAND_KEY_COMPACT_VIEW_INDEX to show them
// See also: https://github.com/androidx/media/issues/410
/*
* It is currently not possible to edit a MediaItem after creation so the isRated value
* is stored in the track.starred value. See https://github.com/androidx/media/issues/33
* TODO: Once the bug is fixed remove this circular reference!
*/
private val mediaPlayerManager by inject<MediaPlayerManager>()
override fun addNotificationActions(
mediaSession: MediaSession,
mediaButtons: ImmutableList<CommandButton>,
builder: NotificationCompat.Builder,
actionFactory: MediaNotification.ActionFactory
): IntArray {
val tmp: MutableList<CommandButton> = mutableListOf()
/*
* TODO:
* It is currently not possible to edit a MediaItem after creation so the isRated value
* is stored in the track.starred value
* See https://github.com/androidx/media/issues/33
*/
val rating = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let {
HeartRating(
it
)
}
if (rating is HeartRating) {
tmp.add(
CommandButton.Builder()
.setDisplayName("Love")
.setIconResId(
if (rating.isHeart) R.drawable.ic_star_full
else R.drawable.ic_star_hollow
)
.setSessionCommand(
SessionCommand(
SESSION_CUSTOM_SET_RATING,
HeartRating(rating.isHeart).toBundle()
)
)
.setExtras(HeartRating(rating.isHeart).toBundle())
.setEnabled(true)
.build()
)
}
return super.addNotificationActions(
mediaSession,
ImmutableList.copyOf((mediaButtons + tmp)),
builder,
actionFactory
)
}
override fun getMediaButtons(
session: MediaSession,
playerCommands: Player.Commands,

View File

@ -68,7 +68,7 @@ class PlaybackService :
private var equalizer: EqualizerController? = null
private val activeServerProvider: ActiveServerProvider by inject()
private lateinit var librarySessionCallback: AutoMediaBrowserCallback
private lateinit var librarySessionCallback: MediaLibrarySession.Callback
private var rxBusSubscription = CompositeDisposable()
@ -132,13 +132,6 @@ class PlaybackService :
setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext()))
// TODO: Remove minor code duplication with updateBackend()
val desiredBackend = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
MediaPlayerManager.PlayerBackend.JUKEBOX
} else {
MediaPlayerManager.PlayerBackend.LOCAL
}
player = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
Timber.i("Jukebox enabled by default")
getJukeboxPlayer()
@ -146,8 +139,6 @@ class PlaybackService :
getLocalPlayer()
}
actualBackend = desiredBackend
// Create browser interface
librarySessionCallback = AutoMediaBrowserCallback(this)
@ -157,11 +148,6 @@ class PlaybackService :
.setBitmapLoader(ArtworkBitmapLoader())
.build()
if (!librarySessionCallback.customLayout.isEmpty()) {
// Send custom layout to legacy session.
mediaLibrarySession.setCustomLayout(librarySessionCallback.customLayout)
}
// Set a listener to update the API client when the active server has changed
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
// Set the player wake mode
@ -223,7 +209,6 @@ class PlaybackService :
player.addListener(listener)
mediaLibrarySession.player = player
actualBackend = newBackend
}
@ -296,14 +281,7 @@ class PlaybackService :
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
// Since we cannot update the metadata of the media item after creation,
// we cannot set change the rating on it
// Therefore the track must be our source of truth
val track = mediaItem?.toTrack()
if (track != null) {
updateCustomHeartButton(track.starred)
}
updateWidgetTrack(track)
updateWidgetTrack(mediaItem?.toTrack())
cacheNextSongs()
}
@ -313,10 +291,6 @@ class PlaybackService :
}
}
private fun updateCustomHeartButton(isHeart: Boolean) {
librarySessionCallback.updateCustomHeartButton(mediaLibrarySession, isHeart)
}
private fun cacheNextSongs() {
if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return
Timber.d("PlaybackService caching the next songs")
@ -420,10 +394,6 @@ class PlaybackService :
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic.error"
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages"
const val CUSTOM_COMMAND_TOGGLE_HEART_ON =
"org.moire.ultrasonic.HEART_ON"
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF =
"org.moire.ultrasonic.HEART_OFF"
private const val NOTIFICATION_ID = 3009
}
}

View File

@ -11,7 +11,6 @@ import android.app.Notification
import android.app.Service
import android.content.Intent
import android.net.wifi.WifiManager
import android.os.Binder
import android.os.Build
import android.os.Handler
import android.os.IBinder
@ -40,6 +39,7 @@ import org.moire.ultrasonic.util.FileUtil.getCompleteFile
import org.moire.ultrasonic.util.FileUtil.getPartialFile
import org.moire.ultrasonic.util.FileUtil.getPinnedFile
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.SimpleServiceBinder
import org.moire.ultrasonic.util.Storage
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification
@ -452,5 +452,3 @@ class DownloadService : Service(), KoinComponent {
}
}
}
class SimpleServiceBinder<S>(val service: S) : Binder()