mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 08:12:26 +01:00
calls: end and mute from the nav bar, with the call's name and time in place of the top banner
This commit is contained in:
1 parent
a26605c0ab
commit
6945f6755e
5 files changed
+130
-45
No files matched your search
@@ -23,19 +23,19 @@ data class Caller(val number: String, val name: String?) {
|
|||||||
sealed interface CallUiState {
|
sealed interface CallUiState {
|
||||||
data object None : CallUiState
|
data object None : CallUiState
|
||||||
data class Ringing(val caller: Caller, val canDecline: Boolean) : CallUiState
|
data class Ringing(val caller: Caller, val canDecline: Boolean) : CallUiState
|
||||||
data class Active(val caller: Caller?) : CallUiState
|
/** [since] is the uptime the call went off-hook, for the timer. */
|
||||||
|
data class Active(val caller: Caller?, val since: Long) : CallUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the phone's call state for the car screen, and answers or declines through the two
|
* Tracks the phone's call state for the car screen, and answers or declines through the
|
||||||
* doors a sideloaded app can use: [android.telecom.TelecomManager.acceptRingingCall] to answer,
|
* doors a sideloaded app can use: [android.telecom.TelecomManager.acceptRingingCall] to answer,
|
||||||
* and [GearslipCallScreening]'s response to decline one that is still ringing.
|
* [GearslipCallScreening]'s response to decline one that is still ringing, and
|
||||||
|
* [android.telecom.TelecomManager.endCall] to hang up, which needs only ANSWER_PHONE_CALLS for an
|
||||||
|
* ordinary carrier call. Mute is the phone's microphone mute, put back when the call ends.
|
||||||
*
|
*
|
||||||
* Ending a call already answered needs MODIFY_PHONE_STATE, a privileged permission Gearslip
|
* Call audio stays on the phone's normal voice path; nothing here sends it over the USB link,
|
||||||
* cannot hold, so a call taken here is finished the way it would be without Gearslip at all -
|
* which only ever carries media playback.
|
||||||
* from the phone itself, a paired headset, or the car's own Bluetooth if it has any. Call audio
|
|
||||||
* stays on the phone's normal voice path too; nothing here sends it over the USB link, which
|
|
||||||
* only ever carries media playback.
|
|
||||||
*/
|
*/
|
||||||
object CarCalls {
|
object CarCalls {
|
||||||
|
|
||||||
@@ -47,9 +47,14 @@ object CarCalls {
|
|||||||
private var pendingRespond: ((allow: Boolean) -> Unit)? = null
|
private var pendingRespond: ((allow: Boolean) -> Unit)? = null
|
||||||
private var ringingCaller: Caller? = null
|
private var ringingCaller: Caller? = null
|
||||||
|
|
||||||
|
private val _muted = MutableStateFlow(false)
|
||||||
|
val muted: StateFlow<Boolean> = _muted.asStateFlow()
|
||||||
|
private var audio: android.media.AudioManager? = null
|
||||||
|
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
if (watcher != null) return
|
if (watcher != null) return
|
||||||
if (context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) return
|
if (context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) return
|
||||||
|
audio = context.getSystemService(android.media.AudioManager::class.java)
|
||||||
val manager = context.getSystemService(TelephonyManager::class.java) ?: return
|
val manager = context.getSystemService(TelephonyManager::class.java) ?: return
|
||||||
val callback = object : TelephonyCallback(), TelephonyCallback.CallStateListener {
|
val callback = object : TelephonyCallback(), TelephonyCallback.CallStateListener {
|
||||||
override fun onCallStateChanged(state: Int) = onPhoneState(state)
|
override fun onCallStateChanged(state: Int) = onPhoneState(state)
|
||||||
@@ -66,6 +71,7 @@ object CarCalls {
|
|||||||
watcher = null
|
watcher = null
|
||||||
pendingRespond = null
|
pendingRespond = null
|
||||||
ringingCaller = null
|
ringingCaller = null
|
||||||
|
unmute()
|
||||||
_state.value = CallUiState.None
|
_state.value = CallUiState.None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +80,15 @@ object CarCalls {
|
|||||||
TelephonyManager.CALL_STATE_IDLE -> {
|
TelephonyManager.CALL_STATE_IDLE -> {
|
||||||
pendingRespond = null
|
pendingRespond = null
|
||||||
ringingCaller = null
|
ringingCaller = null
|
||||||
|
// A mute left on would silence the microphone for everything after the call.
|
||||||
|
unmute()
|
||||||
_state.value = CallUiState.None
|
_state.value = CallUiState.None
|
||||||
}
|
}
|
||||||
TelephonyManager.CALL_STATE_OFFHOOK -> {
|
TelephonyManager.CALL_STATE_OFFHOOK -> {
|
||||||
pendingRespond = null
|
pendingRespond = null
|
||||||
_state.value = CallUiState.Active(ringingCaller)
|
if (_state.value !is CallUiState.Active) {
|
||||||
|
_state.value = CallUiState.Active(ringingCaller, android.os.SystemClock.uptimeMillis())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TelephonyManager.CALL_STATE_RINGING -> {
|
TelephonyManager.CALL_STATE_RINGING -> {
|
||||||
// The number itself only ever arrives through the screening service; if this
|
// The number itself only ever arrives through the screening service; if this
|
||||||
@@ -109,6 +119,39 @@ object CarCalls {
|
|||||||
runCatching { respond(false) }.onFailure { GearslipLog.w("calls: decline failed: ${it.message}") }
|
runCatching { respond(false) }.onFailure { GearslipLog.w("calls: decline failed: ${it.message}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A call Gearslip itself is placing, so the call pill can say who it's to. */
|
||||||
|
fun outgoingCall(context: Context, number: String) {
|
||||||
|
ringingCaller = Caller(number, lookUpName(context, number))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun end(context: Context) {
|
||||||
|
if (context.checkSelfPermission(Manifest.permission.ANSWER_PHONE_CALLS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
GearslipLog.w("calls: ending a call needs ANSWER_PHONE_CALLS, which was never granted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val telecom = context.getSystemService(TelecomManager::class.java) ?: return
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
runCatching { telecom.endCall() }
|
||||||
|
.onSuccess { if (!it) GearslipLog.w("calls: the phone wouldn't end the call") }
|
||||||
|
.onFailure { GearslipLog.w("calls: end failed: ${it.message}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleMute() {
|
||||||
|
val manager = audio ?: return
|
||||||
|
val want = !_muted.value
|
||||||
|
runCatching { manager.isMicrophoneMute = want }
|
||||||
|
.onFailure { GearslipLog.w("calls: mute failed: ${it.message}") }
|
||||||
|
// Read back rather than trusted: some phones let the call app overrule it.
|
||||||
|
_muted.value = manager.isMicrophoneMute
|
||||||
|
if (_muted.value != want) GearslipLog.w("calls: the phone didn't take the mute change")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unmute() {
|
||||||
|
if (!_muted.value) return
|
||||||
|
runCatching { audio?.isMicrophoneMute = false }
|
||||||
|
_muted.value = false
|
||||||
|
}
|
||||||
|
|
||||||
fun answer(context: Context) {
|
fun answer(context: Context) {
|
||||||
if (context.checkSelfPermission(Manifest.permission.ANSWER_PHONE_CALLS) != PackageManager.PERMISSION_GRANTED) {
|
if (context.checkSelfPermission(Manifest.permission.ANSWER_PHONE_CALLS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
GearslipLog.w("calls: answer needs ANSWER_PHONE_CALLS, which was never granted")
|
GearslipLog.w("calls: answer needs ANSWER_PHONE_CALLS, which was never granted")
|
||||||
|
|||||||
@@ -33,21 +33,17 @@ import app.seb3thehacker.gearslip.call.CallUiState
|
|||||||
import app.seb3thehacker.gearslip.call.CarCalls
|
import app.seb3thehacker.gearslip.call.CarCalls
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sits above whatever the car is showing while a call is ringing or under way. Ringing takes the
|
* Takes the whole car screen while a call is ringing, because nothing else matters more right
|
||||||
* whole screen, because nothing else matters more right now; an active call is a thin band along
|
* now. A call under way moves into the nav bar instead (see NavCallPill), so it covers nothing.
|
||||||
* the top, since from here the driver can only see who it is, not end it - see [CarCalls].
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CallOverlay(modifier: Modifier = Modifier) {
|
fun CallOverlay() {
|
||||||
val state by CarCalls.state.collectAsState()
|
val state by CarCalls.state.collectAsState()
|
||||||
val context = LocalContext.current.applicationContext
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
// Covers the box regardless of where the caller aligned it, so the alignment below is
|
|
||||||
// only ever felt by the active-call banner, which is the one that isn't full-screen.
|
|
||||||
is CallUiState.Ringing -> IncomingCall(s, onAnswer = { CarCalls.answer(context) }, onDecline = CarCalls::decline)
|
is CallUiState.Ringing -> IncomingCall(s, onAnswer = { CarCalls.answer(context) }, onDecline = CarCalls::decline)
|
||||||
is CallUiState.Active -> ActiveCallBanner(s, modifier)
|
else -> {}
|
||||||
CallUiState.None -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,25 +106,3 @@ private fun CallButton(icon: ImageVector, label: String, color: Color, enabled:
|
|||||||
Text(label, style = MaterialTheme.typography.labelLarge)
|
Text(label, style = MaterialTheme.typography.labelLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActiveCallBanner(s: CallUiState.Active, modifier: Modifier = Modifier) {
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Icon(Icons.Filled.Call, contentDescription = null, modifier = Modifier.size(20.dp))
|
|
||||||
Spacer(Modifier.height(0.dp))
|
|
||||||
Text(
|
|
||||||
" On a call" + (s.caller?.let { " with ${it.label}" } ?: ""),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.filled.Call
|
||||||
|
import app.seb3thehacker.gearslip.call.CarCalls
|
||||||
|
import app.seb3thehacker.gearslip.call.CallUiState
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
@@ -241,7 +244,9 @@ fun CarUi() {
|
|||||||
}
|
}
|
||||||
// A full wait from when hiding becomes possible, not from whenever the last touch was.
|
// A full wait from when hiding becomes possible, not from whenever the last touch was.
|
||||||
LaunchedEffect(barMayHide) { if (barMayHide) controlsWake.touch() }
|
LaunchedEffect(barMayHide) { if (barMayHide) controlsWake.touch() }
|
||||||
val barShown = !keyboardVisible && (!barMayHide || controlsWake.navBarAwake)
|
// A call keeps the bar up whatever the setting: End and Mute live on it.
|
||||||
|
val onCall = CarCalls.state.collectAsState().value is CallUiState.Active
|
||||||
|
val barShown = !keyboardVisible && (!barMayHide || controlsWake.navBarAwake || onCall)
|
||||||
val homeInset = if (floatingBar && barShown) NAV_BAR_HEIGHT else 0.dp
|
val homeInset = if (floatingBar && barShown) NAV_BAR_HEIGHT else 0.dp
|
||||||
// Only the screen that asked: going Home or anywhere else ends it.
|
// Only the screen that asked: going Home or anywhere else ends it.
|
||||||
val fullscreen = CarFullscreen.active && screen != CarScreen.Home
|
val fullscreen = CarFullscreen.active && screen != CarScreen.Home
|
||||||
@@ -309,7 +314,7 @@ fun CarUi() {
|
|||||||
)
|
)
|
||||||
// Above the popup: a call is more urgent than any notification.
|
// Above the popup: a call is more urgent than any notification.
|
||||||
VoiceHelpOverlay(Modifier.align(Alignment.Center))
|
VoiceHelpOverlay(Modifier.align(Alignment.Center))
|
||||||
CallOverlay(Modifier.align(Alignment.TopCenter))
|
CallOverlay()
|
||||||
AssistantOverlay(Modifier.align(Alignment.BottomCenter).padding(bottom = homeInset))
|
AssistantOverlay(Modifier.align(Alignment.BottomCenter).padding(bottom = homeInset))
|
||||||
if (app.seb3thehacker.gearslip.BuildConfig.DEBUG) {
|
if (app.seb3thehacker.gearslip.BuildConfig.DEBUG) {
|
||||||
// Bottom corner, not top: a screen's own header almost always puts
|
// Bottom corner, not top: a screen's own header almost always puts
|
||||||
@@ -320,7 +325,7 @@ fun CarUi() {
|
|||||||
}
|
}
|
||||||
if (floatingBar || fullscreen) {
|
if (floatingBar || fullscreen) {
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
if (fullscreen) CarFullscreen.revealed else barShown,
|
if (fullscreen) CarFullscreen.revealed || onCall else barShown,
|
||||||
Modifier.align(Alignment.BottomCenter),
|
Modifier.align(Alignment.BottomCenter),
|
||||||
enter = androidx.compose.animation.slideInVertically { it },
|
enter = androidx.compose.animation.slideInVertically { it },
|
||||||
exit = androidx.compose.animation.slideOutVertically { it },
|
exit = androidx.compose.animation.slideOutVertically { it },
|
||||||
@@ -564,12 +569,16 @@ private fun CarNavBar(navigator: CarNavigator) {
|
|||||||
|
|
||||||
// Next turn first, off the map only; the player shares the space when it fits,
|
// Next turn first, off the map only; the player shares the space when it fits,
|
||||||
// and gives way to the turn when it doesn't.
|
// and gives way to the turn when it doesn't.
|
||||||
|
// A call takes the player's place until it ends.
|
||||||
val turn = rememberNextTurn().takeIf { navigator.current != CarScreen.Home }
|
val turn = rememberNextTurn().takeIf { navigator.current != CarScreen.Home }
|
||||||
|
val call = CarCalls.state.collectAsState().value as? CallUiState.Active
|
||||||
BoxWithConstraints(Modifier.weight(1f).padding(horizontal = 16.dp), contentAlignment = Alignment.Center) {
|
BoxWithConstraints(Modifier.weight(1f).padding(horizontal = 16.dp), contentAlignment = Alignment.Center) {
|
||||||
val roomForBoth = maxWidth >= TURN_AND_PLAYER_MIN_WIDTH
|
val roomForBoth = maxWidth >= TURN_AND_PLAYER_MIN_WIDTH
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
turn?.let { NavTurnPill(it) { navigator.home() } }
|
if (call == null || roomForBoth) turn?.let { NavTurnPill(it) { navigator.home() } }
|
||||||
if (turn == null || roomForBoth) {
|
if (call != null) {
|
||||||
|
NavCallPill(call, Modifier.weight(1f, fill = false))
|
||||||
|
} else if (turn == null || roomForBoth) {
|
||||||
NavNowPlaying(navigator, selected = navigator.current == CarScreen.Media, modifier = Modifier.weight(1f, fill = false))
|
NavNowPlaying(navigator, selected = navigator.current == CarScreen.Media, modifier = Modifier.weight(1f, fill = false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -836,6 +845,58 @@ private fun NavTurnPill(turn: NextTurn, onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The call under way: who, how long, and Mute and End right here. Ringing has its own screen. */
|
||||||
|
@Composable
|
||||||
|
private fun NavCallPill(call: CallUiState.Active, modifier: Modifier = Modifier) {
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
val muted by CarCalls.muted.collectAsState()
|
||||||
|
val elapsed by produceState(0L, call.since) {
|
||||||
|
while (true) {
|
||||||
|
value = (android.os.SystemClock.uptimeMillis() - call.since) / 1000
|
||||||
|
delay(1000 - (android.os.SystemClock.uptimeMillis() - call.since) % 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.height(44.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = scheme.secondaryContainer,
|
||||||
|
contentColor = scheme.onSecondaryContainer,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxHeight().padding(start = 12.dp, end = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Call, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||||
|
Text(
|
||||||
|
call.caller?.label ?: "On a call",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f, fill = false),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"%d:%02d".format(elapsed / 60, elapsed % 60),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
GsIconButton(
|
||||||
|
if (muted) MediaIcons.MicOff else MediaIcons.Mic,
|
||||||
|
if (muted) "Unmute" else "Mute",
|
||||||
|
CarCalls::toggleMute,
|
||||||
|
tone = if (muted) GsTone.Primary else GsTone.Neutral, size = 36.dp, iconSize = 22.dp,
|
||||||
|
)
|
||||||
|
GsIconButton(
|
||||||
|
MediaIcons.CallEnd, "End call", { CarCalls.end(context) },
|
||||||
|
colors = GsColors(scheme.error, scheme.onError), size = 36.dp, iconSize = 22.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** "Title - Artist" on one line. The text itself cuts it off, and only where the pill runs out of room. */
|
/** "Title - Artist" on one line. The text itself cuts it off, and only where the pill runs out of room. */
|
||||||
private fun nowPlayingLabel(title: String, artist: String): String =
|
private fun nowPlayingLabel(title: String, artist: String): String =
|
||||||
listOf(title.trim(), artist.trim()).filter { it.isNotEmpty() }.joinToString(" - ")
|
listOf(title.trim(), artist.trim()).filter { it.isNotEmpty() }.joinToString(" - ")
|
||||||
|
|||||||
@@ -691,6 +691,10 @@ internal object MediaIcons {
|
|||||||
val Fullscreen = icon("Fullscreen", "M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z")
|
val Fullscreen = icon("Fullscreen", "M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z")
|
||||||
val FullscreenExit = icon("FullscreenExit", "M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z")
|
val FullscreenExit = icon("FullscreenExit", "M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z")
|
||||||
|
|
||||||
|
val CallEnd = icon("CallEnd", "M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.87,1.12 -2.66,1.85 -0.18,0.18 -0.43,0.28 -0.7,0.28 -0.28,0 -0.53,-0.11 -0.71,-0.29L0.29,13.08c-0.18,-0.17 -0.29,-0.42 -0.29,-0.7 0,-0.28 0.11,-0.53 0.29,-0.71C3.34,8.78 7.46,7 12,7s8.66,1.78 11.71,4.67c0.18,0.18 0.29,0.43 0.29,0.71 0,0.28 -0.11,0.53 -0.29,0.71l-2.48,2.48c-0.18,0.18 -0.43,0.29 -0.71,0.29 -0.27,0 -0.52,-0.11 -0.7,-0.28 -0.79,-0.74 -1.69,-1.36 -2.67,-1.85 -0.33,-0.16 -0.56,-0.5 -0.56,-0.9v-3.1C15.15,9.25 13.6,9 12,9z")
|
||||||
|
val Mic = icon("Mic", "M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z")
|
||||||
|
val MicOff = icon("MicOff", "M19,11h-1.7c0,0.74 -0.16,1.43 -0.43,2.05l1.23,1.23c0.56,-0.98 0.9,-2.09 0.9,-3.28zM14.98,11.17c0,-0.06 0.02,-0.11 0.02,-0.17L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01L9,11c0,1.66 1.33,3 2.99,3 0.22,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73 4.27,3z")
|
||||||
|
|
||||||
private fun icon(name: String, path: String, fillType: PathFillType = PathFillType.NonZero) = ImageVector.Builder(
|
private fun icon(name: String, path: String, fillType: PathFillType = PathFillType.NonZero) = ImageVector.Builder(
|
||||||
name, 24.dp, 24.dp, 24f, 24f,
|
name, 24.dp, 24.dp, 24f, 24f,
|
||||||
).addPath(addPathNodes(path), pathFillType = fillType, fill = SolidColor(androidx.compose.ui.graphics.Color.Black)).build()
|
).addPath(addPathNodes(path), pathFillType = fillType, fill = SolidColor(androidx.compose.ui.graphics.Color.Black)).build()
|
||||||
|
|||||||
@@ -94,7 +94,10 @@ internal fun placeCall(context: Context, number: String) {
|
|||||||
}
|
}
|
||||||
val telecom = context.getSystemService(android.telecom.TelecomManager::class.java)
|
val telecom = context.getSystemService(android.telecom.TelecomManager::class.java)
|
||||||
runCatching { telecom!!.placeCall(Uri.fromParts("tel", number, null), null) }
|
runCatching { telecom!!.placeCall(Uri.fromParts("tel", number, null), null) }
|
||||||
.onSuccess { GearslipLog.i("calls: placing a call") }
|
.onSuccess {
|
||||||
|
GearslipLog.i("calls: placing a call")
|
||||||
|
app.seb3thehacker.gearslip.call.CarCalls.outgoingCall(context, number)
|
||||||
|
}
|
||||||
.onFailure { GearslipLog.w("calls: placing a call failed: ${it.message}") }
|
.onFailure { GearslipLog.w("calls: placing a call failed: ${it.message}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user