mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Make Vulkan a *not required* feature (#6961)
* Make Vulkan a *not required* feature * Add `DeviceHasVulkanSupport` helper to check this on runtime * When trying to open a screen with a map in it, check if vulkan is supported and display a new error dialog in that case * Fix random lint issue * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
27cdc0fe7d
commit
a91e1fa2ae
+2
-2
@@ -75,10 +75,10 @@ internal fun RoomSummaryRow(
|
|||||||
room: RoomListRoomSummary,
|
room: RoomListRoomSummary,
|
||||||
hideInviteAvatars: Boolean,
|
hideInviteAvatars: Boolean,
|
||||||
isInviteSeen: Boolean,
|
isInviteSeen: Boolean,
|
||||||
showUnreadCount: Boolean = false,
|
|
||||||
onClick: (RoomListRoomSummary) -> Unit,
|
onClick: (RoomListRoomSummary) -> Unit,
|
||||||
eventSink: (RoomListEvent) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
showUnreadCount: Boolean = false,
|
||||||
|
eventSink: (RoomListEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
when (room.displayType) {
|
when (room.displayType) {
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.location.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderingMapsNotSupportedDialog(onSubmit: () -> Unit) {
|
||||||
|
ErrorDialog(
|
||||||
|
title = stringResource(CommonStrings.vulkan_not_supported_dialog_title_android),
|
||||||
|
content = stringResource(CommonStrings.vulkan_not_supported_dialog_content_android),
|
||||||
|
onSubmit = onSubmit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun RenderingMapsNotSupportedDialogPreview() = ElementPreview {
|
||||||
|
RenderingMapsNotSupportedDialog(onSubmit = {})
|
||||||
|
}
|
||||||
@@ -5,13 +5,21 @@
|
|||||||
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
~ Please see LICENSE files in the repository root for full details.
|
~ Please see LICENSE files in the repository root for full details.
|
||||||
-->
|
-->
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Declare Vulkan as an optional feature to prevent installation issues on devices that do not support it. -->
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.vulkan.version"
|
||||||
|
android:required="false"
|
||||||
|
tools:replace="android:required" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<service
|
<service
|
||||||
android:name=".live.service.LiveLocationSharingService"
|
android:name=".live.service.LiveLocationSharingService"
|
||||||
|
|||||||
+34
@@ -11,6 +11,9 @@ package io.element.android.features.messages.impl
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
@@ -29,6 +32,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint
|
|||||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||||
import io.element.android.features.location.api.LocationService
|
import io.element.android.features.location.api.LocationService
|
||||||
|
import io.element.android.features.location.api.RenderingMapsNotSupportedDialog
|
||||||
import io.element.android.features.location.api.ShareLocationEntryPoint
|
import io.element.android.features.location.api.ShareLocationEntryPoint
|
||||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
import io.element.android.features.location.api.ShowLocationMode
|
||||||
@@ -53,6 +57,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||||||
import io.element.android.features.messages.impl.timeline.model.event.duration
|
import io.element.android.features.messages.impl.timeline.model.event.duration
|
||||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
|
import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport
|
||||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
@@ -121,6 +126,7 @@ class MessagesFlowNode(
|
|||||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||||
private val dateFormatter: DateFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
private val hasVulkanSupport: DeviceHasVulkanSupport,
|
||||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
||||||
@@ -188,6 +194,8 @@ class MessagesFlowNode(
|
|||||||
|
|
||||||
private val callback: MessagesEntryPoint.Callback = callback()
|
private val callback: MessagesEntryPoint.Callback = callback()
|
||||||
|
|
||||||
|
private var displayVulkanNotSupportedError by mutableStateOf(false)
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
@@ -267,7 +275,11 @@ class MessagesFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToSendLocation() {
|
override fun navigateToSendLocation() {
|
||||||
|
if (hasVulkanSupport()) {
|
||||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
|
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
|
||||||
|
} else {
|
||||||
|
displayVulkanNotSupportedError = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToCreatePoll() {
|
override fun navigateToCreatePoll() {
|
||||||
@@ -279,7 +291,11 @@ class MessagesFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToCurrentLiveLocation() {
|
override fun navigateToCurrentLiveLocation() {
|
||||||
|
if (hasVulkanSupport()) {
|
||||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||||
|
} else {
|
||||||
|
displayVulkanNotSupportedError = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
||||||
@@ -506,7 +522,11 @@ class MessagesFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToSendLocation() {
|
override fun navigateToSendLocation() {
|
||||||
|
if (hasVulkanSupport()) {
|
||||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
|
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
|
||||||
|
} else {
|
||||||
|
displayVulkanNotSupportedError = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToCreatePoll() {
|
override fun navigateToCreatePoll() {
|
||||||
@@ -518,7 +538,11 @@ class MessagesFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToCurrentLiveLocation() {
|
override fun navigateToCurrentLiveLocation() {
|
||||||
|
if (hasVulkanSupport()) {
|
||||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||||
|
} else {
|
||||||
|
displayVulkanNotSupportedError = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
||||||
@@ -607,6 +631,7 @@ class MessagesFlowNode(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineItemLocationContent -> {
|
is TimelineItemLocationContent -> {
|
||||||
|
if (hasVulkanSupport()) {
|
||||||
val mode = when (event.content.mode) {
|
val mode = when (event.content.mode) {
|
||||||
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
|
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
|
||||||
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
|
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
|
||||||
@@ -619,6 +644,10 @@ class MessagesFlowNode(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
|
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
|
||||||
|
} else {
|
||||||
|
displayVulkanNotSupportedError = true
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -691,6 +720,11 @@ class MessagesFlowNode(
|
|||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
mentionSpanTheme.updateStyles()
|
mentionSpanTheme.updateStyles()
|
||||||
|
|
||||||
|
if (displayVulkanNotSupportedError) {
|
||||||
|
RenderingMapsNotSupportedDialog { displayVulkanNotSupportedError = false }
|
||||||
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalMentionSpanUpdater provides mentionSpanUpdater
|
LocalMentionSpanUpdater provides mentionSpanUpdater
|
||||||
) {
|
) {
|
||||||
|
|||||||
+3
@@ -23,6 +23,7 @@ import io.element.android.features.messages.api.MessagesEntryPoint
|
|||||||
import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider
|
import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider
|
||||||
import io.element.android.features.messages.impl.timeline.createTimelineController
|
import io.element.android.features.messages.impl.timeline.createTimelineController
|
||||||
import io.element.android.features.poll.test.create.FakeCreatePollEntryPoint
|
import io.element.android.features.poll.test.create.FakeCreatePollEntryPoint
|
||||||
|
import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
@@ -42,6 +43,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
|
|||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -85,6 +87,7 @@ class DefaultMessagesEntryPointTest {
|
|||||||
knockRequestsListEntryPoint = FakeKnockRequestsListEntryPoint(),
|
knockRequestsListEntryPoint = FakeKnockRequestsListEntryPoint(),
|
||||||
dateFormatter = FakeDateFormatter(),
|
dateFormatter = FakeDateFormatter(),
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
hasVulkanSupport = DeviceHasVulkanSupport(mockk(relaxed = true))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : MessagesEntryPoint.Callback {
|
val callback = object : MessagesEntryPoint.Callback {
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.system
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
|
|
||||||
|
private const val VULKAN_VERSION_1_0 = 0x400003
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the device supports Vulkan 1.0.
|
||||||
|
*
|
||||||
|
* This is needed for the location screens that contain maps using MapLibre UI components.
|
||||||
|
*
|
||||||
|
* Needed until https://github.com/maplibre/maplibre-native/issues/3079 is resolved and we can automatically choose between OpenGL and Vulkan renderers,
|
||||||
|
* or no devices support OpenGL anymore.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
class DeviceHasVulkanSupport(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
|
operator fun invoke(): Boolean {
|
||||||
|
return context.packageManager.hasSystemFeature("android.hardware.vulkan.version", VULKAN_VERSION_1_0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -546,4 +546,6 @@ Are you sure you want to continue?"</string>
|
|||||||
<string name="timeline_decryption_failure_historical_event_user_not_joined">"You don\'t have access to this message"</string>
|
<string name="timeline_decryption_failure_historical_event_user_not_joined">"You don\'t have access to this message"</string>
|
||||||
<string name="timeline_decryption_failure_unable_to_decrypt">"Unable to decrypt message"</string>
|
<string name="timeline_decryption_failure_unable_to_decrypt">"Unable to decrypt message"</string>
|
||||||
<string name="timeline_decryption_failure_withheld_unverified">"This message was blocked either because you did not verify your device or because the sender needs to verify your digital identity."</string>
|
<string name="timeline_decryption_failure_withheld_unverified">"This message was blocked either because you did not verify your device or because the sender needs to verify your digital identity."</string>
|
||||||
|
<string name="vulkan_not_supported_dialog_content_android">"Your device is too old, a device with Android 8 or newer is required."</string>
|
||||||
|
<string name="vulkan_not_supported_dialog_title_android">"Rendering maps is not supported"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user