Merge pull request #4011 from nextcloud/contacts_theming

Contacts theming
This commit is contained in:
Marcel Hibbe 2024-08-09 15:51:13 +02:00 коммит произвёл GitHub
Родитель a9593f25b6 4508cbb0aa
Коммит 85b94d679b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
16 изменённых файлов: 555 добавлений и 115 удалений

Просмотреть файл

@ -158,6 +158,8 @@ ext {
workVersion = "2.9.1"
espressoVersion = "3.6.1"
media3_version = "1.4.0"
coroutines_version = "1.3.9"
mockitoKotlinVersion = "4.1.0"
}
configurations.configureEach {
@ -333,6 +335,14 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.06.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}
tasks.register('installGitHooks', Copy) {

Просмотреть файл

@ -13,10 +13,8 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -35,6 +33,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
@ -61,9 +60,8 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModelProvider
import autodagger.AutoInjector
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@ -72,7 +70,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ContactsActivityCompose : ComponentActivity() {
class ContactsActivityCompose : BaseActivity() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@ -83,9 +81,12 @@ class ContactsActivityCompose : ComponentActivity() {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java]
setContent {
MaterialTheme {
val colorScheme = viewThemeUtils.getColorScheme(this)
val uiState = contactsViewModel.contactsViewState.collectAsState()
MaterialTheme(
colorScheme = colorScheme
) {
val context = LocalContext.current
Scaffold(
topBar = {
@ -96,7 +97,6 @@ class ContactsActivityCompose : ComponentActivity() {
)
},
content = {
val uiState = contactsViewModel.contactsViewState.collectAsState()
Column(Modifier.padding(it)) {
ConversationCreationOptions(context = context)
ContactsList(
@ -170,6 +170,7 @@ fun ContactsItem(contacts: List<AutocompleteUser>, contactsViewModel: ContactsVi
}
items(contactsForInitial) { contact ->
ContactItemRow(contact = contact, contactsViewModel = contactsViewModel, context = context)
Log.d(CompanionClass.TAG, "Contacts:$contact")
}
}
}
@ -205,15 +206,10 @@ fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewMod
verticalAlignment = Alignment.CenterVertically
) {
val imageUri = contact.id?.let { contactsViewModel.getImageUri(it, true) }
val imageRequest = ImageRequest.Builder(context)
.data(imageUri)
.transformations(CircleCropTransformation())
.error(R.drawable.account_circle_96dp)
.placeholder(R.drawable.account_circle_96dp)
.build()
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val loadedImage = loadImage(imageUri, context, errorPlaceholderImage)
AsyncImage(
model = imageRequest,
model = loadedImage,
contentDescription = stringResource(R.string.user_avatar),
modifier = Modifier.size(width = 45.dp, height = 45.dp)
)
@ -246,8 +242,10 @@ fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewMod
fun AppBar(title: String, context: Context, contactsViewModel: ContactsViewModel) {
val searchQuery by contactsViewModel.searchQuery.collectAsState()
val searchState = contactsViewModel.searchState.collectAsState()
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
IconButton(onClick = {
(context as? Activity)?.finish()
@ -282,13 +280,13 @@ fun ConversationCreationOptions(context: Context) {
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
Icon(
painter = painterResource(id = R.drawable.baseline_chat_bubble_outline_24),
modifier = Modifier
.width(40.dp)
.height(40.dp)
.padding(8.dp),
painter = painterResource(R.drawable.baseline_chat_bubble_outline_24),
contentDescription = stringResource(R.string.new_conversation_creation_icon)
contentDescription = null
)
Text(
modifier = Modifier
@ -308,13 +306,13 @@ fun ConversationCreationOptions(context: Context) {
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
Icon(
Icons.AutoMirrored.Filled.List,
modifier = Modifier
.width(40.dp)
.height(40.dp)
.padding(8.dp),
painter = painterResource(R.drawable.baseline_format_list_bulleted_24),
contentDescription = stringResource(R.string.join_open_conversations_icon)
contentDescription = null
)
Text(
modifier = Modifier

Просмотреть файл

@ -0,0 +1,36 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.util.DebugLogger
import com.nextcloud.talk.utils.ContactUtils
class ContactsApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
val imageLoader = ImageLoader.Builder(this)
.memoryCache {
MemoryCache.Builder(this)
.maxSizePercent(ContactUtils.CACHE_MEMORY_SIZE_PERCENTAGE)
.build()
}
.diskCache {
DiskCache.Builder()
.maxSizePercent(ContactUtils.CACHE_DISK_SIZE_PERCENTAGE)
.directory(cacheDir)
.build()
}
.logger(DebugLogger())
.build()
return imageLoader
}
}

Просмотреть файл

@ -13,4 +13,5 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
interface ContactsRepository {
suspend fun getContacts(searchQuery: String?, shareTypes: List<String>): AutocompleteOverall
suspend fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?): RoomOverall
fun getImageUri(avatarId: String, requestBigSize: Boolean): String
}

Просмотреть файл

@ -64,4 +64,12 @@ class ContactsRepositoryImpl(
)
return response
}
override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return ApiUtils.getUrlForAvatar(
_currentUser.baseUrl,
avatarId,
requestBigSize
)
}
}

Просмотреть файл

@ -9,27 +9,21 @@ package com.nextcloud.talk.contacts
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class ContactsViewModel @Inject constructor(
private val repository: ContactsRepository,
private val userManager: UserManager
private val repository: ContactsRepository
) : ViewModel() {
private val _contactsViewState = MutableStateFlow<ContactsUiState>(ContactsUiState.None)
val contactsViewState: StateFlow<ContactsUiState> = _contactsViewState
private val _roomViewState = MutableStateFlow<RoomUiState>(RoomUiState.None)
val roomViewState: StateFlow<RoomUiState> = _roomViewState
private val _currentUser = userManager.currentUser.blockingGet()
val currentUser: User = _currentUser
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery
private val shareTypes: MutableList<String> = mutableListOf(ShareType.User.shareType)
@ -86,13 +80,8 @@ class ContactsViewModel @Inject constructor(
}
}
}
fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return ApiUtils.getUrlForAvatar(
_currentUser.baseUrl,
avatarId,
requestBigSize
)
return repository.getImageUri(avatarId, requestBigSize)
}
}

Просмотреть файл

@ -0,0 +1,24 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@email.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts
import android.content.Context
import androidx.compose.runtime.Composable
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
@Composable
fun loadImage(imageUri: String?, context: Context, errorPlaceholderImage: Int): ImageRequest {
val imageRequest = ImageRequest.Builder(context)
.data(imageUri)
.transformations(CircleCropTransformation())
.error(errorPlaceholderImage)
.placeholder(errorPlaceholderImage)
.build()
return imageRequest
}

Просмотреть файл

@ -7,7 +7,6 @@
package com.nextcloud.talk.contacts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
@ -17,13 +16,10 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@ -34,82 +30,65 @@ import com.nextcloud.talk.R
@Composable
fun DisplaySearch(text: String, onTextChange: (String) -> Unit, contactsViewModel: ContactsViewModel) {
Surface(
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.background(Color.White)
) {
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
modifier = Modifier
.fillMaxWidth(),
value = text,
onValueChange = { onTextChange(it) },
placeholder = {
Text(
text = stringResource(R.string.nc_search),
color = Color.DarkGray
)
},
.height(60.dp),
value = text,
onValueChange = { onTextChange(it) },
placeholder = {
Text(
text = stringResource(R.string.nc_search)
)
},
textStyle = TextStyle(
color = Color.Black,
fontSize = 16.sp
),
singleLine = true,
leadingIcon = {
textStyle = TextStyle(
fontSize = 16.sp
),
singleLine = true,
leadingIcon = {
IconButton(
onClick = {
onTextChange("")
contactsViewModel.updateSearchState(false)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
},
trailingIcon = {
if (text.isNotEmpty()) {
IconButton(
onClick = {
onTextChange("")
contactsViewModel.updateSearchState(false)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(R.string.back_button),
tint = Color.Black
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.close_icon)
)
}
},
}
},
trailingIcon = {
if (text.isNotEmpty()) {
IconButton(
onClick = {
onTextChange("")
}
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.close_icon),
tint = Color.Black
)
}
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
if (text.trim().isNotEmpty()) {
keyboardController?.hide()
} else {
return@KeyboardActions
}
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
if (text.trim().isNotEmpty()) {
keyboardController?.hide()
} else {
return@KeyboardActions
}
}
),
maxLines = 1,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.White,
unfocusedContainerColor = Color.White,
disabledContainerColor = Color.White,
focusedTextColor = Color.Black,
cursorColor = Color.Black
)
)
}
}
),
maxLines = 1
)
}

Просмотреть файл

@ -12,6 +12,8 @@ import android.provider.ContactsContract
object ContactUtils {
const val MAX_CONTACT_LIMIT = 50
const val CACHE_MEMORY_SIZE_PERCENTAGE = 0.1
const val CACHE_DISK_SIZE_PERCENTAGE = 0.02
fun getDisplayNameFromDeviceContact(context: Context, id: String?): String? {
var displayName: String? = null

Просмотреть файл

@ -11,8 +11,8 @@
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L6,16l-2,2L4,4h16v12z"/>
</vector>

Просмотреть файл

@ -0,0 +1,126 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts
import com.nextcloud.talk.contacts.apiService.FakeItem
import com.nextcloud.talk.contacts.repository.FakeRepositoryError
import com.nextcloud.talk.contacts.repository.FakeRepositorySuccess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class ContactsViewModelTest {
private lateinit var viewModel: ContactsViewModel
private val repository: ContactsRepository = FakeRepositorySuccess()
val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Before
fun setUp() {
viewModel = ContactsViewModel(repository)
}
@Test
fun `fetch contacts`() =
runTest {
viewModel = ContactsViewModel(repository)
viewModel.getContactsFromSearchParams()
assert(viewModel.contactsViewState.value is ContactsUiState.Success)
val successState = viewModel.contactsViewState.value as ContactsUiState.Success
assert(successState.contacts == FakeItem.contacts)
}
@Test
fun `test error contacts state`() =
runTest {
viewModel = ContactsViewModel(FakeRepositoryError())
assert(viewModel.contactsViewState.value is ContactsUiState.Error)
val errorState = viewModel.contactsViewState.value as ContactsUiState.Error
assert(errorState.message == "unable to fetch contacts")
}
@Test
fun `update search query`() {
viewModel.updateSearchQuery("Ma")
assert(viewModel.searchQuery.value == "Ma")
}
@Test
fun `initial search query is empty string`() {
viewModel.updateSearchQuery("")
assert(viewModel.searchQuery.value == "")
}
@Test
fun `initial shareType is User`() {
assert(viewModel.shareTypeList.contains(ShareType.User.shareType))
}
@Test
fun `update shareTypes`() {
viewModel.updateShareTypes(ShareType.Group.shareType)
assert(viewModel.shareTypeList.contains(ShareType.Group.shareType))
}
@Test
fun `initial room state is none`() =
runTest {
assert(viewModel.roomViewState.value is RoomUiState.None)
}
@Test
fun `test success room state`() =
runTest {
viewModel.createRoom("1", "users", "s@gmail.com", null)
assert(viewModel.roomViewState.value is RoomUiState.Success)
val successState = viewModel.roomViewState.value as RoomUiState.Success
assert(successState.conversation == FakeItem.roomOverall.ocs!!.data)
}
@Test
fun `test failure room state`() =
runTest {
viewModel = ContactsViewModel(FakeRepositoryError())
viewModel.createRoom("1", "users", "s@gmail.com", null)
assert(viewModel.roomViewState.value is RoomUiState.Error)
val errorState = viewModel.roomViewState.value as RoomUiState.Error
assert(errorState.message == "unable to create room")
}
@Test
fun `test image uri`() {
val expectedImageUri = "https://mydomain.com/index.php/avatar/vidya/512"
val imageUri = viewModel.getImageUri("vidya", false)
assert(imageUri == expectedImageUri)
}
@Test
fun `test error image uri`() {
val expectedImageUri = "https://mydoman.com/index.php/avatar/vidya/512"
val imageUri = viewModel.getImageUri("vidya", false)
assert(imageUri != expectedImageUri)
}
}

Просмотреть файл

@ -0,0 +1,58 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.apiService
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOCS
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOCS
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericMeta
import org.mockito.Mockito.mock
object FakeItem {
val contacts: List<AutocompleteUser> =
listOf(
AutocompleteUser(id = "android", label = "Android", source = "users"),
AutocompleteUser(id = "android1", label = "Android 1", source = "users"),
AutocompleteUser(id = "android2", label = "Android 2", source = "users"),
AutocompleteUser(id = "Benny", label = "Benny J", source = "users"),
AutocompleteUser(id = "Benjamin", label = "Benjamin Schmidt", source = "users"),
AutocompleteUser(id = "Chris", label = "Christoph Schmidt", source = "users"),
AutocompleteUser(id = "Daniel", label = "Daniel H", source = "users"),
AutocompleteUser(id = "Dennis", label = "Dennis Richard", source = "users"),
AutocompleteUser(id = "Emma", label = "Emma Jackson", source = "users"),
AutocompleteUser(id = "Emily", label = "Emily Jackson", source = "users"),
AutocompleteUser(id = "Mario", label = "Mario Schmidt", source = "users"),
AutocompleteUser(id = "Maria", label = "Maria Schmidt", source = "users"),
AutocompleteUser(id = "Samsung", label = "Samsung A52", source = "users"),
AutocompleteUser(id = "Tom", label = "Tom Müller", source = "users"),
AutocompleteUser(id = "Tony", label = "Tony Baker", source = "users")
)
val contactsOverall = AutocompleteOverall(
ocs = AutocompleteOCS(
meta = GenericMeta(
status = "ok",
statusCode = 200,
message = "OK"
),
data = contacts
)
)
val roomOverall: RoomOverall = RoomOverall(
ocs = RoomOCS(
meta = GenericMeta(
status = "ok",
statusCode = 200,
message = "OK"
),
data = mock(Conversation::class.java)
)
)
}

Просмотреть файл

@ -0,0 +1,31 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kota@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.repository
import com.nextcloud.talk.contacts.ContactsRepository
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.conversations.RoomOverall
class FakeRepositoryError : ContactsRepository {
override suspend fun getContacts(searchQuery: String?, shareTypes: List<String>): AutocompleteOverall {
throw Exception("unable to fetch contacts")
}
override suspend fun createRoom(
roomType: String,
sourceType: String,
userId: String,
conversationName: String?
): RoomOverall {
throw Exception("unable to create room")
}
override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return "https://mydoman.com/index.php/avatar/$avatarId/512"
}
}

Просмотреть файл

@ -0,0 +1,32 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@email.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.repository
import com.nextcloud.talk.contacts.ContactsRepository
import com.nextcloud.talk.contacts.apiService.FakeItem
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.conversations.RoomOverall
class FakeRepositorySuccess : ContactsRepository {
override suspend fun getContacts(searchQuery: String?, shareTypes: List<String>): AutocompleteOverall {
return FakeItem.contactsOverall
}
override suspend fun createRoom(
roomType: String,
sourceType: String,
userId: String,
conversationName: String?
): RoomOverall {
return FakeItem.roomOverall
}
override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return "https://mydomain.com/index.php/avatar/$avatarId/512"
}
}

Просмотреть файл

@ -11,12 +11,10 @@
buildscript {
ext {
kotlinVersion = '1.9.23'
hilt_version = '2.44'
kotlinVersion = '2.0.0'
hilt_version = '2.44'
}
repositories {
google()
gradlePluginPortal()

Просмотреть файл

@ -4,13 +4,13 @@
<verify-metadata>true</verify-metadata>
<verify-signatures>true</verify-signatures>
<trusted-artifacts>
<trust file="tensorflow-lite-metadata-0.1.0-rc2.pom" reason="differing hash on every CI run - temp global trust"/>
<trust group="androidx.fragment"/>
<trust group="com.android.tools.build" name="aapt2" version="8.4.1-11315950" reason="ships OS specific artifacts (win/linux) - temp global trust"/>
<trust group="com.github.nextcloud-deps" name="android-talk-webrtc" version="110.5481.0" reason="ships OS specific artifacts (win/linux) - temp global trust"/>
<trust group="com.google.dagger"/>
<trust group="org.javassist" name="javassist" version="3.26.0-GA" reason="java assist"/>
<trust file=".*-sources[.]jar" regex="true"/>
<trust group="com.google.dagger" />
<trust group="org.javassist" name="javassist" version="3.26.0-GA" reason="java assist"/>
<trust group="androidx.fragment"/>
<trust file="tensorflow-lite-metadata-0.1.0-rc2.pom" reason="differing hash on every CI run - temp global trust"/>
</trusted-artifacts>
<ignored-keys>
<ignored-key id="0AA3E5C3D232E79B" reason="Key couldn't be downloaded from any key server"/>
@ -39,7 +39,10 @@
<trusted-key id="0F07D1201BDDAB67CFB84EB479752DB6C966F0B8" group="com.google.android" name="annotations" version="4.1.1.4"/>
<trusted-key id="10F3C7A02ECA55E502BADCF3991EFB94DB91127D" group="org.ow2" name="ow2" version="1.5.1"/>
<trusted-key id="120D6F34E627ED3A772EBBFE55C7E5E701832382" group="org.snakeyaml" name="snakeyaml-engine" version="2.6"/>
<trusted-key id="147B691A19097624902F4EA9689CBE64F4BC997F" group="org.mockito"/>
<trusted-key id="147B691A19097624902F4EA9689CBE64F4BC997F">
<trusting group="org.mockito"/>
<trusting group="org.mockito.kotlin" name="mockito-kotlin" version="4.1.0"/>
</trusted-key>
<trusted-key id="1597AB231B7ADD7E14B1D9C43F00DB67AE236E2E" group="org.conscrypt" name="conscrypt-android" version="2.5.2"/>
<trusted-key id="190D5A957FF22273E601F7A7C92C5FEC70161C62" group="org.apache" name="apache" version="18"/>
<trusted-key id="19BEAB2D799C020F17C69126B16698A4ADF4D638" group="org.checkerframework"/>
@ -156,6 +159,7 @@
<trusted-key id="8756C4F765C9AC3CB6B85D62379CE192D401AB61">
<trusting group="eu.davidea"/>
<trusting group="org.jetbrains.intellij.deps"/>
<trusting group="org.jetbrains.kotlinx"/>
</trusted-key>
<trusted-key id="8E3A02905A1AE67E7B0F9ACD3967D4EDA591B991" group="org.jetbrains.kotlinx" name="kotlinx-html-jvm" version="0.8.1"/>
<trusted-key id="8F9A3C6D105B9F57844A721D79E193516BE7998F" group="org.dom4j" name="dom4j" version="2.1.4"/>
@ -232,6 +236,7 @@
<trusted-key id="E4AC7874F3479A0F1F8ECF9960BB45F36B649F22" group="fr.dudie" name="nominatim-api" version="3.4"/>
<trusted-key id="E77417AC194160A3FABD04969A259C7EE636C5ED" group="^com[.]google($|([.].*))" regex="true"/>
<trusted-key id="E7DC75FC24FB3C8DFE8086AD3D5839A2262CBBFB" group="org.jetbrains.kotlinx"/>
<trusted-key id="64B9B09F164AA0BF88742EB61188B69F6D6259CA" group="com.google.accompanist"/>
<trusted-key id="E82D2EAF2E83830CE1F7F6BE571A5291E827E1C7" group="net.java" name="jvnet-parent" version="3"/>
<trusted-key id="E85AED155021AF8A6C6B7A4A7C7D8456294423BA" group="org.objenesis"/>
<trusted-key id="EAA526B91DD83BA3E1B9636FA730529CA355A63E" group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2.1"/>
@ -294,6 +299,14 @@
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.7.0">
<artifact name="activity-compose-1.7.0.aar">
<sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="activity-compose-1.7.0.module">
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.0.0">
<artifact name="annotation-1.0.0.jar">
<sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -326,6 +339,50 @@
<sha256 value="9516c2ae44284ea0bd3d0eade0ee638879b708cbe31e3af92ba96c300604ebc3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.exifinterface" name="exifinterface" version="1.3.6">
<artifact name="exifinterface-1.3.6.aar">
<sha256 value="1804105e9e05fdd8f760413bad5de498c381aa329f4f9d94c851bc891ac654c6" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="exifinterface-1.3.6.module">
<sha256 value="5e9fd84ca3fd3b7706f6856fa4383107de8676bf7c42b7d4b8108949414d6201" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.1.0">
<artifact name="core-1.1.0.pom">
<sha256 value="dae46132cdcd46b798425f7cb78fd65890869b6d26101ccdcd43461a4f51754c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.3.2">
<artifact name="core-1.3.2.pom">
<sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.test.ext" name="junit" version="1.1.5">
<artifact name="junit-1.1.5.aar">
<sha256 value="4307c0e60f5d701db9c59bcd9115af705113c36a9132fa3dbad58db1294e9bfd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="junit-1.1.5.pom">
<sha256 value="4cff0df04cae25831e821ef2f9129245783460e98d0fd67d8f6824065a134c4e" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.8.0">
<artifact name="core-ktx-1.8.0.module">
<sha256 value="a91bc3e02f209f643dd8275345a9e3003ce20d64fc0760eccf479c1709842f72" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.3.0">
<artifact name="annotation-experimental-1.3.0.aar">
<sha256 value="abfd29c8556e5bd0325a9f769ab9e9d154ff4a5515c476cdd5a2a8285b1b19dc" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="annotation-experimental-1.3.0.module">
<sha256 value="5eebeaff01d042e06dcf292abf8964ad391e4b0159f0090f16253d6045d38da0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
<artifact name="annotation-experimental-1.1.0-rc01.module">
<sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.5.0">
<artifact name="annotation-1.5.0.jar">
<sha256 value="261fb7c0210858500bab66d34354972a75166ab4182add283780b05513d6ec4a" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -342,6 +399,14 @@
<sha256 value="fbc64f5c44a7added8b6eab517cf7d70555e25153bf5d44a6ed9b0e5312f7de9" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.exifinterface" name="exifinterface" version="1.3.2">
<artifact name="exifinterface-1.3.2.aar">
<sha256 value="8770c180103e0b8c04a07eb4c59153af639b09eca25deae9bdcdaf869d1e5b6b" origin="Generated by Gradle"/>
</artifact>
<artifact name="exifinterface-1.3.2.module">
<sha256 value="10ba5b5cbea7f5c8758be4fdaec60a3545e891a1130d830a442b88cf5336a885" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.0.0">
<artifact name="annotation-experimental-1.0.0.pom">
<sha256 value="6b73ff6608f4b1d6cbab620b65708a382d0b39901cf4e6b0d16f84a1b04d7732" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -355,6 +420,11 @@
<sha256 value="0361d1526a4d7501255e19779e09e93cdbd07fee0e2f5c50b7a137432d510119" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
<artifact name="annotation-experimental-1.1.0-rc01.module">
<sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
<artifact name="annotation-experimental-1.1.0-rc01.module">
<sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -376,6 +446,76 @@
<sha256 value="9b6974a7dfe26d3c209dd63e16f8ee2461b57a091789160ca1eb492bb1bf3f84" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.3.0">
<artifact name="annotation-experimental-1.3.0.aar">
<sha256 value="abfd29c8556e5bd0325a9f769ab9e9d154ff4a5515c476cdd5a2a8285b1b19dc" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="annotation-experimental-1.3.0.module">
<sha256 value="5eebeaff01d042e06dcf292abf8964ad391e4b0159f0090f16253d6045d38da0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.7.0">
<artifact name="activity-compose-1.7.0.aar">
<sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="activity-compose-1.7.0.module">
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
<artifact name="runtime-saveable-1.0.1.module">
<sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.0.1">
<artifact name="ui-1.0.1.aar">
<sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-1.0.1.module">
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose" name="compose-bom" version="2024.06.00">
<artifact name="compose-bom-2024.06.00.pom">
<sha256 value="1b391a969ff81c0bb43b3711e92d977e8bfa72457a11d8a37910a7051bdc3045" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.7.0">
<artifact name="activity-compose-1.7.0.aar">
<sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="activity-compose-1.7.0.module">
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
<artifact name="runtime-saveable-1.0.1.module">
<sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.0.1">
<artifact name="ui-1.0.1.aar">
<sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-1.0.1.module">
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose" name="compose-bom" version="2024.06.00">
<artifact name="compose-bom-2024.06.00.pom">
<sha256 value="1b391a969ff81c0bb43b3711e92d977e8bfa72457a11d8a37910a7051bdc3045" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.1.0">
<artifact name="appcompat-1.1.0.pom">
<sha256 value="340d617121f8ef8e02a6680c8f357aa3e542276d0c8a1cdcb6fd98984b2cb7b9" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -4902,6 +5042,14 @@
<sha256 value="9a35e48f20f3021c21e469d52fa88b3acc08b20dab77b6c646f72f6fb205ec92" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
<component group="com.novoda" name="merlin" version="1.2.1">
<artifact name="merlin-1.2.1.aar">
<sha256 value="c62d03d0fde57f26fa633feeee24d7dfed3d66cc81097e4d6306b076cc7d70b6" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="merlin-1.2.1.pom">
<sha256 value="62dbaffb68b60ca317c05bba83a9fea8d866c3d3e7a2bd928c69591aa2fe4418" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.squareup" name="javapoet" version="1.2.0">
<artifact name="javapoet-1.2.0.pom">
<pgp value="9E84765A7AA3E3D3D5598A408E3F0DE7AE354651"/>
@ -5860,7 +6008,7 @@
<ignored-keys>
<ignored-key id="DB0597E3144342256BC81E3EC727D053C4481CF5" reason="PGP verification failed"/>
</ignored-keys>
<sha256 value="8359ad51e0476c8e0df7188a43f16d49733c4a428fb45e99794b783f01b97520" origin="Generated by Gradle" reason="PGP signature verification failed!"/>
<sha256 value="9a4f5e5674366c156c90391662f03ed7c5971d6aa63832df74a271da6ff82e96" origin="Generated by Gradle" reason="PGP signature verification failed!"/>
</artifact>
</component>
<component group="org.xerial" name="sqlite-jdbc" version="3.41.2.2">