ganfra
3 months ago
620 changed files with 6818 additions and 1241 deletions
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Render selected/deselected room list filters on top |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
BugReporting | Add public device keys to rageshakes |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Ringing call notifications and full screen ringing screen for DMs when the device is locked. |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Set auto captilization, multiline and autocompletion flags for the markdown EditText. |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Restoree Markdown text input contents when returning to the room screen. |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Move push provider setting to the "Notifications" screen and display it only when several push provider are available. |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Fixed sending rich content from android keyboards on the markdown text input |
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
Simplify notifications by removing the custom persistence layer. |
||||
|
||||
Bump minSdk to 24 (Android 7). |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Add a feature flag ShowBlockedUsersDetails, disabled by default to render display name and avatar of blocked users in the blocked users list. |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Be more specific with the widget permissions |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Analytics | Add support for SuperProperties |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Fix crash when restoring the selection values in the plain text editor. |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Track when the user starts a room call and when they enable formatting options on the message composer |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: Add support for incoming share (text or files) from other apps. Bug fixes. |
||||
Full changelog: https://github.com/element-hq/element-x-android/releases |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_analytics_settings_help_us_improve">"Võimalike rakenduse vigade leidmiseks palun jaga anonüümset kasutusteavet."</string> |
||||
<string name="screen_analytics_settings_read_terms">"Sa võid lugeda meie kasutustingimusi %1$s"</string> |
||||
<string name="screen_analytics_settings_read_terms_content_link">"siin"</string> |
||||
<string name="screen_analytics_settings_share_data">"Jaga andmeid rakenduse kasutuse kohta"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_analytics_prompt_data_usage">"Me ei salvesta ega profileeri sinu isiklikke andmeid"</string> |
||||
<string name="screen_analytics_prompt_help_us_improve">"Võimalike rakenduse vigade leidmiseks palun jaga anonüümset kasutusteavet."</string> |
||||
<string name="screen_analytics_prompt_read_terms">"Sa võid lugeda meie kasutustingimusi %1$s"</string> |
||||
<string name="screen_analytics_prompt_read_terms_content_link">"siin"</string> |
||||
<string name="screen_analytics_prompt_settings">"Selle valiku saad igal ajal välja lülitada"</string> |
||||
<string name="screen_analytics_prompt_third_party_sharing">"Me ei jaga andmeid kolmandate osapooltega"</string> |
||||
<string name="screen_analytics_prompt_title">"Aita parandada %1$s rakendust"</string> |
||||
</resources> |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-library") |
||||
id("kotlin-parcelize") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.features.call.api" |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(projects.anvilannotations) |
||||
implementation(projects.libraries.architecture) |
||||
implementation(projects.libraries.core) |
||||
implementation(projects.libraries.matrix.api) |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.api |
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
|
||||
/** |
||||
* Entry point for the call feature. |
||||
*/ |
||||
interface ElementCallEntryPoint { |
||||
/** |
||||
* Start a call of the given type. |
||||
* @param callType The type of call to start. |
||||
*/ |
||||
fun startCall(callType: CallType) |
||||
|
||||
/** |
||||
* Handle an incoming call. |
||||
* @param callType The type of call. |
||||
* @param eventId The event id of the event that started the call. |
||||
* @param senderId The user id of the sender of the event that started the call. |
||||
* @param roomName The name of the room the call is in. |
||||
* @param senderName The name of the sender of the event that started the call. |
||||
* @param avatarUrl The avatar url of the room or DM. |
||||
* @param timestamp The timestamp of the event that started the call. |
||||
* @param notificationChannelId The id of the notification channel to use for the call notification. |
||||
*/ |
||||
fun handleIncomingCall( |
||||
callType: CallType.RoomCall, |
||||
eventId: EventId, |
||||
senderId: UserId, |
||||
roomName: String?, |
||||
senderName: String?, |
||||
avatarUrl: String?, |
||||
timestamp: Long, |
||||
notificationChannelId: String, |
||||
) |
||||
} |
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.impl |
||||
|
||||
import android.content.Context |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.features.call.api.CallType |
||||
import io.element.android.features.call.api.ElementCallEntryPoint |
||||
import io.element.android.features.call.impl.notifications.CallNotificationData |
||||
import io.element.android.features.call.impl.utils.ActiveCallManager |
||||
import io.element.android.features.call.impl.utils.IntentProvider |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import javax.inject.Inject |
||||
|
||||
@ContributesBinding(AppScope::class) |
||||
class DefaultElementCallEntryPoint @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val activeCallManager: ActiveCallManager, |
||||
) : ElementCallEntryPoint { |
||||
companion object { |
||||
const val EXTRA_CALL_TYPE = "EXTRA_CALL_TYPE" |
||||
const val REQUEST_CODE = 2255 |
||||
} |
||||
|
||||
override fun startCall(callType: CallType) { |
||||
context.startActivity(IntentProvider.createIntent(context, callType)) |
||||
} |
||||
|
||||
override fun handleIncomingCall( |
||||
callType: CallType.RoomCall, |
||||
eventId: EventId, |
||||
senderId: UserId, |
||||
roomName: String?, |
||||
senderName: String?, |
||||
avatarUrl: String?, |
||||
timestamp: Long, |
||||
notificationChannelId: String, |
||||
) { |
||||
val incomingCallNotificationData = CallNotificationData( |
||||
sessionId = callType.sessionId, |
||||
roomId = callType.roomId, |
||||
eventId = eventId, |
||||
senderId = senderId, |
||||
roomName = roomName, |
||||
senderName = senderName, |
||||
avatarUrl = avatarUrl, |
||||
timestamp = timestamp, |
||||
notificationChannelId = notificationChannelId, |
||||
) |
||||
activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData) |
||||
} |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.impl.notifications |
||||
|
||||
import android.os.Parcelable |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import kotlinx.parcelize.Parcelize |
||||
|
||||
@Parcelize |
||||
data class CallNotificationData( |
||||
val sessionId: SessionId, |
||||
val roomId: RoomId, |
||||
val eventId: EventId, |
||||
val senderId: UserId, |
||||
val roomName: String?, |
||||
val senderName: String?, |
||||
val avatarUrl: String?, |
||||
val notificationChannelId: String, |
||||
val timestamp: Long, |
||||
) : Parcelable |
@ -0,0 +1,130 @@
@@ -0,0 +1,130 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package io.element.android.features.call.impl.notifications |
||||
|
||||
import android.app.Notification |
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.media.AudioManager |
||||
import android.media.RingtoneManager |
||||
import androidx.core.app.NotificationCompat |
||||
import androidx.core.app.PendingIntentCompat |
||||
import androidx.core.app.Person |
||||
import io.element.android.appconfig.ElementCallConfig |
||||
import io.element.android.features.call.api.CallType |
||||
import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver |
||||
import io.element.android.features.call.impl.ui.IncomingCallActivity |
||||
import io.element.android.features.call.impl.utils.IntentProvider |
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder |
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader |
||||
import javax.inject.Inject |
||||
import kotlin.time.Duration.Companion.seconds |
||||
|
||||
/** |
||||
* Creates a notification for a ringing call. |
||||
*/ |
||||
class RingingCallNotificationCreator @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val matrixClientProvider: MatrixClientProvider, |
||||
private val imageLoaderHolder: ImageLoaderHolder, |
||||
private val notificationBitmapLoader: NotificationBitmapLoader, |
||||
) { |
||||
companion object { |
||||
/** |
||||
* Request code for the decline action. |
||||
*/ |
||||
const val DECLINE_REQUEST_CODE = 1 |
||||
|
||||
/** |
||||
* Request code for the full screen intent. |
||||
*/ |
||||
const val FULL_SCREEN_INTENT_REQUEST_CODE = 2 |
||||
} |
||||
|
||||
suspend fun createNotification( |
||||
sessionId: SessionId, |
||||
roomId: RoomId, |
||||
eventId: EventId, |
||||
senderId: UserId, |
||||
roomName: String?, |
||||
senderDisplayName: String, |
||||
roomAvatarUrl: String?, |
||||
notificationChannelId: String, |
||||
timestamp: Long, |
||||
): Notification? { |
||||
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null |
||||
val imageLoader = imageLoaderHolder.get(matrixClient) |
||||
val largeIcon = notificationBitmapLoader.getUserIcon(roomAvatarUrl, imageLoader) |
||||
|
||||
val caller = Person.Builder() |
||||
.setName(senderDisplayName) |
||||
.setIcon(largeIcon) |
||||
.setImportant(true) |
||||
.build() |
||||
|
||||
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId)) |
||||
|
||||
val declineIntent = PendingIntentCompat.getBroadcast( |
||||
context, |
||||
DECLINE_REQUEST_CODE, |
||||
Intent(context, DeclineCallBroadcastReceiver::class.java), |
||||
PendingIntent.FLAG_CANCEL_CURRENT, |
||||
false, |
||||
)!! |
||||
|
||||
val fullScreenIntent = PendingIntentCompat.getActivity( |
||||
context, |
||||
FULL_SCREEN_INTENT_REQUEST_CODE, |
||||
Intent(context, IncomingCallActivity::class.java).apply { |
||||
putExtra( |
||||
IncomingCallActivity.EXTRA_NOTIFICATION_DATA, |
||||
CallNotificationData(sessionId, roomId, eventId, senderId, roomName, senderDisplayName, roomAvatarUrl, notificationChannelId, timestamp) |
||||
) |
||||
}, |
||||
PendingIntent.FLAG_CANCEL_CURRENT, |
||||
false |
||||
) |
||||
|
||||
val ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) |
||||
return NotificationCompat.Builder(context, notificationChannelId) |
||||
.setSmallIcon(CommonDrawables.ic_notification_small) |
||||
.setPriority(NotificationCompat.PRIORITY_MAX) |
||||
.setCategory(NotificationCompat.CATEGORY_CALL) |
||||
.setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true)) |
||||
.addPerson(caller) |
||||
.setAutoCancel(true) |
||||
.setWhen(timestamp) |
||||
.setOngoing(true) |
||||
.setShowWhen(false) |
||||
.setSound(ringtoneUri, AudioManager.STREAM_RING) |
||||
.setTimeoutAfter(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds.inWholeMilliseconds) |
||||
.setContentIntent(answerIntent) |
||||
.setDeleteIntent(declineIntent) |
||||
.setFullScreenIntent(fullScreenIntent, true) |
||||
.build() |
||||
.apply { |
||||
flags = flags.or(Notification.FLAG_INSISTENT) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.impl.receivers |
||||
|
||||
import android.content.BroadcastReceiver |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import io.element.android.features.call.impl.di.CallBindings |
||||
import io.element.android.features.call.impl.utils.ActiveCallManager |
||||
import io.element.android.libraries.architecture.bindings |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* Broadcast receiver to decline the incoming call. |
||||
*/ |
||||
class DeclineCallBroadcastReceiver : BroadcastReceiver() { |
||||
@Inject |
||||
lateinit var activeCallManager: ActiveCallManager |
||||
override fun onReceive(context: Context, intent: Intent?) { |
||||
context.bindings<CallBindings>().inject(this) |
||||
activeCallManager.hungUpCall() |
||||
} |
||||
} |
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.impl.ui |
||||
|
||||
import android.os.Bundle |
||||
import android.view.WindowManager |
||||
import androidx.activity.compose.setContent |
||||
import androidx.appcompat.app.AppCompatActivity |
||||
import androidx.core.content.IntentCompat |
||||
import androidx.lifecycle.lifecycleScope |
||||
import io.element.android.features.call.api.CallType |
||||
import io.element.android.features.call.api.ElementCallEntryPoint |
||||
import io.element.android.features.call.impl.di.CallBindings |
||||
import io.element.android.features.call.impl.notifications.CallNotificationData |
||||
import io.element.android.features.call.impl.utils.ActiveCallManager |
||||
import io.element.android.features.call.impl.utils.CallState |
||||
import io.element.android.libraries.architecture.bindings |
||||
import kotlinx.coroutines.flow.filter |
||||
import kotlinx.coroutines.flow.launchIn |
||||
import kotlinx.coroutines.flow.onEach |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* Activity that's displayed as a full screen intent when an incoming call is received. |
||||
*/ |
||||
class IncomingCallActivity : AppCompatActivity() { |
||||
companion object { |
||||
/** |
||||
* Extra key for the notification data. |
||||
*/ |
||||
const val EXTRA_NOTIFICATION_DATA = "EXTRA_NOTIFICATION_DATA" |
||||
} |
||||
|
||||
@Inject |
||||
lateinit var elementCallEntryPoint: ElementCallEntryPoint |
||||
|
||||
@Inject |
||||
lateinit var activeCallManager: ActiveCallManager |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
|
||||
applicationContext.bindings<CallBindings>().inject(this) |
||||
|
||||
// Set flags so it can be displayed in the lock screen |
||||
@Suppress("DEPRECATION") |
||||
window.addFlags( |
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or |
||||
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or |
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or |
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
||||
) |
||||
|
||||
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) } |
||||
if (notificationData != null) { |
||||
setContent { |
||||
IncomingCallScreen( |
||||
notificationData = notificationData, |
||||
onAnswer = ::onAnswer, |
||||
onCancel = ::onCancel, |
||||
) |
||||
} |
||||
} else { |
||||
// No data, finish the activity |
||||
finish() |
||||
return |
||||
} |
||||
|
||||
activeCallManager.activeCall |
||||
.filter { it?.callState !is CallState.Ringing } |
||||
.onEach { finish() } |
||||
.launchIn(lifecycleScope) |
||||
} |
||||
|
||||
private fun onAnswer(notificationData: CallNotificationData) { |
||||
elementCallEntryPoint.startCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId)) |
||||
} |
||||
|
||||
private fun onCancel() { |
||||
activeCallManager.hungUpCall() |
||||
} |
||||
} |
@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.impl.ui |
||||
|
||||
import androidx.compose.foundation.border |
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.material3.FilledIconButton |
||||
import androidx.compose.material3.IconButtonDefaults |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.vector.ImageVector |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.compound.theme.ElementTheme |
||||
import io.element.android.compound.tokens.generated.CompoundIcons |
||||
import io.element.android.features.call.impl.R |
||||
import io.element.android.features.call.impl.notifications.CallNotificationData |
||||
import io.element.android.libraries.designsystem.background.OnboardingBackground |
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||
import io.element.android.libraries.designsystem.theme.components.Icon |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
|
||||
@Composable |
||||
internal fun IncomingCallScreen( |
||||
notificationData: CallNotificationData, |
||||
onAnswer: (CallNotificationData) -> Unit, |
||||
onCancel: () -> Unit, |
||||
) { |
||||
ElementTheme { |
||||
OnboardingBackground() |
||||
Column( |
||||
modifier = Modifier.fillMaxSize(), |
||||
horizontalAlignment = Alignment.CenterHorizontally, |
||||
verticalArrangement = Arrangement.Bottom |
||||
) { |
||||
Column( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.padding(start = 20.dp, end = 20.dp, top = 124.dp) |
||||
.weight(1f), |
||||
horizontalAlignment = Alignment.CenterHorizontally |
||||
) { |
||||
Avatar( |
||||
avatarData = AvatarData( |
||||
id = notificationData.senderId.value, |
||||
name = notificationData.senderName, |
||||
url = notificationData.avatarUrl, |
||||
size = AvatarSize.IncomingCall, |
||||
) |
||||
) |
||||
Spacer(modifier = Modifier.height(24.dp)) |
||||
Text( |
||||
text = notificationData.senderName ?: notificationData.senderId.value, |
||||
style = ElementTheme.typography.fontHeadingMdBold, |
||||
textAlign = TextAlign.Center, |
||||
) |
||||
Spacer(modifier = Modifier.height(8.dp)) |
||||
Text( |
||||
text = stringResource(R.string.screen_incoming_call_subtitle_android), |
||||
style = ElementTheme.typography.fontBodyLgRegular, |
||||
color = ElementTheme.colors.textSecondary, |
||||
textAlign = TextAlign.Center, |
||||
) |
||||
} |
||||
Row( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp), |
||||
horizontalArrangement = Arrangement.SpaceBetween, |
||||
verticalAlignment = Alignment.CenterVertically |
||||
) { |
||||
ActionButton( |
||||
size = 64.dp, |
||||
onClick = { onAnswer(notificationData) }, |
||||
icon = CompoundIcons.VoiceCall(), |
||||
title = stringResource(CommonStrings.action_accept), |
||||
backgroundColor = ElementTheme.colors.iconSuccessPrimary, |
||||
borderColor = ElementTheme.colors.borderSuccessSubtle |
||||
) |
||||
|
||||
ActionButton( |
||||
size = 64.dp, |
||||
onClick = onCancel, |
||||
icon = CompoundIcons.EndCall(), |
||||
title = stringResource(CommonStrings.action_reject), |
||||
backgroundColor = ElementTheme.colors.iconCriticalPrimary, |
||||
borderColor = ElementTheme.colors.borderCriticalSubtle |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun ActionButton( |
||||
size: Dp, |
||||
onClick: () -> Unit, |
||||
icon: ImageVector, |
||||
title: String, |
||||
backgroundColor: Color, |
||||
borderColor: Color, |
||||
contentDescription: String? = title, |
||||
borderSize: Dp = 1.33.dp, |
||||
) { |
||||
Column( |
||||
modifier = Modifier.width(120.dp), |
||||
horizontalAlignment = Alignment.CenterHorizontally |
||||
) { |
||||
FilledIconButton( |
||||
modifier = Modifier.size(size + borderSize) |
||||
.border(borderSize, borderColor, CircleShape), |
||||
onClick = onClick, |
||||
colors = IconButtonDefaults.filledIconButtonColors( |
||||
containerColor = backgroundColor, |
||||
contentColor = Color.White, |
||||
) |
||||
) { |
||||
Icon( |
||||
modifier = Modifier.size(32.dp), |
||||
imageVector = icon, |
||||
contentDescription = contentDescription |
||||
) |
||||
} |
||||
Spacer(modifier = Modifier.height(16.dp)) |
||||
Text( |
||||
text = title, |
||||
style = ElementTheme.typography.fontBodyLgMedium, |
||||
color = ElementTheme.colors.textPrimary, |
||||
overflow = TextOverflow.Ellipsis, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@PreviewsDayNight |
||||
@Composable |
||||
internal fun IncomingCallScreenPreview() { |
||||
ElementPreview { |
||||
IncomingCallScreen( |
||||
notificationData = CallNotificationData( |
||||
sessionId = SessionId("@alice:matrix.org"), |
||||
roomId = RoomId("!1234:matrix.org"), |
||||
eventId = EventId("\$asdadadsad:matrix.org"), |
||||
senderId = UserId("@bob:matrix.org"), |
||||
roomName = "A room", |
||||
senderName = "Bob", |
||||
avatarUrl = null, |
||||
notificationChannelId = "incoming_call", |
||||
timestamp = 0L, |
||||
), |
||||
onAnswer = {}, |
||||
onCancel = {}, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,206 @@
@@ -0,0 +1,206 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.impl.utils |
||||
|
||||
import android.annotation.SuppressLint |
||||
import androidx.core.app.NotificationManagerCompat |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.appconfig.ElementCallConfig |
||||
import io.element.android.features.call.impl.notifications.CallNotificationData |
||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.di.SingleIn |
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType |
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider |
||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.Job |
||||
import kotlinx.coroutines.delay |
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
import kotlinx.coroutines.flow.StateFlow |
||||
import kotlinx.coroutines.launch |
||||
import timber.log.Timber |
||||
import javax.inject.Inject |
||||
import kotlin.time.Duration.Companion.seconds |
||||
|
||||
/** |
||||
* Manages the active call state. |
||||
*/ |
||||
interface ActiveCallManager { |
||||
/** |
||||
* The active call state flow, which will be updated when the active call changes. |
||||
*/ |
||||
val activeCall: StateFlow<ActiveCall?> |
||||
|
||||
/** |
||||
* Registers an incoming call if there isn't an existing active call and posts a [CallState.Ringing] notification. |
||||
* @param notificationData The data for the incoming call notification. |
||||
*/ |
||||
fun registerIncomingCall(notificationData: CallNotificationData) |
||||
|
||||
/** |
||||
* Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification. |
||||
*/ |
||||
fun incomingCallTimedOut() |
||||
|
||||
/** |
||||
* Hangs up the active call and removes any associated UI. |
||||
*/ |
||||
fun hungUpCall() |
||||
|
||||
/** |
||||
* Called when the user joins a call. It will remove any existing UI and set the call state as [CallState.InCall]. |
||||
* |
||||
* @param sessionId The session ID of the user joining the call. |
||||
* @param roomId The room ID of the call. |
||||
*/ |
||||
fun joinedCall(sessionId: SessionId, roomId: RoomId) |
||||
} |
||||
|
||||
@SingleIn(AppScope::class) |
||||
@ContributesBinding(AppScope::class) |
||||
class DefaultActiveCallManager @Inject constructor( |
||||
private val coroutineScope: CoroutineScope, |
||||
private val matrixClientProvider: MatrixClientProvider, |
||||
private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler, |
||||
private val ringingCallNotificationCreator: RingingCallNotificationCreator, |
||||
private val notificationManagerCompat: NotificationManagerCompat, |
||||
) : ActiveCallManager { |
||||
private var timedOutCallJob: Job? = null |
||||
|
||||
override val activeCall = MutableStateFlow<ActiveCall?>(null) |
||||
|
||||
override fun registerIncomingCall(notificationData: CallNotificationData) { |
||||
if (activeCall.value != null) { |
||||
displayMissedCallNotification(notificationData) |
||||
Timber.w("Already have an active call, ignoring incoming call: $notificationData") |
||||
return |
||||
} |
||||
activeCall.value = ActiveCall( |
||||
sessionId = notificationData.sessionId, |
||||
roomId = notificationData.roomId, |
||||
callState = CallState.Ringing(notificationData), |
||||
) |
||||
|
||||
timedOutCallJob = coroutineScope.launch { |
||||
showIncomingCallNotification(notificationData) |
||||
|
||||
// Wait for the call to end |
||||
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds) |
||||
incomingCallTimedOut() |
||||
} |
||||
} |
||||
|
||||
override fun incomingCallTimedOut() { |
||||
val previousActiveCall = activeCall.value ?: return |
||||
val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return |
||||
activeCall.value = null |
||||
|
||||
cancelIncomingCallNotification() |
||||
|
||||
displayMissedCallNotification(notificationData) |
||||
} |
||||
|
||||
override fun hungUpCall() { |
||||
cancelIncomingCallNotification() |
||||
timedOutCallJob?.cancel() |
||||
activeCall.value = null |
||||
} |
||||
|
||||
override fun joinedCall(sessionId: SessionId, roomId: RoomId) { |
||||
cancelIncomingCallNotification() |
||||
timedOutCallJob?.cancel() |
||||
|
||||
activeCall.value = ActiveCall( |
||||
sessionId = sessionId, |
||||
roomId = roomId, |
||||
callState = CallState.InCall, |
||||
) |
||||
// Send call notification to the room |
||||
coroutineScope.launch { |
||||
matrixClientProvider.getOrRestore(sessionId) |
||||
.getOrNull() |
||||
?.getRoom(roomId) |
||||
?.sendCallNotificationIfNeeded() |
||||
} |
||||
} |
||||
|
||||
@SuppressLint("MissingPermission") |
||||
private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) { |
||||
val notification = ringingCallNotificationCreator.createNotification( |
||||
sessionId = notificationData.sessionId, |
||||
roomId = notificationData.roomId, |
||||
eventId = notificationData.eventId, |
||||
senderId = notificationData.senderId, |
||||
roomName = notificationData.roomName, |
||||
senderDisplayName = notificationData.senderName ?: notificationData.senderId.value, |
||||
roomAvatarUrl = notificationData.avatarUrl, |
||||
notificationChannelId = notificationData.notificationChannelId, |
||||
timestamp = notificationData.timestamp |
||||
) ?: return |
||||
runCatching { |
||||
notificationManagerCompat.notify( |
||||
NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL), |
||||
notification, |
||||
) |
||||
}.onFailure { |
||||
Timber.e(it, "Failed to publish notification for incoming call") |
||||
} |
||||
} |
||||
|
||||
private fun cancelIncomingCallNotification() { |
||||
notificationManagerCompat.cancel(NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL)) |
||||
} |
||||
|
||||
private fun displayMissedCallNotification(notificationData: CallNotificationData) { |
||||
coroutineScope.launch { |
||||
onMissedCallNotificationHandler.addMissedCallNotification( |
||||
sessionId = notificationData.sessionId, |
||||
roomId = notificationData.roomId, |
||||
eventId = notificationData.eventId, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Represents an active call. |
||||
*/ |
||||
data class ActiveCall( |
||||
val sessionId: SessionId, |
||||
val roomId: RoomId, |
||||
val callState: CallState, |
||||
) |
||||
|
||||
/** |
||||
* Represents the state of an active call. |
||||
*/ |
||||
sealed interface CallState { |
||||
/** |
||||
* The call is in a ringing state. |
||||
* @param notificationData The data for the incoming call notification. |
||||
*/ |
||||
data class Ringing(val notificationData: CallNotificationData) : CallState |
||||
|
||||
/** |
||||
* The call is in an in-call state. |
||||
*/ |
||||
data object InCall : CallState |
||||
} |
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.impl.utils |
||||
|
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import androidx.core.app.PendingIntentCompat |
||||
import io.element.android.features.call.api.CallType |
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint |
||||
import io.element.android.features.call.impl.ui.ElementCallActivity |
||||
|
||||
internal object IntentProvider { |
||||
fun createIntent(context: Context, callType: CallType): Intent = Intent(context, ElementCallActivity::class.java).apply { |
||||
putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callType) |
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION) |
||||
} |
||||
|
||||
fun getPendingIntent(context: Context, callType: CallType): PendingIntent { |
||||
return PendingIntentCompat.getActivity( |
||||
context, |
||||
DefaultElementCallEntryPoint.REQUEST_CODE, |
||||
createIntent(context, callType), |
||||
0, |
||||
false |
||||
)!! |
||||
} |
||||
} |
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call |
||||
|
||||
import android.content.Intent |
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.call.api.CallType |
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint |
||||
import io.element.android.features.call.impl.notifications.CallNotificationData |
||||
import io.element.android.features.call.impl.ui.ElementCallActivity |
||||
import io.element.android.features.call.utils.FakeActiveCallManager |
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID |
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID |
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2 |
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
import org.robolectric.RobolectricTestRunner |
||||
import org.robolectric.RuntimeEnvironment |
||||
import org.robolectric.Shadows.shadowOf |
||||
|
||||
@RunWith(RobolectricTestRunner::class) |
||||
class DefaultElementCallEntryPointTest { |
||||
@Test |
||||
fun `startCall - starts ElementCallActivity setup with the needed extras`() { |
||||
val entryPoint = createEntryPoint() |
||||
entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID)) |
||||
|
||||
val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java) |
||||
val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity |
||||
assertThat(intent.component).isEqualTo(expectedIntent.component) |
||||
assertThat(intent.extras?.containsKey("EXTRA_CALL_TYPE")).isTrue() |
||||
} |
||||
|
||||
@Test |
||||
fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() { |
||||
val registerIncomingCallLambda = lambdaRecorder<CallNotificationData, Unit> {} |
||||
val activeCallManager = FakeActiveCallManager(registerIncomingCallResult = registerIncomingCallLambda) |
||||
val entryPoint = createEntryPoint(activeCallManager = activeCallManager) |
||||
|
||||
entryPoint.handleIncomingCall( |
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), |
||||
eventId = AN_EVENT_ID, |
||||
senderId = A_USER_ID_2, |
||||
roomName = "roomName", |
||||
senderName = "senderName", |
||||
avatarUrl = "avatarUrl", |
||||
timestamp = 0, |
||||
notificationChannelId = "notificationChannelId", |
||||
) |
||||
|
||||
registerIncomingCallLambda.assertions().isCalledOnce() |
||||
} |
||||
|
||||
private fun createEntryPoint( |
||||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(), |
||||
) = DefaultElementCallEntryPoint( |
||||
context = InstrumentationRegistry.getInstrumentation().targetContext, |
||||
activeCallManager = activeCallManager, |
||||
) |
||||
} |
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.notifications |
||||
|
||||
import androidx.core.graphics.drawable.IconCompat |
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import coil.ImageLoader |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator |
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID |
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID |
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2 |
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient |
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider |
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder |
||||
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader |
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
import org.robolectric.RobolectricTestRunner |
||||
|
||||
@RunWith(RobolectricTestRunner::class) |
||||
class RingingCallNotificationCreatorTest { |
||||
@Test |
||||
fun `createNotification - with no associated MatrixClient does nothing`() = runTest { |
||||
val notificationCreator = createRingingCallNotificationCreator( |
||||
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("No client found")) }) |
||||
) |
||||
|
||||
val result = notificationCreator.createTestNotification() |
||||
|
||||
assertThat(result).isNull() |
||||
} |
||||
|
||||
@Test |
||||
fun `createNotification - creates a valid notification`() = runTest { |
||||
val notificationCreator = createRingingCallNotificationCreator( |
||||
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }) |
||||
) |
||||
|
||||
val result = notificationCreator.createTestNotification() |
||||
|
||||
assertThat(result).isNotNull() |
||||
} |
||||
|
||||
@Test |
||||
fun `createNotification - tries to load the avatar URL`() = runTest { |
||||
val getUserIconLambda = lambdaRecorder<String?, ImageLoader, IconCompat?> { _, _ -> null } |
||||
val notificationCreator = createRingingCallNotificationCreator( |
||||
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }), |
||||
notificationBitmapLoader = FakeNotificationBitmapLoader(getUserIconResult = getUserIconLambda) |
||||
) |
||||
|
||||
notificationCreator.createTestNotification() |
||||
|
||||
getUserIconLambda.assertions().isCalledOnce() |
||||
} |
||||
|
||||
private suspend fun RingingCallNotificationCreator.createTestNotification() = createNotification( |
||||
sessionId = A_SESSION_ID, |
||||
roomId = A_ROOM_ID, |
||||
eventId = AN_EVENT_ID, |
||||
senderId = A_USER_ID_2, |
||||
roomName = "Room", |
||||
senderDisplayName = "Johnnie Murphy", |
||||
roomAvatarUrl = "https://example.com/avatar.jpg", |
||||
notificationChannelId = "channelId", |
||||
timestamp = 0L, |
||||
) |
||||
|
||||
private fun createRingingCallNotificationCreator( |
||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), |
||||
imageLoaderHolder: FakeImageLoaderHolder = FakeImageLoaderHolder(), |
||||
notificationBitmapLoader: FakeNotificationBitmapLoader = FakeNotificationBitmapLoader(), |
||||
) = RingingCallNotificationCreator( |
||||
context = InstrumentationRegistry.getInstrumentation().targetContext, |
||||
matrixClientProvider = matrixClientProvider, |
||||
imageLoaderHolder = imageLoaderHolder, |
||||
notificationBitmapLoader = notificationBitmapLoader, |
||||
) |
||||
} |
@ -0,0 +1,203 @@
@@ -0,0 +1,203 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.utils |
||||
|
||||
import androidx.core.app.NotificationManagerCompat |
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator |
||||
import io.element.android.features.call.impl.utils.ActiveCall |
||||
import io.element.android.features.call.impl.utils.CallState |
||||
import io.element.android.features.call.impl.utils.DefaultActiveCallManager |
||||
import io.element.android.features.call.test.aCallNotificationData |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID |
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 |
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID |
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient |
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider |
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom |
||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType |
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider |
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder |
||||
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler |
||||
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader |
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder |
||||
import io.element.android.tests.testutils.lambda.value |
||||
import io.mockk.mockk |
||||
import io.mockk.verify |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.test.TestScope |
||||
import kotlinx.coroutines.test.advanceTimeBy |
||||
import kotlinx.coroutines.test.runCurrent |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
import org.robolectric.RobolectricTestRunner |
||||
|
||||
@RunWith(RobolectricTestRunner::class) |
||||
class DefaultActiveCallManagerTest { |
||||
private val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL) |
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) |
||||
@Test |
||||
fun `registerIncomingCall - sets the incoming call as active`() = runTest { |
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true) |
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) |
||||
|
||||
assertThat(manager.activeCall.value).isNull() |
||||
|
||||
val callNotificationData = aCallNotificationData() |
||||
manager.registerIncomingCall(callNotificationData) |
||||
|
||||
assertThat(manager.activeCall.value).isEqualTo( |
||||
ActiveCall( |
||||
sessionId = callNotificationData.sessionId, |
||||
roomId = callNotificationData.roomId, |
||||
callState = CallState.Ringing(callNotificationData) |
||||
) |
||||
) |
||||
|
||||
runCurrent() |
||||
|
||||
verify { notificationManagerCompat.notify(notificationId, any()) } |
||||
} |
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) |
||||
@Test |
||||
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest { |
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> } |
||||
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda) |
||||
val manager = createActiveCallManager( |
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler, |
||||
) |
||||
|
||||
// Register existing call |
||||
val callNotificationData = aCallNotificationData() |
||||
manager.registerIncomingCall(callNotificationData) |
||||
val activeCall = manager.activeCall.value |
||||
|
||||
// Now add a new call |
||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) |
||||
|
||||
assertThat(manager.activeCall.value).isEqualTo(activeCall) |
||||
assertThat(manager.activeCall.value?.roomId).isNotEqualTo(A_ROOM_ID_2) |
||||
|
||||
advanceTimeBy(1) |
||||
|
||||
addMissedCallNotificationLambda.assertions() |
||||
.isCalledOnce() |
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID)) |
||||
} |
||||
|
||||
@Test |
||||
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest { |
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> } |
||||
val manager = createActiveCallManager( |
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda) |
||||
) |
||||
|
||||
manager.incomingCallTimedOut() |
||||
|
||||
addMissedCallNotificationLambda.assertions().isNeverCalled() |
||||
} |
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) |
||||
@Test |
||||
fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest { |
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true) |
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> } |
||||
val manager = createActiveCallManager( |
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda), |
||||
notificationManagerCompat = notificationManagerCompat, |
||||
) |
||||
|
||||
manager.registerIncomingCall(aCallNotificationData()) |
||||
assertThat(manager.activeCall.value).isNotNull() |
||||
|
||||
manager.incomingCallTimedOut() |
||||
advanceTimeBy(1) |
||||
|
||||
assertThat(manager.activeCall.value).isNull() |
||||
addMissedCallNotificationLambda.assertions().isCalledOnce() |
||||
verify { notificationManagerCompat.cancel(notificationId) } |
||||
} |
||||
|
||||
@Test |
||||
fun `hungUpCall - removes existing call`() = runTest { |
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true) |
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) |
||||
|
||||
manager.registerIncomingCall(aCallNotificationData()) |
||||
assertThat(manager.activeCall.value).isNotNull() |
||||
|
||||
manager.hungUpCall() |
||||
assertThat(manager.activeCall.value).isNull() |
||||
|
||||
verify { notificationManagerCompat.cancel(notificationId) } |
||||
} |
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) |
||||
@Test |
||||
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest { |
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true) |
||||
val sendCallNotifyLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) } |
||||
val room = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotifyLambda) |
||||
val client = FakeMatrixClient().apply { |
||||
givenGetRoomResult(A_ROOM_ID, room) |
||||
} |
||||
val manager = createActiveCallManager( |
||||
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }), |
||||
notificationManagerCompat = notificationManagerCompat, |
||||
) |
||||
assertThat(manager.activeCall.value).isNull() |
||||
|
||||
manager.joinedCall(A_SESSION_ID, A_ROOM_ID) |
||||
assertThat(manager.activeCall.value).isEqualTo( |
||||
ActiveCall( |
||||
sessionId = A_SESSION_ID, |
||||
roomId = A_ROOM_ID, |
||||
callState = CallState.InCall, |
||||
) |
||||
) |
||||
|
||||
runCurrent() |
||||
|
||||
sendCallNotifyLambda.assertions().isCalledOnce() |
||||
verify { notificationManagerCompat.cancel(notificationId) } |
||||
} |
||||
|
||||
private fun TestScope.createActiveCallManager( |
||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), |
||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(), |
||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true), |
||||
) = DefaultActiveCallManager( |
||||
coroutineScope = this, |
||||
matrixClientProvider = matrixClientProvider, |
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler, |
||||
ringingCallNotificationCreator = RingingCallNotificationCreator( |
||||
context = InstrumentationRegistry.getInstrumentation().targetContext, |
||||
matrixClientProvider = matrixClientProvider, |
||||
imageLoaderHolder = FakeImageLoaderHolder(), |
||||
notificationBitmapLoader = FakeNotificationBitmapLoader(), |
||||
), |
||||
notificationManagerCompat = notificationManagerCompat, |
||||
) |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.utils |
||||
|
||||
import io.element.android.features.call.impl.notifications.CallNotificationData |
||||
import io.element.android.features.call.impl.utils.ActiveCall |
||||
import io.element.android.features.call.impl.utils.ActiveCallManager |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
|
||||
class FakeActiveCallManager( |
||||
var registerIncomingCallResult: (CallNotificationData) -> Unit = {}, |
||||
var incomingCallTimedOutResult: () -> Unit = {}, |
||||
var hungUpCallResult: () -> Unit = {}, |
||||
var joinedCallResult: (SessionId, RoomId) -> Unit = { _, _ -> }, |
||||
) : ActiveCallManager { |
||||
override val activeCall = MutableStateFlow<ActiveCall?>(null) |
||||
|
||||
override fun registerIncomingCall(notificationData: CallNotificationData) { |
||||
registerIncomingCallResult(notificationData) |
||||
} |
||||
|
||||
override fun incomingCallTimedOut() { |
||||
incomingCallTimedOutResult() |
||||
} |
||||
|
||||
override fun hungUpCall() { |
||||
hungUpCallResult() |
||||
} |
||||
|
||||
override fun joinedCall(sessionId: SessionId, roomId: RoomId) { |
||||
joinedCallResult(sessionId, roomId) |
||||
} |
||||
|
||||
fun setActiveCall(value: ActiveCall?) { |
||||
this.activeCall.value = value |
||||
} |
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-compose-library") |
||||
id("kotlin-parcelize") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.features.call.test" |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(projects.libraries.architecture) |
||||
implementation(projects.libraries.core) |
||||
|
||||
api(projects.features.call.api) |
||||
implementation(projects.features.call.impl) |
||||
implementation(projects.libraries.matrix.api) |
||||
implementation(projects.libraries.matrix.test) |
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.test |
||||
|
||||
import io.element.android.features.call.impl.notifications.CallNotificationData |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL |
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID |
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME |
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID |
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2 |
||||
import io.element.android.libraries.matrix.test.A_USER_NAME |
||||
|
||||
fun aCallNotificationData( |
||||
sessionId: SessionId = A_SESSION_ID, |
||||
roomId: RoomId = A_ROOM_ID, |
||||
eventId: EventId = AN_EVENT_ID, |
||||
senderId: UserId = A_USER_ID_2, |
||||
roomName: String = A_ROOM_NAME, |
||||
senderName: String? = A_USER_NAME, |
||||
avatarUrl: String? = AN_AVATAR_URL, |
||||
notificationChannelId: String = "channel_id", |
||||
timestamp: Long = 0L, |
||||
): CallNotificationData = CallNotificationData( |
||||
sessionId = sessionId, |
||||
roomId = roomId, |
||||
eventId = eventId, |
||||
senderId = senderId, |
||||
roomName = roomName, |
||||
senderName = senderName, |
||||
avatarUrl = avatarUrl, |
||||
notificationChannelId = notificationChannelId, |
||||
timestamp = timestamp, |
||||
) |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.call.test |
||||
|
||||
import io.element.android.features.call.api.CallType |
||||
import io.element.android.features.call.api.ElementCallEntryPoint |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
|
||||
class FakeElementCallEntryPoint( |
||||
var startCallResult: (CallType) -> Unit = {}, |
||||
var handleIncomingCallResult: (CallType.RoomCall, EventId, UserId, String?, String?, String?, String) -> Unit = { _, _, _, _, _, _, _ -> } |
||||
) : ElementCallEntryPoint { |
||||
override fun startCall(callType: CallType) { |
||||
startCallResult(callType) |
||||
} |
||||
|
||||
override fun handleIncomingCall( |
||||
callType: CallType.RoomCall, |
||||
eventId: EventId, |
||||
senderId: UserId, |
||||
roomName: String?, |
||||
senderName: String?, |
||||
avatarUrl: String?, |
||||
timestamp: Long, |
||||
notificationChannelId: String |
||||
) { |
||||
handleIncomingCallResult(callType, eventId, senderId, roomName, senderName, avatarUrl, notificationChannelId) |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_create_room_action_create_room">"Uus jututuba"</string> |
||||
<string name="screen_create_room_add_people_title">"Kutsu osalejaid"</string> |
||||
<string name="screen_create_room_private_option_description">"Sõnumid siin jututoas on krüptitud ja seda ei saa hiljem välja lülitada."</string> |
||||
<string name="screen_create_room_public_option_description">"Sõnumid pole krüptitud ja neid saavad kõik lugeda. Soovi korral saad hiljem krüptimise sisse lülitada."</string> |
||||
<string name="screen_create_room_room_name_label">"Jututoa nimi"</string> |
||||
<string name="screen_create_room_title">"Loo jututuba"</string> |
||||
</resources> |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_notification_optin_subtitle">"Sa võid seadistusi hiljem alati muuta."</string> |
||||
<string name="screen_notification_optin_title">"Luba teavitused ja kunagi ei jää sul sõnumid märkamata"</string> |
||||
<string name="screen_welcome_bullet_1">"Kõned, küsitlused, otsing ja palju muud lisanduvad hiljem selle aasta jooksul."</string> |
||||
<string name="screen_welcome_bullet_2">"Krüptitud jututubade sõnumite ajalugu pole veel saadaval."</string> |
||||
<string name="screen_welcome_bullet_3">"Me soovime teada mida sa arvad. Seadistuste lehel olevast valikust võid saata meile oma kommentaare."</string> |
||||
<string name="screen_welcome_button">"Alustame!"</string> |
||||
<string name="screen_welcome_subtitle">"Sa peaksid teadma alljärgnevat:"</string> |
||||
<string name="screen_welcome_title">"Tere tulemast rakendusse %1$s!"</string> |
||||
</resources> |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="leave_conversation_alert_subtitle">"Kas sa oled kindel, et soovid sellest vestlusest lahkuda? See vestlus pole avalik ja uuesti liitumiseks vajad kutset."</string> |
||||
<string name="leave_room_alert_empty_subtitle">"Kas sa oled kindel, et soovid sellest jututoast lahkuda? Sa oled siin viimane osaleja ja peale sinu lahkumist ei saa keegi enam liituda, isegi sina mitte."</string> |
||||
<string name="leave_room_alert_private_subtitle">"Kas sa oled kindel, et soovid sellest jututoast lahkuda? See jututuba pole avalik ja uuesti liitumiseks vajad kutset."</string> |
||||
<string name="leave_room_alert_subtitle">"Kas sa oled kindel, et soovid sellest jututoast lahkuda?"</string> |
||||
</resources> |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_app_lock_biometric_authentication">"biomeetrilist autentimist"</string> |
||||
<string name="screen_app_lock_biometric_unlock">"biomeetrilist lukustuse eemaldamist"</string> |
||||
<string name="screen_app_lock_biometric_unlock_title_android">"Eemalda lukustus biomeetrilise tuvastuse abil"</string> |
||||
<string name="screen_app_lock_forgot_pin">"Kas unustasid PIN-koodi?"</string> |
||||
<string name="screen_app_lock_settings_change_pin">"Muuda PIN-koodi"</string> |
||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Kasuta lukustuse eemaldamiseks biomeetrilist tuvastust"</string> |
||||
<string name="screen_app_lock_settings_remove_pin">"Eemalda PIN-kood"</string> |
||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Kas sa oled kindel, et soovid eemaldada PIN-koodi?"</string> |
||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Kas eemaldame PIN-koodi?"</string> |
||||
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Kasuta %1$s"</string> |
||||
<string name="screen_app_lock_setup_biometric_unlock_skip">"Pigem kasutan PIN-koodi"</string> |
||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästa aega ja kasuta alati %1$s rakenduse lukustuse eemaldamiseks"</string> |
||||
<string name="screen_app_lock_setup_choose_pin">"Vali PIN-kood"</string> |
||||
<string name="screen_app_lock_setup_confirm_pin">"Korda PIN-koodi"</string> |
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Turvakaalutlustel sa ei saa sellist PIN-koodi kasutada"</string> |
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Kasuta mõnda teist PIN-koodi"</string> |
||||
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s vestlustele turvalisust ja privaatsust, lukusta oma nutiseade. |
||||
|
||||
Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvakaalutlustel logitakse sind rakendusest välja."</string> |
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Palun sisesta sama PIN-kood kaks korda"</string> |
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koodid ei klapi omavahel"</string> |
||||
<string name="screen_app_lock_signout_alert_message">"Jätkamaks pead uuesti sisse logima ja looma uue PIN-koodi"</string> |
||||
<string name="screen_app_lock_signout_alert_title">"Sa oled logimas välja"</string> |
||||
<plurals name="screen_app_lock_subtitle"> |
||||
<item quantity="one">"Sul on lukustuse eemaldamiseks jäänud %1$d katse"</item> |
||||
<item quantity="other">"Sul on lukustuse eemaldamiseks jäänud %1$d katset"</item> |
||||
</plurals> |
||||
<plurals name="screen_app_lock_subtitle_wrong_pin"> |
||||
<item quantity="one">"Vale PIN-kood. Saad proovida veel %1$d korra"</item> |
||||
<item quantity="other">"Vale PIN-kood. Saad proovida veel %1$d korda"</item> |
||||
</plurals> |
||||
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetrilist tuvastust"</string> |
||||
<string name="screen_app_lock_use_pin_android">"Kasuta PIN-koodi"</string> |
||||
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string> |
||||
</resources> |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="screen_account_provider_change">"Muuda teenusepakkujat"</string> |
||||
<string name="screen_account_provider_form_hint">"Koduserveri aadress"</string> |
||||
<string name="screen_account_provider_form_notice">"Sisesta otsingusõna või domeeni nimi."</string> |
||||
<string name="screen_account_provider_form_subtitle">"Otsi äriühingut, kogukonda või võrgus leiduvat Matrixi serverit."</string> |
||||
<string name="screen_account_provider_form_title">"Leia teenusepakkuja"</string> |
||||
<string name="screen_account_provider_signin_title">"Sa oled sisse logimas %s teenusesse"</string> |
||||
<string name="screen_account_provider_signup_title">"Sa oled loomas kasutajakontot %s teenuses"</string> |
||||
<string name="screen_change_server_form_header">"Koduserveri url"</string> |
||||
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string> |
||||
<string name="screen_change_server_title">"Vali oma server"</string> |
||||
<string name="screen_login_error_deactivated_account">"Konto on kasutusest eemaldatud."</string> |
||||
<string name="screen_login_error_invalid_credentials">"Vigane kasutajanimi ja/või salasõna"</string> |
||||
<string name="screen_login_error_invalid_user_id">"See ei ole korrektne kasutajanimi. Õige vorming on: „@kasutaja:koduserver.ee“"</string> |
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Proovi uuesti"</string> |
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Jätkamiseks pead lubama, et %1$s saab kasutada sinu nutiseadme kaamerat"</string> |
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"QR-koodi lugemiseks luba kaamerat kasutada"</string> |
||||
<string name="screen_qr_code_login_scanning_state_title">"Skaneeri QR-koodi"</string> |
||||
<string name="screen_qr_code_login_start_over_button">"Alusta uuesti"</string> |
||||
<string name="screen_qr_code_login_unknown_error_description">"Tekkis ootamatu viga. Palun proovi uuesti."</string> |
||||
<string name="screen_qr_code_login_verify_code_loading">"Ootame sinu teise seadme järgi"</string> |
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Sinu teenusepakkuja võib sisselogimisel eeldada selle verifitseerimiskoodi kasutamist."</string> |
||||
<string name="screen_qr_code_login_verify_code_title">"Sinu verifitseerimiskood"</string> |
||||
<string name="screen_waitlist_message_success">"Tere tulemast rakendusse %1$s!"</string> |
||||
</resources> |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue