Element Call SPA integration (#1283)
* Integrate Element Call into EX, being able to open its URLs and handle the call in-app. * Add custom scheme support with format `element:call?url=...`. * Update androix.webkit * Silence the foreground service notification. - Allow foreground service tap action to re-open the ongoing call. - Unify notification small icons in different modules using a vector one. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
committed by
GitHub
parent
36119106a9
commit
a06bea4d71
@@ -198,6 +198,7 @@ dependencies {
|
||||
allLibrariesImpl()
|
||||
allServicesImpl()
|
||||
allFeaturesImpl(rootDir, logger)
|
||||
implementation(projects.features.call)
|
||||
implementation(projects.anvilannotations)
|
||||
implementation(projects.appnav)
|
||||
anvil(projects.anvilcodegen)
|
||||
|
||||
1
changelog.d/1300.feature
Normal file
1
changelog.d/1300.feature
Normal file
@@ -0,0 +1 @@
|
||||
Integrate Element Call into EX by embedding a call in a WebView.
|
||||
37
features/call/build.gradle.kts
Normal file
37
features/call/build.gradle.kts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.call"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(libs.androidx.webkit)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
}
|
||||
61
features/call/src/main/AndroidManifest.xml
Normal file
61
features/call/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
~ Copyright (c) 2023 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ElementCallActivity"
|
||||
android:label="@string/element_call"
|
||||
android:exported="true"
|
||||
android:taskAffinity="io.element.android.features.call"
|
||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:host="call.element.io" />
|
||||
</intent-filter>
|
||||
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="element" />
|
||||
<data android:host="call" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<service android:name=".CallForegroundService" android:enabled="true" android:foregroundServiceType="mediaPlayback" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
class CallForegroundService : Service() {
|
||||
|
||||
companion object {
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, CallForegroundService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val intent = Intent(context, CallForegroundService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var notificationManagerCompat: NotificationManagerCompat
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
notificationManagerCompat = NotificationManagerCompat.from(this)
|
||||
|
||||
val foregroundServiceChannel = NotificationChannelCompat.Builder(
|
||||
"call_foreground_service_channel",
|
||||
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||
).setName(
|
||||
getString(R.string.call_foreground_service_channel_title_android).ifEmpty { "Ongoing call" }
|
||||
).build()
|
||||
notificationManagerCompat.createNotificationChannel(foregroundServiceChannel)
|
||||
|
||||
val callActivityIntent = Intent(this, ElementCallActivity::class.java)
|
||||
val pendingIntent = PendingIntentCompat.getActivity(this, 0, callActivityIntent, 0, false)
|
||||
val notification = NotificationCompat.Builder(this, foregroundServiceChannel.id)
|
||||
.setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification_small))
|
||||
.setContentTitle(getString(R.string.call_foreground_service_title_android))
|
||||
.setContentText(getString(R.string.call_foreground_service_message_android))
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
startForeground(1, notification)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.net.Uri
|
||||
import java.net.URLDecoder
|
||||
|
||||
object CallIntentDataParser {
|
||||
|
||||
private val validHttpSchemes = sequenceOf("http", "https")
|
||||
|
||||
fun parse(data: String?): String? {
|
||||
val parsedUrl = data?.let { Uri.parse(data) } ?: return null
|
||||
val scheme = parsedUrl.scheme
|
||||
return when {
|
||||
scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> data
|
||||
scheme == "element" && parsedUrl.host == "call" -> {
|
||||
// We use this custom scheme to load arbitrary URLs for other instances of Element Call,
|
||||
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host
|
||||
parsedUrl.getQueryParameter("url")
|
||||
?.let { URLDecoder.decode(it, "utf-8") }
|
||||
?.takeIf {
|
||||
val internalUri = Uri.parse(it)
|
||||
internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank()
|
||||
}
|
||||
}
|
||||
// This should never be possible, but we still need to take into account the possibility
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
typealias RequestPermissionCallback = (Array<String>) -> Unit
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun CallScreenView(
|
||||
url: String?,
|
||||
userAgent: String,
|
||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElementTheme {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.element_call)) },
|
||||
navigationIcon = {
|
||||
BackButton(
|
||||
imageVector = Icons.Default.Close,
|
||||
onClick = onClose
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
CallWebView(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.fillMaxSize(),
|
||||
url = url,
|
||||
userAgent = userAgent,
|
||||
onPermissionsRequested = { request ->
|
||||
val androidPermissions = mapWebkitPermissions(request.resources)
|
||||
val callback: RequestPermissionCallback = { request.grant(it) }
|
||||
requestPermissions(androidPermissions.toTypedArray(), callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallWebView(
|
||||
url: String?,
|
||||
userAgent: String,
|
||||
onPermissionsRequested: (PermissionRequest) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isInpectionMode = LocalInspectionMode.current
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
if (!isInpectionMode) {
|
||||
setup(userAgent, onPermissionsRequested)
|
||||
if (url != null) {
|
||||
loadUrl(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
update = { webView ->
|
||||
if (!isInpectionMode && url != null) {
|
||||
webView.loadUrl(url)
|
||||
}
|
||||
},
|
||||
onRelease = { webView ->
|
||||
webView.destroy()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun WebView.setup(userAgent: String, onPermissionsRequested: (PermissionRequest) -> Unit) {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
with(settings) {
|
||||
javaScriptEnabled = true
|
||||
allowContentAccess = true
|
||||
allowFileAccess = true
|
||||
domStorageEnabled = true
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
databaseEnabled = true
|
||||
loadsImagesAutomatically = true
|
||||
userAgentString = userAgent
|
||||
}
|
||||
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onPermissionRequest(request: PermissionRequest) {
|
||||
onPermissionsRequested(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun CallScreenViewPreview() {
|
||||
ElementTheme {
|
||||
CallScreenView(
|
||||
url = "https://call.element.io/some-actual-call?with=parameters",
|
||||
userAgent = "",
|
||||
requestPermissions = { _, _ -> },
|
||||
onClose = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import android.webkit.PermissionRequest
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.element.android.features.call.di.CallBindings
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class ElementCallActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var userAgentProvider: UserAgentProvider
|
||||
|
||||
private lateinit var audioManager: AudioManager
|
||||
|
||||
private var requestPermissionCallback: RequestPermissionCallback? = null
|
||||
|
||||
private var audiofocusRequest: AudioFocusRequest? = null
|
||||
private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
|
||||
|
||||
private val requestPermissionsLauncher = registerPermissionResultLauncher()
|
||||
|
||||
private var isDarkMode = false
|
||||
private val urlState = mutableStateOf<String?>(null)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationContext.bindings<CallBindings>().inject(this)
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
urlState.value = intent?.dataString?.let(::parseUrl) ?: run {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
updateUiMode(resources.configuration)
|
||||
}
|
||||
|
||||
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
requestAudioFocus()
|
||||
|
||||
val userAgent = userAgentProvider.provide()
|
||||
|
||||
setContent {
|
||||
CallScreenView(
|
||||
url = urlState.value,
|
||||
userAgent = userAgent,
|
||||
onClose = this::finish,
|
||||
requestPermissions = { permissions, callback ->
|
||||
requestPermissionCallback = callback
|
||||
requestPermissionsLauncher.launch(permissions)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
updateUiMode(newConfig)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
val intentUrl = intent?.dataString?.let(::parseUrl)
|
||||
when {
|
||||
// New URL, update it and reload the webview
|
||||
intentUrl != null -> urlState.value = intentUrl
|
||||
// Re-opened the activity but we have no url to load or a cached one, finish the activity
|
||||
intent?.dataString == null && urlState.value == null -> finish()
|
||||
// Coming back from notification, do nothing
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
CallForegroundService.stop(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (!isFinishing && !isChangingConfigurations) {
|
||||
CallForegroundService.start(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
releaseAudioFocus()
|
||||
CallForegroundService.stop(this)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// Also remove the task from recents
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
|
||||
private fun parseUrl(url: String?): String? = CallIntentDataParser.parse(url)
|
||||
|
||||
private fun registerPermissionResultLauncher(): ActivityResultLauncher<Array<String>> {
|
||||
return registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
val callback = requestPermissionCallback ?: return@registerForActivityResult
|
||||
val permissionsToGrant = mutableListOf<String>()
|
||||
permissions.forEach { (permission, granted) ->
|
||||
if (granted) {
|
||||
val webKitPermission = when (permission) {
|
||||
Manifest.permission.CAMERA -> PermissionRequest.RESOURCE_VIDEO_CAPTURE
|
||||
Manifest.permission.RECORD_AUDIO -> PermissionRequest.RESOURCE_AUDIO_CAPTURE
|
||||
else -> return@forEach
|
||||
}
|
||||
permissionsToGrant.add(webKitPermission)
|
||||
}
|
||||
}
|
||||
callback(permissionsToGrant.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun requestAudioFocus() {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.build()
|
||||
audioManager.requestAudioFocus(request)
|
||||
audiofocusRequest = request
|
||||
} else {
|
||||
val listener = AudioManager.OnAudioFocusChangeListener { }
|
||||
audioManager.requestAudioFocus(
|
||||
listener,
|
||||
AudioManager.STREAM_VOICE_CALL,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
|
||||
)
|
||||
|
||||
audioFocusChangeListener = listener
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun releaseAudioFocus() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
audiofocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
|
||||
} else {
|
||||
audioFocusChangeListener?.let { audioManager.abandonAudioFocus(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUiMode(configuration: Configuration) {
|
||||
val prevDarkMode = isDarkMode
|
||||
val currentNightMode = configuration.uiMode and Configuration.UI_MODE_NIGHT_YES
|
||||
isDarkMode = currentNightMode != 0
|
||||
if (prevDarkMode != isDarkMode) {
|
||||
if (isDarkMode) {
|
||||
window.setBackgroundDrawableResource(android.R.drawable.screen_background_dark)
|
||||
} else {
|
||||
window.setBackgroundDrawableResource(android.R.drawable.screen_background_light)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun mapWebkitPermissions(permissions: Array<String>): List<String> {
|
||||
return permissions.mapNotNull { permission ->
|
||||
when (permission) {
|
||||
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> Manifest.permission.RECORD_AUDIO
|
||||
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> Manifest.permission.CAMERA
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.features.call.ElementCallActivity
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface CallBindings {
|
||||
fun inject(callActivity: ElementCallActivity)
|
||||
}
|
||||
6
features/call/src/main/res/values-fr/translations.xml
Normal file
6
features/call/src/main/res/values-fr/translations.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="call_foreground_service_channel_title_android">"Appel en cours"</string>
|
||||
<string name="call_foreground_service_message_android">"Appuyez pour retourner à l\'appel."</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Appel en cours"</string>
|
||||
</resources>
|
||||
20
features/call/src/main/res/values/do_not_translate.xml
Normal file
20
features/call/src/main/res/values/do_not_translate.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2023 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string translatable="false" name="element_call">Element Call</string>
|
||||
</resources>
|
||||
6
features/call/src/main/res/values/localazy.xml
Normal file
6
features/call/src/main/res/values/localazy.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="call_foreground_service_channel_title_android">"Ongoing call"</string>
|
||||
<string name="call_foreground_service_message_android">"Tap to return to the call"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Call in progress"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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 com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.net.URLEncoder
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class CallIntentDataParserTests {
|
||||
|
||||
@Test
|
||||
fun `a null data returns null`() {
|
||||
val url: String? = null
|
||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty data returns null`() {
|
||||
val url = ""
|
||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid data returns null`() {
|
||||
val url = "!"
|
||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `data with no scheme returns null`() {
|
||||
val url = "test"
|
||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call urls will be returned as is`() {
|
||||
val httpBaseUrl = "http://call.element.io"
|
||||
val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters"
|
||||
val httpsBaseUrl = "https://call.element.io"
|
||||
val httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters"
|
||||
assertThat(CallIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl)
|
||||
assertThat(CallIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl)
|
||||
assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl)
|
||||
assertThat(CallIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HTTP and HTTPS urls that don't come from EC return null`() {
|
||||
val httpBaseUrl = "http://app.element.io"
|
||||
val httpsBaseUrl = "https://app.element.io"
|
||||
val httpInvalidUrl = "http://"
|
||||
val httpsInvalidUrl = "http://"
|
||||
assertThat(CallIntentDataParser.parse(httpBaseUrl)).isNull()
|
||||
assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isNull()
|
||||
assertThat(CallIntentDataParser.parse(httpInvalidUrl)).isNull()
|
||||
assertThat(CallIntentDataParser.parse(httpsInvalidUrl)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme with call host and url param gets url extracted`() {
|
||||
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
|
||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||
val url = "element://call?url=$encodedUrl"
|
||||
assertThat(CallIntentDataParser.parse(url)).isEqualTo(embeddedUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme with call host and no url param returns null`() {
|
||||
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
|
||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||
val url = "element://call?no-url=$encodedUrl"
|
||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme with no call host returns null`() {
|
||||
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
|
||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||
val url = "element://no-call?url=$encodedUrl"
|
||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme with no data returns null`() {
|
||||
val url = "element://call?url="
|
||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.Manifest
|
||||
import android.webkit.PermissionRequest
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class MapWebkitPermissionsTest {
|
||||
|
||||
@Test
|
||||
fun `given Webkit's RESOURCE_AUDIO_CAPTURE returns Android's RECORD_AUDIO permission`() {
|
||||
val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE))
|
||||
assertThat(permission).isEqualTo(listOf(Manifest.permission.RECORD_AUDIO))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given Webkit's RESOURCE_VIDEO_CAPTURE returns Android's CAMERA permission`() {
|
||||
val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
|
||||
assertThat(permission).isEqualTo(listOf(Manifest.permission.CAMERA))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given any other permission, it returns nothing`() {
|
||||
val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID))
|
||||
assertThat(permission).isEqualTo(emptyList<String>())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -91,6 +91,7 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re
|
||||
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||
androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" }
|
||||
androidx_preference = "androidx.preference:preference:1.2.1"
|
||||
androidx_webkit = "androidx.webkit:webkit:1.8.0"
|
||||
|
||||
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
|
||||
# Warning: issue on alpha07, make sure this is working when upgrading
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.libraries.designsystem.utils
|
||||
|
||||
import io.element.android.libraries.designsystem.R
|
||||
|
||||
typealias CommonDrawables = R.drawable
|
||||
@@ -0,0 +1,7 @@
|
||||
<vector android:height="32dp" android:viewportHeight="54"
|
||||
android:viewportWidth="54" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#000000" android:pathData="M19.4,3.2C19.4,1.5 20.9,0 22.7,0C34.6,0 44.3,9.7 44.3,21.6C44.3,23.4 42.8,24.8 41,24.8C39.3,24.8 37.8,23.4 37.8,21.6C37.8,13.3 31,6.5 22.7,6.5C20.9,6.5 19.4,5 19.4,3.2Z"/>
|
||||
<path android:fillColor="#000000" android:pathData="M34.6,50.8C34.6,52.6 33.1,54 31.3,54C19.4,54 9.7,44.3 9.7,32.4C9.7,30.6 11.2,29.2 13,29.2C14.8,29.2 16.2,30.6 16.2,32.4C16.2,40.8 23,47.5 31.3,47.5C33.1,47.5 34.6,49 34.6,50.8Z"/>
|
||||
<path android:fillColor="#000000" android:pathData="M3.2,34.6C1.5,34.6 -0,33.1 -0,31.3C-0,19.4 9.7,9.7 21.6,9.7C23.4,9.7 24.8,11.2 24.8,13C24.8,14.8 23.4,16.2 21.6,16.2C13.3,16.2 6.5,23 6.5,31.3C6.5,33.1 5,34.6 3.2,34.6Z"/>
|
||||
<path android:fillColor="#000000" android:pathData="M50.8,19.4C52.6,19.4 54,20.9 54,22.7C54,34.6 44.3,44.3 32.4,44.3C30.6,44.3 29.2,42.8 29.2,41C29.2,39.3 30.6,37.8 32.4,37.8C40.8,37.8 47.5,31 47.5,22.7C47.5,20.9 49,19.4 50.8,19.4Z"/>
|
||||
</vector>
|
||||
@@ -38,6 +38,7 @@ dependencies {
|
||||
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.network)
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
@@ -67,7 +68,7 @@ class NotificationFactory @Inject constructor(
|
||||
else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)
|
||||
}
|
||||
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
@@ -141,7 +142,7 @@ class NotificationFactory @Inject constructor(
|
||||
inviteNotifiableEvent: InviteNotifiableEvent
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
@@ -185,7 +186,7 @@ class NotificationFactory @Inject constructor(
|
||||
simpleNotifiableEvent: SimpleNotifiableEvent,
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
@@ -220,7 +221,7 @@ class NotificationFactory @Inject constructor(
|
||||
fallbackNotifiableEvent: FallbackNotifiableEvent,
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(false)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
@@ -261,7 +262,7 @@ class NotificationFactory @Inject constructor(
|
||||
lastMessageTimestamp: Long
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
val smallIcon = R.drawable.ic_notification
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
val channelId = notificationChannels.getChannelIdForMessage(noisy)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(true)
|
||||
@@ -301,7 +302,7 @@ class NotificationFactory @Inject constructor(
|
||||
return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest())
|
||||
.setContentTitle(buildMeta.applicationName)
|
||||
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setSmallIcon(CommonDrawables.ic_notification_small)
|
||||
.setLargeIcon(getBitmap(R.drawable.element_logo_green))
|
||||
.setColor(ContextCompat.getColor(context, R.color.notification_accent_color))
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -195,9 +195,9 @@
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"Další nastavení"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Halsové a video hovory"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Neshoda konfigurace"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností.
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností.
|
||||
|
||||
Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní.
|
||||
Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní.
|
||||
|
||||
Pokud budete pokračovat, některá nastavení se mohou změnit."</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Přímé zprávy"</string>
|
||||
|
||||
@@ -67,9 +67,6 @@
|
||||
<string name="action_take_photo">"Prendre une photo"</string>
|
||||
<string name="action_view_source">"Afficher la source"</string>
|
||||
<string name="action_yes">"Oui"</string>
|
||||
<string name="call_foreground_service_channel_title_android">"Appel en cours"</string>
|
||||
<string name="call_foreground_service_message_android">"Appuyez pour retourner à l\'appel."</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Appel en cours"</string>
|
||||
<string name="common_about">"À propos"</string>
|
||||
<string name="common_acceptable_use_policy">"Politique d\'utilisation acceptable"</string>
|
||||
<string name="common_advanced_settings">"Paramètres avancés"</string>
|
||||
|
||||
@@ -194,9 +194,9 @@
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"Дополнительные параметры"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Аудио и видео звонки"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Несоответствие конфигурации"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Мы упростили настройки уведомлений, чтобы упростить поиск опций.
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Мы упростили настройки уведомлений, чтобы упростить поиск опций.
|
||||
|
||||
Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны.
|
||||
Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны.
|
||||
|
||||
Если вы продолжите, некоторые настройки могут быть изменены."</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Прямые чаты"</string>
|
||||
|
||||
@@ -67,9 +67,6 @@
|
||||
<string name="action_take_photo">"Take photo"</string>
|
||||
<string name="action_view_source">"View Source"</string>
|
||||
<string name="action_yes">"Yes"</string>
|
||||
<string name="call_foreground_service_channel_title_android">"Ongoing call"</string>
|
||||
<string name="call_foreground_service_message_android">"Tap to return to the call"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Call in progress"</string>
|
||||
<string name="common_about">"About"</string>
|
||||
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
|
||||
<string name="common_advanced_settings">"Advanced settings"</string>
|
||||
@@ -207,6 +204,11 @@
|
||||
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
|
||||
<string name="room_timeline_read_marker_title">"New"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Share analytics data"</string>
|
||||
<string name="screen_edit_profile_display_name">"Display name"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
||||
<string name="screen_edit_profile_error">"An unknown error was encountered and the information couldn\'t be changed."</string>
|
||||
<string name="screen_edit_profile_error_title">"Unable to update profile"</string>
|
||||
<string name="screen_edit_profile_updating_details">"Updating profile…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
|
||||
@@ -52,6 +52,7 @@ dependencies {
|
||||
// `testOptions { unitTests.isIncludeAndroidResources = true }` in the app build.gradle.kts file
|
||||
// implementation(projects.app)
|
||||
implementation(projects.appnav)
|
||||
implementation(projects.features.call)
|
||||
allLibrariesImpl()
|
||||
allServicesImpl()
|
||||
allFeaturesImpl(rootDir, logger)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -48,6 +48,8 @@
|
||||
<ignore path="**/localazy.xml" />
|
||||
<!-- Ignore unused resource in debug build type -->
|
||||
<ignore path="**/src/debug/**" />
|
||||
<!-- Ignore unused resources in designsystem since they're imported elsewhere through aliases and can't be properly detected -->
|
||||
<ignore path="**/libraries/designsystem/src/main/res/**" />
|
||||
</issue>
|
||||
|
||||
<!-- UX -->
|
||||
|
||||
@@ -128,6 +128,12 @@
|
||||
"includeRegex": [
|
||||
"screen_create_poll_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": ":features:call",
|
||||
"includeRegex": [
|
||||
"call_.*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user