fix: notification actions, PiP hangup, PiP compact UI, and PiP remote actions

1. Notification: silence channel sound (CallAudioManager handles ringtone),
   use CallNotificationReceiver for accept/reject actions instead of
   launching MainActivity
2. Cancel call notification when call is accepted (Connecting state)
3. PiP: hang up call when PiP is dismissed (activity destroyed)
4. PiP: add RemoteAction buttons (hangup, mute toggle) since Compose
   buttons are not interactive in PiP mode
5. PiP: show compact UI with smaller avatar (48dp) and smaller text
   when in picture-in-picture mode

https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ
This commit is contained in:
Claude
2026-04-02 20:04:16 +00:00
parent ede5706582
commit 4526a16ff9
9 changed files with 458 additions and 72 deletions
+4
View File
@@ -269,6 +269,10 @@
android:name=".service.notifications.NotificationReplyReceiver"
android:exported="false" />
<receiver
android:name=".ui.call.CallNotificationReceiver"
android:exported="false" />
</application>
@@ -120,6 +120,7 @@ class CallController(
audioManager.stopRingbackTone()
audioManager.switchToCallAudioMode()
audioManager.acquireProximityWakeLock()
NotificationUtils.cancelCallNotification(context)
}
is CallState.Connected -> {
@@ -523,6 +523,9 @@ object NotificationUtils {
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = stringRes(applicationContext, R.string.app_notification_calls_channel_description)
// Silence the notification sound — CallAudioManager plays the ringtone
setSound(null, null)
enableVibration(false)
}
val notificationManager: NotificationManager =
@@ -544,8 +547,11 @@ object NotificationUtils {
val channel = getOrCreateCallChannel(applicationContext)
// Tapping the notification opens the CallActivity
val contentIntent =
Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() }
Intent(applicationContext, com.vitorpamplona.amethyst.ui.call.CallActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val contentPendingIntent =
PendingIntent.getActivity(
@@ -555,17 +561,29 @@ object NotificationUtils {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val fullScreenIntent =
Intent(applicationContext, MainActivity::class.java).apply {
data = uri.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
val acceptIntent =
Intent(applicationContext, com.vitorpamplona.amethyst.ui.call.CallNotificationReceiver::class.java).apply {
action = com.vitorpamplona.amethyst.ui.call.CallNotificationReceiver.ACTION_ACCEPT_CALL
}
val fullScreenPendingIntent =
PendingIntent.getActivity(
val acceptPendingIntent =
PendingIntent.getBroadcast(
applicationContext,
CALL_NOTIFICATION_ID + 1,
fullScreenIntent,
acceptIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val rejectIntent =
Intent(applicationContext, com.vitorpamplona.amethyst.ui.call.CallNotificationReceiver::class.java).apply {
action = com.vitorpamplona.amethyst.ui.call.CallNotificationReceiver.ACTION_REJECT_CALL
}
val rejectPendingIntent =
PendingIntent.getBroadcast(
applicationContext,
CALL_NOTIFICATION_ID + 2,
rejectIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
@@ -577,15 +595,16 @@ object NotificationUtils {
.setContentText(callerName)
.setLargeIcon(callerBitmap)
.setContentIntent(contentPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setFullScreenIntent(contentPendingIntent, true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setAutoCancel(true)
.setOngoing(true)
.setTimeoutAfter(60_000)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_reject), contentPendingIntent)
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_accept), fullScreenPendingIntent)
.setSilent(true)
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_reject), rejectPendingIntent)
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_accept), acceptPendingIntent)
notificationManager.notify("call", CALL_NOTIFICATION_ID, builder.build())
}
@@ -20,19 +20,51 @@
*/
package com.vitorpamplona.amethyst.ui.call
import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.util.Rational
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.call.CallState
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class CallActivity : AppCompatActivity() {
val isInPipMode = mutableStateOf(false)
private val pipActionReceiver =
object : BroadcastReceiver() {
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(
context: Context,
intent: Intent,
) {
when (intent.action) {
ACTION_PIP_HANGUP -> {
GlobalScope.launch { ActiveCallHolder.callManager?.hangup() }
}
ACTION_PIP_TOGGLE_MUTE -> {
ActiveCallHolder.callController?.toggleAudioMute()
updatePipParams()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
@@ -46,6 +78,8 @@ class CallActivity : AppCompatActivity() {
return
}
registerPipReceiver()
setContent {
AmethystTheme {
CallScreen(
@@ -53,6 +87,7 @@ class CallActivity : AppCompatActivity() {
callController = callController,
accountViewModel = accountViewModel,
onCallEnded = { finish() },
isInPipMode = isInPipMode.value,
)
}
}
@@ -65,12 +100,25 @@ class CallActivity : AppCompatActivity() {
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (!isInPictureInPictureMode) {
// User expanded from PiP — bring the full call UI back
isInPipMode.value = isInPictureInPictureMode
}
@OptIn(DelicateCoroutinesApi::class)
override fun onDestroy() {
unregisterPipReceiver()
// If the activity is being destroyed while still in an active call
// (e.g. user swiped PiP away), hang up the call.
val state = ActiveCallHolder.callManager?.state?.value
if (state is CallState.Connected || state is CallState.Connecting || state is CallState.Offering) {
GlobalScope.launch { ActiveCallHolder.callManager?.hangup() }
}
super.onDestroy()
}
private fun enterPipIfActive() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val callManager = ActiveCallHolder.callManager ?: return
val state = callManager.state.value
val isActive =
@@ -78,21 +126,103 @@ class CallActivity : AppCompatActivity() {
state is CallState.Connecting ||
state is CallState.Offering
if (isActive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isActive) {
try {
val params =
PictureInPictureParams
.Builder()
.setAspectRatio(Rational(9, 16))
.build()
enterPictureInPictureMode(params)
enterPictureInPictureMode(buildPipParams())
} catch (_: Exception) {
// PiP not supported or activity not in correct state
}
}
}
private fun updatePipParams() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
if (!isInPipMode.value) return
try {
setPictureInPictureParams(buildPipParams())
} catch (_: Exception) {
}
}
private fun buildPipParams(): PictureInPictureParams {
val builder =
PictureInPictureParams
.Builder()
.setAspectRatio(Rational(16, 9))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setActions(buildPipActions())
}
return builder.build()
}
private fun buildPipActions(): List<RemoteAction> {
val actions = mutableListOf<RemoteAction>()
// Mute / Unmute toggle
val isMuted = ActiveCallHolder.callController?.isAudioMuted?.value == true
val muteIntent =
PendingIntent.getBroadcast(
this,
PIP_MUTE_REQUEST_CODE,
Intent(ACTION_PIP_TOGGLE_MUTE).setPackage(packageName),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val muteAction =
RemoteAction(
Icon.createWithResource(
this,
if (isMuted) R.drawable.ic_mic_off else R.drawable.ic_mic_on,
),
getString(if (isMuted) R.string.call_unmute else R.string.call_mute),
getString(if (isMuted) R.string.call_unmute else R.string.call_mute),
muteIntent,
)
actions.add(muteAction)
// Hangup
val hangupIntent =
PendingIntent.getBroadcast(
this,
PIP_HANGUP_REQUEST_CODE,
Intent(ACTION_PIP_HANGUP).setPackage(packageName),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val hangupAction =
RemoteAction(
Icon.createWithResource(this, R.drawable.ic_call_end),
getString(R.string.call_hangup),
getString(R.string.call_hangup),
hangupIntent,
)
actions.add(hangupAction)
return actions
}
private fun registerPipReceiver() {
val filter =
IntentFilter().apply {
addAction(ACTION_PIP_HANGUP)
addAction(ACTION_PIP_TOGGLE_MUTE)
}
registerReceiver(pipActionReceiver, filter, RECEIVER_NOT_EXPORTED)
}
private fun unregisterPipReceiver() {
try {
unregisterReceiver(pipActionReceiver)
} catch (_: Exception) {
}
}
companion object {
private const val ACTION_PIP_HANGUP = "com.vitorpamplona.amethyst.PIP_HANGUP"
private const val ACTION_PIP_TOGGLE_MUTE = "com.vitorpamplona.amethyst.PIP_TOGGLE_MUTE"
private const val PIP_HANGUP_REQUEST_CODE = 0x60001
private const val PIP_MUTE_REQUEST_CODE = 0x60002
fun launch(context: Context) {
context.startActivity(
Intent(context, CallActivity::class.java).apply {
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.call
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class CallNotificationReceiver : BroadcastReceiver() {
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(
context: Context,
intent: Intent,
) {
when (intent.action) {
ACTION_ACCEPT_CALL -> {
val callController = ActiveCallHolder.callController
val callManager = ActiveCallHolder.callManager
val state = callManager?.state?.value
if (state is com.vitorpamplona.amethyst.commons.call.CallState.IncomingCall) {
callController?.acceptIncomingCall(state.sdpOffer)
}
NotificationUtils.cancelCallNotification(context)
CallActivity.launch(context)
}
ACTION_REJECT_CALL -> {
val callManager = ActiveCallHolder.callManager
GlobalScope.launch {
callManager?.rejectCall()
}
NotificationUtils.cancelCallNotification(context)
}
}
}
companion object {
const val ACTION_ACCEPT_CALL = "com.vitorpamplona.amethyst.ACCEPT_CALL"
const val ACTION_REJECT_CALL = "com.vitorpamplona.amethyst.REJECT_CALL"
}
}
@@ -94,6 +94,7 @@ fun CallScreen(
callController: CallController?,
accountViewModel: AccountViewModel,
onCallEnded: () -> Unit,
isInPipMode: Boolean = false,
) {
val callState by callManager.state.collectAsState()
val scope = rememberCoroutineScope()
@@ -121,73 +122,97 @@ fun CallScreen(
}
is CallState.Offering -> {
CallInProgressUI(
peerPubKey = state.peerPubKey,
statusText = stringRes(R.string.call_calling),
accountViewModel = accountViewModel,
onHangup = { scope.launch { callManager.hangup() } },
)
if (isInPipMode) {
PipCallUI(peerPubKey = state.peerPubKey, statusText = stringRes(R.string.call_calling), accountViewModel = accountViewModel)
} else {
CallInProgressUI(
peerPubKey = state.peerPubKey,
statusText = stringRes(R.string.call_calling),
accountViewModel = accountViewModel,
onHangup = { scope.launch { callManager.hangup() } },
)
}
}
is CallState.IncomingCall -> {
val isVideoCall = state.callType == com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType.VIDEO
val acceptWithPermission =
rememberCallWithPermission(context, isVideo = isVideoCall) {
callController?.acceptIncomingCall(state.sdpOffer)
}
IncomingCallUI(
callerPubKey = state.callerPubKey,
callType = state.callType,
accountViewModel = accountViewModel,
onAccept = acceptWithPermission,
onReject = { scope.launch { callManager.rejectCall() } },
)
if (isInPipMode) {
PipCallUI(callerPubKey = state.callerPubKey, statusText = stringRes(R.string.call_incoming), accountViewModel = accountViewModel)
} else {
val isVideoCall = state.callType == com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType.VIDEO
val acceptWithPermission =
rememberCallWithPermission(context, isVideo = isVideoCall) {
callController?.acceptIncomingCall(state.sdpOffer)
}
IncomingCallUI(
callerPubKey = state.callerPubKey,
callType = state.callType,
accountViewModel = accountViewModel,
onAccept = acceptWithPermission,
onReject = { scope.launch { callManager.rejectCall() } },
)
}
}
is CallState.Connecting -> {
CallInProgressUI(
peerPubKey = state.peerPubKey,
statusText = stringRes(R.string.call_connecting),
accountViewModel = accountViewModel,
onHangup = { scope.launch { callManager.hangup() } },
)
if (isInPipMode) {
PipCallUI(peerPubKey = state.peerPubKey, statusText = stringRes(R.string.call_connecting), accountViewModel = accountViewModel)
} else {
CallInProgressUI(
peerPubKey = state.peerPubKey,
statusText = stringRes(R.string.call_connecting),
accountViewModel = accountViewModel,
onHangup = { scope.launch { callManager.hangup() } },
)
}
}
is CallState.Connected -> {
ConnectedCallUI(
state = state,
callController = callController,
accountViewModel = accountViewModel,
onHangup = { scope.launch { callManager.hangup() } },
onToggleMute = { callController?.toggleAudioMute() },
onToggleVideo = { callController?.toggleVideo() },
onCycleAudioRoute = { callController?.cycleAudioRoute() },
)
if (isInPipMode) {
PipConnectedCallUI(state = state, callController = callController, accountViewModel = accountViewModel)
} else {
ConnectedCallUI(
state = state,
callController = callController,
accountViewModel = accountViewModel,
onHangup = { scope.launch { callManager.hangup() } },
onToggleMute = { callController?.toggleAudioMute() },
onToggleVideo = { callController?.toggleVideo() },
onCycleAudioRoute = { callController?.cycleAudioRoute() },
)
}
}
is CallState.Ended -> {
CallInProgressUI(
peerPubKey = state.peerPubKey,
statusText = stringRes(R.string.call_ended),
accountViewModel = accountViewModel,
onHangup = { onCallEnded() },
)
if (!isInPipMode) {
CallInProgressUI(
peerPubKey = state.peerPubKey,
statusText = stringRes(R.string.call_ended),
accountViewModel = accountViewModel,
onHangup = { onCallEnded() },
)
}
LaunchedEffect(Unit) {
delay(2000)
onCallEnded()
}
}
}
errorMessage?.let { error ->
Snackbar(
modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
action = {
Text(
stringRes(R.string.call_dismiss),
modifier =
Modifier.padding(8.dp),
color = MaterialTheme.colorScheme.inversePrimary,
)
},
) {
Text(error)
if (!isInPipMode) {
errorMessage?.let { error ->
Snackbar(
modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
action = {
Text(
stringRes(R.string.call_dismiss),
modifier =
Modifier.padding(8.dp),
color = MaterialTheme.colorScheme.inversePrimary,
)
},
) {
Text(error)
}
}
}
}
@@ -557,6 +582,120 @@ private fun formatDuration(seconds: Long): String {
return "%02d:%02d".format(mins, secs)
}
@Composable
private fun PipCallUI(
peerPubKey: String = "",
callerPubKey: String = "",
statusText: String,
accountViewModel: AccountViewModel,
) {
val pubKey = peerPubKey.ifEmpty { callerPubKey }
Box(
modifier =
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
LoadUser(baseUserHex = pubKey, accountViewModel = accountViewModel) { user ->
if (user != null) {
ClickableUserPicture(
baseUser = user,
size = 48.dp,
accountViewModel = accountViewModel,
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = statusText,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 10.sp,
)
}
}
}
@Composable
private fun PipConnectedCallUI(
state: CallState.Connected,
callController: CallController?,
accountViewModel: AccountViewModel,
) {
var elapsed by remember { mutableLongStateOf(0L) }
LaunchedEffect(state.startedAtEpoch) {
while (true) {
elapsed = TimeUtils.now() - state.startedAtEpoch
delay(1000)
}
}
val emptyVideoFlow = remember { kotlinx.coroutines.flow.MutableStateFlow<VideoTrack?>(null) }
val remoteVideoTrack by (callController?.remoteVideoTrack ?: emptyVideoFlow).collectAsState()
val defaultFalse = remember { kotlinx.coroutines.flow.MutableStateFlow(false) }
val isRemoteVideoActive by (callController?.isRemoteVideoActive ?: defaultFalse).collectAsState()
Box(
modifier =
Modifier
.fillMaxSize()
.background(Color.Black),
) {
// Remote video full screen in PiP
if (isRemoteVideoActive) {
remoteVideoTrack?.let { track ->
VideoRenderer(
videoTrack = track,
eglBase = callController?.getEglBase(),
modifier = Modifier.fillMaxSize(),
mirror = false,
)
}
}
if (!isRemoteVideoActive) {
// Show small avatar + timer in PiP
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
LoadUser(baseUserHex = state.peerPubKey, accountViewModel = accountViewModel) { user ->
if (user != null) {
ClickableUserPicture(
baseUser = user,
size = 48.dp,
accountViewModel = accountViewModel,
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = formatDuration(elapsed),
color = Color.White.copy(alpha = 0.7f),
fontSize = 10.sp,
)
}
} else {
// Timer overlay
Text(
text = formatDuration(elapsed),
color = Color.White.copy(alpha = 0.7f),
fontSize = 10.sp,
modifier =
Modifier
.align(Alignment.TopCenter)
.padding(top = 4.dp),
)
}
}
}
@Composable
private fun KeepScreenOn() {
val context = LocalContext.current
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="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"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="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.17V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01V11c0,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.1H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73 4.27,3z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="@android:color/white"
android:pathData="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,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>