mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Ask for record audio permission when enabling visualizer
This commit is contained in:
parent
c91ac434e0
commit
8aa64c6fef
5 changed files with 184 additions and 28 deletions
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -38,11 +39,15 @@ fun MusicViewOptionsDialog(
|
|||
appPreferences: AppPreferences,
|
||||
onDismissRequest: () -> Unit,
|
||||
onViewOptionsChange: (AppPreferences) -> Unit,
|
||||
onEnableVisualizer: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
val columnState = rememberLazyListState()
|
||||
val items = remember { getMusicPreferences() }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties =
|
||||
|
|
@ -55,6 +60,7 @@ fun MusicViewOptionsDialog(
|
|||
window.setGravity(Gravity.END)
|
||||
window.setDimAmount(0f)
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.view_options),
|
||||
|
|
@ -79,18 +85,48 @@ fun MusicViewOptionsDialog(
|
|||
pref as AppPreference<AppPreferences, Any>
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val value = pref.getter.invoke(appPreferences)
|
||||
ComposablePreference(
|
||||
preference = pref,
|
||||
value = value,
|
||||
onNavigate = {},
|
||||
onValueChange = { newValue ->
|
||||
onViewOptionsChange.invoke(pref.setter(appPreferences, newValue))
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
onClickPreference = { pref ->
|
||||
},
|
||||
)
|
||||
// Using title is a bit hacky
|
||||
when (pref.title) {
|
||||
R.string.show_visualizer -> {
|
||||
ComposablePreference(
|
||||
preference = pref,
|
||||
value = value,
|
||||
onNavigate = {},
|
||||
onValueChange = { newValue ->
|
||||
newValue as Boolean
|
||||
if (newValue) {
|
||||
onEnableVisualizer.invoke()
|
||||
} else {
|
||||
onViewOptionsChange.invoke(
|
||||
pref.setter(
|
||||
appPreferences,
|
||||
newValue,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
onClickPreference = { pref ->
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ComposablePreference(
|
||||
preference = pref,
|
||||
value = value,
|
||||
onNavigate = {},
|
||||
onValueChange = { newValue ->
|
||||
onViewOptionsChange.invoke(pref.setter(appPreferences, newValue))
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
onClickPreference = { pref ->
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.music
|
||||
|
||||
import android.Manifest
|
||||
import android.view.Gravity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
|
|
@ -41,6 +44,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
|
|
@ -49,9 +53,11 @@ import com.github.damontecres.wholphin.data.model.AudioItem
|
|||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.rememberQueue
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.findActivity
|
||||
import com.github.damontecres.wholphin.ui.playback.BottomDialog
|
||||
import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
||||
|
|
@ -141,6 +147,14 @@ fun NowPlayingPage(
|
|||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
|
||||
var showRationaleDialog by remember { mutableStateOf(false) }
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
) { isGranted ->
|
||||
viewModel.startVisualizer(isGranted, true)
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
Box(
|
||||
modifier =
|
||||
|
|
@ -317,6 +331,34 @@ fun NowPlayingPage(
|
|||
appPreferences = preferences,
|
||||
onDismissRequest = { showViewOptionsDialog = false },
|
||||
onViewOptionsChange = { viewModel.updatePreferences(it) },
|
||||
onEnableVisualizer = {
|
||||
val showRationale =
|
||||
context
|
||||
.findActivity()
|
||||
?.shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO) == true
|
||||
when {
|
||||
!state.visualizerPermissions && showRationale -> {
|
||||
showRationaleDialog = true
|
||||
}
|
||||
|
||||
!state.visualizerPermissions -> {
|
||||
launcher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
else -> {
|
||||
viewModel.startVisualizer(true, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showRationaleDialog) {
|
||||
RecordAudioRationaleDialog(
|
||||
onDismissRequest = { showRationaleDialog = false },
|
||||
onClick = {
|
||||
showRationaleDialog = false
|
||||
launcher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -356,3 +398,31 @@ fun NowPlayingBottomDialog(
|
|||
gravity = Gravity.START,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordAudioRationaleDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
BasicDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.visualizer_rationale),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.continue_string),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ data class NowPlayingState(
|
|||
val musicServiceState: MusicServiceState,
|
||||
val lyrics: LyricDto? = null,
|
||||
val currentLyricIndex: Int? = null,
|
||||
val visualizerPermissions: Boolean = false,
|
||||
) {
|
||||
val hasLyrics: Boolean get() = lyrics != null && lyrics.lyrics.isNotEmpty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.music
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.audiofx.Visualizer
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -11,6 +15,7 @@ import com.github.damontecres.wholphin.data.model.AudioItem
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.updateMusicPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.MusicService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
|
|
@ -24,13 +29,17 @@ import com.mayakapps.kache.InMemoryKache
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.lyricsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
|
|
@ -48,6 +57,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||
class NowPlayingViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val musicService: MusicService,
|
||||
private val backdropService: BackdropService,
|
||||
|
|
@ -62,6 +72,7 @@ class NowPlayingViewModel
|
|||
fun create(): NowPlayingViewModel
|
||||
}
|
||||
|
||||
private val visualizerMutex = Mutex()
|
||||
private var visualizer: Visualizer? = null
|
||||
|
||||
val controllerViewState =
|
||||
|
|
@ -87,25 +98,16 @@ class NowPlayingViewModel
|
|||
player.removeListener(this)
|
||||
visualizer?.release()
|
||||
}
|
||||
val visualizerPermissions =
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
startVisualizer(visualizerPermissions, false)
|
||||
viewModelScope.launchDefault {
|
||||
musicService.state.collectLatest { musicServiceState ->
|
||||
if (musicServiceState.status != NowPlayingStatus.IDLE) {
|
||||
viewModelScope.launchDefault {
|
||||
if (visualizer == null) {
|
||||
visualizer =
|
||||
Visualizer(onMain { player.audioSessionId }).apply {
|
||||
captureSize = Visualizer.getCaptureSizeRange()[1]
|
||||
setDataCaptureListener(
|
||||
this@NowPlayingViewModel,
|
||||
Visualizer.getMaxCaptureRate() / 2,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
visualizer?.enabled =
|
||||
musicServiceState.status == NowPlayingStatus.PLAYING
|
||||
}
|
||||
visualizer?.enabled = musicServiceState.status == NowPlayingStatus.PLAYING
|
||||
}
|
||||
|
||||
state.update { it.copy(musicServiceState = musicServiceState) }
|
||||
|
|
@ -294,4 +296,49 @@ class NowPlayingViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVisualizer() {
|
||||
viewModelScope.launchDefault {
|
||||
visualizerMutex.withLock {
|
||||
val prefs = preferencesDataStore.data.first()
|
||||
if (visualizer == null &&
|
||||
state.value.visualizerPermissions &&
|
||||
prefs.musicPreferences.showVisualizer
|
||||
) {
|
||||
Timber.v("Creating visualizer")
|
||||
visualizer =
|
||||
Visualizer(onMain { player.audioSessionId }).apply {
|
||||
captureSize = Visualizer.getCaptureSizeRange()[1]
|
||||
setDataCaptureListener(
|
||||
this@NowPlayingViewModel,
|
||||
Visualizer.getMaxCaptureRate() / 2,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startVisualizer(
|
||||
permissionGranted: Boolean,
|
||||
updatePreferences: Boolean,
|
||||
) {
|
||||
Timber.v("startVisualizer: permissionGranted=%s", permissionGranted)
|
||||
state.update {
|
||||
it.copy(
|
||||
visualizerPermissions = permissionGranted,
|
||||
)
|
||||
}
|
||||
viewModelScope.launchDefault {
|
||||
if (updatePreferences || !permissionGranted) {
|
||||
preferencesDataStore.updateData {
|
||||
it.updateMusicPreferences { showVisualizer = permissionGranted }
|
||||
}
|
||||
}
|
||||
initVisualizer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -742,6 +742,8 @@
|
|||
<string name="show_visualizer">Show visualizer</string>
|
||||
<string name="show_backdrop">Show backdrop</string>
|
||||
<string name="play_as_type">Play as?</string>
|
||||
<string name="visualizer_rationale">Visualizing the currently playing audio requires permission to record audio.</string>
|
||||
<string name="continue_string">Continue</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue