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,
|
||||
hideInviteAvatars: Boolean,
|
||||
isInviteSeen: Boolean,
|
||||
showUnreadCount: Boolean = false,
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
eventSink: (RoomListEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showUnreadCount: Boolean = false,
|
||||
eventSink: (RoomListEvent) -> Unit,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
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.
|
||||
~ 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_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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>
|
||||
<service
|
||||
android:name=".live.service.LiveLocationSharingService"
|
||||
|
||||
+49
-15
@@ -11,6 +11,9 @@ package io.element.android.features.messages.impl
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
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.lifecycle.lifecycleScope
|
||||
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.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
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.ShowLocationEntryPoint
|
||||
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.poll.api.create.CreatePollEntryPoint
|
||||
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.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
@@ -121,6 +126,7 @@ class MessagesFlowNode(
|
||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val hasVulkanSupport: DeviceHasVulkanSupport,
|
||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
||||
@@ -188,6 +194,8 @@ class MessagesFlowNode(
|
||||
|
||||
private val callback: MessagesEntryPoint.Callback = callback()
|
||||
|
||||
private var displayVulkanNotSupportedError by mutableStateOf(false)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
@@ -267,7 +275,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToSendLocation() {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToCreatePoll() {
|
||||
@@ -279,7 +291,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToCurrentLiveLocation() {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
||||
@@ -506,7 +522,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToSendLocation() {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToCreatePoll() {
|
||||
@@ -518,7 +538,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToCurrentLiveLocation() {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
||||
@@ -607,18 +631,23 @@ class MessagesFlowNode(
|
||||
)
|
||||
}
|
||||
is TimelineItemLocationContent -> {
|
||||
val mode = when (event.content.mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
|
||||
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
|
||||
location = event.content.mode.location,
|
||||
senderName = event.safeSenderName,
|
||||
senderId = event.senderId,
|
||||
senderAvatarUrl = event.senderAvatar.url,
|
||||
timestamp = event.sentTimeMillis,
|
||||
assetType = event.content.assetType,
|
||||
)
|
||||
if (hasVulkanSupport()) {
|
||||
val mode = when (event.content.mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
|
||||
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
|
||||
location = event.content.mode.location,
|
||||
senderName = event.safeSenderName,
|
||||
senderId = event.senderId,
|
||||
senderAvatarUrl = event.senderAvatar.url,
|
||||
timestamp = event.sentTimeMillis,
|
||||
assetType = event.content.assetType,
|
||||
)
|
||||
}
|
||||
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
null
|
||||
}
|
||||
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@@ -691,6 +720,11 @@ class MessagesFlowNode(
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
mentionSpanTheme.updateStyles()
|
||||
|
||||
if (displayVulkanNotSupportedError) {
|
||||
RenderingMapsNotSupportedDialog { displayVulkanNotSupportedError = false }
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
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.timeline.createTimelineController
|
||||
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.matrix.api.core.EventId
|
||||
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.node.TestParentNode
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -85,6 +87,7 @@ class DefaultMessagesEntryPointTest {
|
||||
knockRequestsListEntryPoint = FakeKnockRequestsListEntryPoint(),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
hasVulkanSupport = DeviceHasVulkanSupport(mockk(relaxed = true))
|
||||
)
|
||||
}
|
||||
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_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="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>
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4f64455657f0f8a5ac7eec71ee206721a79b25dacf470d105a0e6497e570204a
|
||||
size 23954
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43bd5f04f170372b120fa2223c632d6a969c45b6d4fe61b161a7109318f9941f
|
||||
size 22658
|
||||
Reference in New Issue
Block a user