Merge branch 'develop' into feature/fga/live_location_sharing_setup
This commit is contained in:
commit
e8c2790595
131 changed files with 825 additions and 407 deletions
26
.github/renovate.json → .github/renovate.json5
vendored
26
.github/renovate.json → .github/renovate.json5
vendored
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
"config:recommended",
|
||||
],
|
||||
"labels": [
|
||||
"PR-Dependencies"
|
||||
"PR-Dependencies",
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"string:app_name",
|
||||
"gradle"
|
||||
"gradle",
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
|
|
@ -16,15 +16,25 @@
|
|||
"matchPackageNames": [
|
||||
"/^org.jetbrains.kotlin/",
|
||||
"/^com.google.devtools.ksp/",
|
||||
"/^androidx.compose.compiler/"
|
||||
]
|
||||
"/^androidx.compose.compiler/",
|
||||
],
|
||||
},
|
||||
{
|
||||
"versioning": "semver",
|
||||
"matchPackageNames": [
|
||||
"/^org.maplibre/",
|
||||
"/^org.jetbrains.kotlinx:kotlinx-datetime/"
|
||||
"/^org.jetbrains.kotlinx:kotlinx-datetime/",
|
||||
],
|
||||
},
|
||||
{
|
||||
// Limit PostHog Android upgrade to one PR per month, the first day of the month
|
||||
"matchPackageNames": [
|
||||
"com.posthog:posthog-android",
|
||||
],
|
||||
"schedule": [
|
||||
"* * 1 * *",
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
4
.github/workflows/maestro-local.yml
vendored
4
.github/workflows/maestro-local.yml
vendored
|
|
@ -86,7 +86,7 @@ jobs:
|
|||
ref: ${{ github.ref }}
|
||||
persist-credentials: false
|
||||
- name: Download APK artifact from previous job
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: elementx-apk-maestro
|
||||
- name: Enable KVM group perms
|
||||
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
run: curl -fsSL "https://get.maestro.mobile.dev" | bash
|
||||
- name: Run Maestro tests in emulator
|
||||
id: maestro_test
|
||||
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # v2.35.0
|
||||
uses: reactivecircus/android-emulator-runner@5d6e86df22ab11632167a1a6b0c9ab0dc3469586 # v2.36.0
|
||||
continue-on-error: true
|
||||
env:
|
||||
MAESTRO_USERNAME: maestroelement
|
||||
|
|
|
|||
22
.github/workflows/quality.yml
vendored
22
.github/workflows/quality.yml
vendored
|
|
@ -287,12 +287,12 @@ jobs:
|
|||
path: |
|
||||
**/build/reports/**/*.*
|
||||
|
||||
knit:
|
||||
name: Knit checks
|
||||
docs:
|
||||
name: Doc checks
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
|
@ -309,17 +309,9 @@ jobs:
|
|||
- name: Clone submodules
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- name: Use JDK 21
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run Knit
|
||||
run: ./gradlew knitCheck $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Run docs check
|
||||
# This is equivalent to `./gradlew checkDocs`, but we avoid having to install java and gradle
|
||||
run: python3 ./tools/docs/generate_toc.py --verify ./*.md docs/**/*.md
|
||||
|
||||
# Note: to auto fix issues you can use the following command:
|
||||
# shellcheck -f diff <files> | git apply
|
||||
|
|
@ -359,7 +351,7 @@ jobs:
|
|||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
persist-credentials: false
|
||||
- name: Download reports from previous jobs
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- name: Prepare Danger
|
||||
if: always()
|
||||
run: |
|
||||
|
|
|
|||
33
CHANGES.md
33
CHANGES.md
|
|
@ -1,3 +1,36 @@
|
|||
Changes in Element X v26.03.3
|
||||
=============================
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v26.03.3 -->
|
||||
|
||||
## What's Changed
|
||||
### ✨ Features
|
||||
* Support for Voice Call only (no video), parity with web by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5995
|
||||
### 🐛 Bugfixes
|
||||
* Fix read receipts not appearing in threaded timelines by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6297
|
||||
* Try fixing index OOB issues in `Editable.checkSuggestionNeeded` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6303
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6302
|
||||
### 🧱 Build
|
||||
* Add zizmorcore/zizmor-action by @bmarty in https://github.com/element-hq/element-x-android/pull/6286
|
||||
* Add use existing branch confirmation and progress for file download by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6294
|
||||
* Replace `knit` with `generate_toc.py` script by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6279
|
||||
### Dependency upgrades
|
||||
* Update plugin sonarqube to v7.2.3.7755 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6283
|
||||
* Update dependency io.sentry:sentry-android to v8.34.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6289
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.03.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6292
|
||||
* Update dependency com.posthog:posthog-android to v3.35.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6293
|
||||
* Update zizmorcore/zizmor-action action to v0.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6299
|
||||
* fix(deps): update dependency org.maplibre.gl:android-sdk to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6277
|
||||
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.09 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6307
|
||||
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6310
|
||||
### Others
|
||||
* Add code to help debugging the saved nav state graph by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6295
|
||||
* Add network constraints for fetching notifications with `WorkManager` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6305
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.03.2...v26.03.3
|
||||
|
||||
Changes in Element X v26.03.2
|
||||
=============================
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* [Code quality](#code-quality)
|
||||
* [detekt](#detekt)
|
||||
* [ktlint](#ktlint)
|
||||
* [knit](#knit)
|
||||
* [checkDocs](#checkdocs)
|
||||
* [lint](#lint)
|
||||
* [Unit tests](#unit-tests)
|
||||
* [konsist](#konsist)
|
||||
|
|
@ -123,13 +123,13 @@ Note that you can run
|
|||
|
||||
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course)
|
||||
|
||||
#### knit
|
||||
#### checkDocs
|
||||
|
||||
[knit](https://github.com/Kotlin/kotlinx-knit) is a tool which checks markdown files on the project. Also it generates/updates the table of content (toc) of the markdown files.
|
||||
`checkDocs` is a Gradle task which checks markdown files on the project to ensure their table of contents is up to date. It uses `tools/docs/generate_toc.py --verify` under the hood, and has a counterpart `generateDocsToc` task which runs `tools/docs/generate_toc.py` to update the table of contents of markdown files.
|
||||
|
||||
So everytime the toc should be updated, just run
|
||||
<pre>
|
||||
./gradlew knit
|
||||
./gradlew generateDocsToc
|
||||
</pre>
|
||||
|
||||
and commit the changes.
|
||||
|
|
@ -137,7 +137,7 @@ and commit the changes.
|
|||
The CI will check that markdown files are up to date by running
|
||||
|
||||
<pre>
|
||||
./gradlew knitCheck
|
||||
./gradlew checkDocs
|
||||
</pre>
|
||||
|
||||
#### lint
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ plugins {
|
|||
alias(libs.plugins.kotlin.android)
|
||||
// When using precompiled plugins, we need to apply the firebase plugin like this
|
||||
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
||||
alias(libs.plugins.knit)
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
|
|
@ -250,26 +249,6 @@ androidComponents {
|
|||
configureLicensesTasks(reportingExtension)
|
||||
}
|
||||
|
||||
// Knit
|
||||
apply {
|
||||
plugin("kotlinx-knit")
|
||||
}
|
||||
|
||||
knit {
|
||||
files = fileTree(project.rootDir) {
|
||||
include(
|
||||
"**/*.md",
|
||||
"**/*.kt",
|
||||
"*/*.kts",
|
||||
)
|
||||
exclude(
|
||||
"**/build/**",
|
||||
"*/.gradle/**",
|
||||
"**/CHANGES.md",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -175,12 +175,23 @@ tasks.register("runQualityChecks") {
|
|||
tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
|
||||
// tasks.findByName("buildHealth")?.let { dependsOn(it) }
|
||||
}
|
||||
dependsOn(":app:knitCheck")
|
||||
|
||||
dependsOn("checkDocs")
|
||||
// Make sure all checks run even if some fail
|
||||
gradle.startParameter.isContinueOnFailure = true
|
||||
}
|
||||
|
||||
// Register Markdown documentation check task.
|
||||
tasks.register("checkDocs", Exec::class.java) {
|
||||
inputs.files("./*.md", "docs/**/*.md")
|
||||
commandLine("python3", "tools/docs/generate_toc.py", "--verify", *inputs.files.map { it.path }.toTypedArray())
|
||||
}
|
||||
|
||||
// Register Markdown documentation TOC generation task.
|
||||
tasks.register("generateDocsToc", Exec::class.java) {
|
||||
inputs.files("./*.md", "docs/**/*.md")
|
||||
commandLine("python3", "tools/docs/generate_toc.py", *inputs.files.map { it.path }.toTypedArray())
|
||||
}
|
||||
|
||||
// Make sure to delete old screenshots before recording new ones
|
||||
subprojects {
|
||||
val snapshotsDir = File("${project.projectDir}/src/test/snapshots")
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ This document explains how to install Element X Android from a Github Release.
|
|||
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone)
|
||||
* [Installing from the App Bundle](#installing-from-the-app-bundle)
|
||||
* [Requirements](#requirements)
|
||||
* [Steps](#steps)
|
||||
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone)
|
||||
* [Steps](#steps-1)
|
||||
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone-1)
|
||||
|
||||
<!--- END -->
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
<!--- TOC -->
|
||||
|
||||
* [Installing from GitHub](#installing-from-github)
|
||||
* [Create a GitHub token](#create-a-github-token)
|
||||
* [Provide artifact URL](#provide-artifact-url)
|
||||
* [Next steps](#next-steps)
|
||||
* [Future improvement](#future-improvement)
|
||||
* [Installing from GitHub](#installing-from-github)
|
||||
* [Create a GitHub token](#create-a-github-token)
|
||||
* [Provide artifact URL](#provide-artifact-url)
|
||||
* [Next steps](#next-steps)
|
||||
* [Future improvement](#future-improvement)
|
||||
|
||||
<!--- END -->
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* [Stop Synapse](#stop-synapse)
|
||||
* [Troubleshoot](#troubleshoot)
|
||||
* [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver)
|
||||
* [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost8080")
|
||||
* [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-unable-to-contact-localhost8080)
|
||||
* [virtualenv command fails](#virtualenv-command-fails)
|
||||
|
||||
<!--- END -->
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ This document aims to describe how Element android displays notifications to the
|
|||
<!--- TOC -->
|
||||
|
||||
* [Prerequisites Knowledge](#prerequisites-knowledge)
|
||||
* [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver?)
|
||||
* [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver)
|
||||
* [How does a mobile app receives push notification](#how-does-a-mobile-app-receives-push-notification)
|
||||
* [Push VS Notification](#push-vs-notification)
|
||||
* [Push in the matrix federated world](#push-in-the-matrix-federated-world)
|
||||
* [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client?)
|
||||
* [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client)
|
||||
* [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
|
||||
* [Background processing limitations](#background-processing-limitations)
|
||||
* [Element Notification implementations](#element-notification-implementations)
|
||||
|
|
|
|||
|
|
@ -3,23 +3,23 @@
|
|||
<!--- TOC -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Who should read this document?](#who-should-read-this-document?)
|
||||
* [Who should read this document?](#who-should-read-this-document)
|
||||
* [Submitting PR](#submitting-pr)
|
||||
* [Who can submit pull requests?](#who-can-submit-pull-requests?)
|
||||
* [Who can submit pull requests?](#who-can-submit-pull-requests)
|
||||
* [Humans](#humans)
|
||||
* [Draft PR?](#draft-pr?)
|
||||
* [Draft PR?](#draft-pr)
|
||||
* [Base branch](#base-branch)
|
||||
* [PR Review Assignment](#pr-review-assignment)
|
||||
* [PR review time](#pr-review-time)
|
||||
* [Re-request PR review](#re-request-pr-review)
|
||||
* [When create split PR?](#when-create-split-pr?)
|
||||
* [When create split PR?](#when-create-split-pr)
|
||||
* [Avoid fixing other unrelated issue in a big PR](#avoid-fixing-other-unrelated-issue-in-a-big-pr)
|
||||
* [Bots](#bots)
|
||||
* [Renovate](#renovate)
|
||||
* [Gradle wrapper](#gradle-wrapper)
|
||||
* [Sync analytics plan](#sync-analytics-plan)
|
||||
* [Reviewing PR](#reviewing-pr)
|
||||
* [Who can review pull requests?](#who-can-review-pull-requests?)
|
||||
* [Who can review pull requests?](#who-can-review-pull-requests)
|
||||
* [What to have in mind when reviewing a PR](#what-to-have-in-mind-when-reviewing-a-pr)
|
||||
* [Rules](#rules)
|
||||
* [Check the form](#check-the-form)
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
* [Check the commit](#check-the-commit)
|
||||
* [Check the substance](#check-the-substance)
|
||||
* [Make a dedicated meeting to review the PR](#make-a-dedicated-meeting-to-review-the-pr)
|
||||
* [What happen to the issue(s)?](#what-happen-to-the-issues?)
|
||||
* [What happen to the issue(s)?](#what-happen-to-the-issues)
|
||||
* [Merge conflict](#merge-conflict)
|
||||
* [When and who can merge PR](#when-and-who-can-merge-pr)
|
||||
* [Merge type](#merge-type)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 1fd0d297d944186e3af2773e1c5db2938d60f74b
|
||||
Subproject commit cdde60c158ecd0987a3ba6fd79a4617551aff463
|
||||
2
fastlane/metadata/android/en-US/changelogs/202603030.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202603030.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: bug fixes and improvements.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -24,5 +24,11 @@ interface NetworkMonitor {
|
|||
/**
|
||||
* Checks if the active network is being blocked by Doze, even if it's available.
|
||||
*/
|
||||
fun isNetworkBlocked(): Boolean
|
||||
val isNetworkBlocked: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* A flow indicating whether the app is running in an air-gapped environment.
|
||||
* An air-gapped environment is an environment that is not connected to the internet, and where the app can only communicate with a limited set of servers.
|
||||
*/
|
||||
val isInAirGappedEnvironment: StateFlow<Boolean>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
|
|
@ -23,4 +24,8 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
api(projects.features.networkmonitor.api)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,21 @@ package io.element.android.features.networkmonitor.impl
|
|||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
|
@ -39,13 +42,13 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||
@SingleIn(AppScope::class)
|
||||
class DefaultNetworkMonitor(
|
||||
@ApplicationContext context: Context,
|
||||
@AppCoroutineScope
|
||||
appCoroutineScope: CoroutineScope,
|
||||
@AppCoroutineScope appCoroutineScope: CoroutineScope,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : NetworkMonitor {
|
||||
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
|
||||
private val blockedNetworkBlockedChecker = NetworkBlockedChecker(connectivityManager)
|
||||
|
||||
override fun isNetworkBlocked(): Boolean = blockedNetworkBlockedChecker.isNetworkBlocked()
|
||||
override val isNetworkBlocked = MutableStateFlow(NetworkBlockedChecker(connectivityManager).isNetworkBlocked())
|
||||
override val isInAirGappedEnvironment = MutableStateFlow(false)
|
||||
|
||||
override val connectivity: StateFlow<NetworkStatus> = callbackFlow {
|
||||
|
||||
|
|
@ -63,6 +66,27 @@ class DefaultNetworkMonitor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
|
||||
Timber.d("Network ${network.networkHandle} blocked status changed: $blocked.")
|
||||
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
|
||||
// If the network is blocked, it means that Doze is preventing the app from using the network, even if it's available.
|
||||
isNetworkBlocked.value = blocked
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
if (!buildMeta.isEnterpriseBuild) {
|
||||
// The air-gapped environment detection is only relevant for the enterprise build.
|
||||
return
|
||||
}
|
||||
|
||||
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
|
||||
// If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet
|
||||
// (according to Google), which is a common case in air-gapped environments.
|
||||
isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
if (activeNetworksCount.incrementAndGet() > 0) {
|
||||
trySendBlocking(NetworkStatus.Connected)
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import android.net.ConnectivityManager
|
|||
import android.net.NetworkInfo
|
||||
|
||||
/**
|
||||
* Helper to check if the active network in [ConnectivityManager] is blocked.
|
||||
* Helper to synchronously check if the active network in [ConnectivityManager] is blocked.
|
||||
*
|
||||
* This is extracted to its own class because it uses deprecated APIs (but the only ones that are reliable)
|
||||
* and we don't want to suppress deprecations everywhere.
|
||||
* and we don't want to suppress deprecations everywhere in the file this would be called.
|
||||
*/
|
||||
class NetworkBlockedChecker(
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
|
||||
class FakeNetworkMonitor(
|
||||
initialStatus: NetworkStatus = NetworkStatus.Connected,
|
||||
private val isNetworkBlockedLambda: () -> Boolean = { false },
|
||||
) : NetworkMonitor {
|
||||
override val connectivity = MutableStateFlow(initialStatus)
|
||||
override fun isNetworkBlocked(): Boolean = isNetworkBlockedLambda()
|
||||
override val isNetworkBlocked = MutableStateFlow(false)
|
||||
override val isInAirGappedEnvironment = MutableStateFlow(false)
|
||||
|
||||
fun givenNetworkBlocked(isBlocked: Boolean) {
|
||||
isNetworkBlocked.value = isBlocked
|
||||
}
|
||||
|
||||
fun givenIsInAirGappedEnvironment(isInAirGapped: Boolean) {
|
||||
isInAirGappedEnvironment.value = isInAirGapped
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
|||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||
# only fix API breaks and may add some TODOs.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.6"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.11"
|
||||
|
||||
# Others
|
||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
|
|
@ -220,7 +220,7 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref =
|
|||
color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||
|
||||
# Analytics
|
||||
posthog = "com.posthog:posthog-android:3.35.0"
|
||||
posthog = "com.posthog:posthog-android:3.37.0"
|
||||
sentry = "io.sentry:sentry-android:8.34.1"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
||||
|
|
@ -267,7 +267,6 @@ paparazzi = "app.cash.paparazzi:2.0.0-alpha04"
|
|||
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
|
||||
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
||||
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
|
||||
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.1" }
|
||||
sonarqube = "org.sonarqube:7.2.3.7755"
|
||||
licensee = "app.cash.licensee:1.14.1"
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
|
|
|||
|
|
@ -154,4 +154,12 @@ enum class FeatureFlags(
|
|||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
ValidateNetworkWhenSchedulingNotificationFetching(
|
||||
key = "feature.validate_network_when_scheduling_notification_fetching",
|
||||
title = "validate internet connectivity when scheduling notification fetching",
|
||||
description = "Only fetch events for push notifications when the device has internet connectivity. " +
|
||||
"Enabling this can be problematic in air-gapped environments.",
|
||||
defaultValue = { true },
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ class RustEncryptionService(
|
|||
|
||||
override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
service.recover(recoveryKey)
|
||||
service.recoverAndFixBackup(recoveryKey)
|
||||
}.recoverCatching {
|
||||
when (it) {
|
||||
// We ignore import errors because the user will be notified about them via the "Key storage out of sync" detection.
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ class JoinedRustRoom(
|
|||
|
||||
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
|
||||
innerRoom.reportContent(eventId = eventId.value, reason = reason)
|
||||
if (blockUserId != null) {
|
||||
innerRoom.ignoreUser(blockUserId.value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
|||
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.flow.first
|
||||
import timber.log.Timber
|
||||
|
|
@ -49,7 +48,7 @@ class DefaultPushHandler(
|
|||
private val analyticsService: AnalyticsService,
|
||||
private val systemClock: SystemClock,
|
||||
private val workManagerScheduler: WorkManagerScheduler,
|
||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||
private val syncPendingNotificationsRequestFactory: SyncPendingNotificationsRequestBuilder.Factory,
|
||||
resultProcessor: NotificationResultProcessor,
|
||||
) : PushHandler {
|
||||
init {
|
||||
|
|
@ -134,12 +133,7 @@ class DefaultPushHandler(
|
|||
|
||||
if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) {
|
||||
Timber.d("No pending worker for push notifications found")
|
||||
workManagerScheduler.submit(
|
||||
SyncPendingNotificationsRequestBuilder(
|
||||
sessionId = userId,
|
||||
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
|
||||
)
|
||||
)
|
||||
workManagerScheduler.submit(syncPendingNotificationsRequestFactory.create(userId))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
|
|
|
|||
|
|
@ -160,7 +160,8 @@ class FetchPendingNotificationsWorker(
|
|||
networkTimeoutSpans.finish()
|
||||
|
||||
// If there is a problem with the updated network values, report it and retry if needed
|
||||
if (reportConnectivityError(requests = requests, hasNetwork = hasNetwork, isNetworkBlocked = networkMonitor.isNetworkBlocked())) {
|
||||
val isNetworkBlocked = networkMonitor.isNetworkBlocked.first()
|
||||
if (reportConnectivityError(requests = requests, hasNetwork = hasNetwork, isNetworkBlocked = isNetworkBlocked)) {
|
||||
pushHistoryService.insertOrUpdatePushRequests(requests.map { request ->
|
||||
request.copy(retries = request.retries + 1)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,32 +8,87 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.workmanager
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.workDataOf
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder.Companion.SESSION_ID
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerWorkerType
|
||||
import io.element.android.libraries.workmanager.api.workManagerTag
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
import timber.log.Timber
|
||||
|
||||
interface SyncPendingNotificationsRequestBuilder : WorkManagerRequestBuilder {
|
||||
fun interface Factory {
|
||||
fun create(sessionId: SessionId): SyncPendingNotificationsRequestBuilder
|
||||
}
|
||||
|
||||
class SyncPendingNotificationsRequestBuilder(
|
||||
private val sessionId: SessionId,
|
||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||
) : WorkManagerRequestBuilder {
|
||||
companion object {
|
||||
const val SESSION_ID = "session_id"
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedInject
|
||||
class DefaultSyncPendingNotificationsRequestBuilder(
|
||||
@Assisted private val sessionId: SessionId,
|
||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : SyncPendingNotificationsRequestBuilder {
|
||||
@AssistedFactory
|
||||
@ContributesBinding(AppScope::class)
|
||||
interface Factory : SyncPendingNotificationsRequestBuilder.Factory {
|
||||
override fun create(sessionId: SessionId): DefaultSyncPendingNotificationsRequestBuilder
|
||||
}
|
||||
|
||||
override suspend fun build(): Result<List<WorkManagerRequestWrapper>> {
|
||||
val type = WorkManagerWorkerType.Unique(
|
||||
name = workManagerTag(sessionId = sessionId, requestType = WorkManagerRequestType.NOTIFICATION_SYNC),
|
||||
policy = ExistingWorkPolicy.APPEND_OR_REPLACE,
|
||||
)
|
||||
|
||||
val networkRequestBuilder = NetworkRequest.Builder()
|
||||
// Allow any kind of network that can have internet connectivity.
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
// By default, the network request will require the device to not be in VPN, but since some customers use a VPN to connect to their homeserver,
|
||||
// we need to allow VPN networks.
|
||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
|
||||
// If we're in an air-gapped environment, we shouldn't validate internet connectivity, as the checker will fail and the worker won't run at all.
|
||||
// Note this will always be false for FOSS, since the feature is only enabled in Element Pro.
|
||||
if (networkMonitor.isInAirGappedEnvironment.first()) {
|
||||
Timber.d("In an air-gapped environment, not adding NET_CAPABILITY_VALIDATED to the network request")
|
||||
networkRequestBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
} else if (featureFlagService.isFeatureEnabled(FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching)) {
|
||||
Timber.d("Not in an air-gapped environment, adding NET_CAPABILITY_VALIDATED to the network request")
|
||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
|
||||
val networkConstraints = Constraints.Builder()
|
||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), NetworkType.NOT_REQUIRED)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<FetchPendingNotificationsWorker>()
|
||||
.setInputData(workDataOf(SESSION_ID to sessionId.value))
|
||||
.apply {
|
||||
|
|
@ -44,8 +99,10 @@ class SyncPendingNotificationsRequestBuilder(
|
|||
setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
}
|
||||
}
|
||||
.setConstraints(networkConstraints)
|
||||
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||
.build()
|
||||
|
||||
return Result.success(listOf(WorkManagerRequestWrapper(request, type)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import io.element.android.libraries.push.impl.history.PushHistoryService
|
|||
import io.element.android.libraries.push.impl.notifications.FakeNotificationResultProcessor
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||
import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder
|
||||
import io.element.android.libraries.push.test.workmanager.FakeSyncPendingNotificationsRequestBuilder
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
|
||||
|
|
@ -34,7 +36,6 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa
|
|||
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
|
||||
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -216,7 +217,6 @@ class DefaultPushHandlerTest {
|
|||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
systemClock: FakeSystemClock = FakeSystemClock(),
|
||||
buildVersionSdkIntProvider: FakeBuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
|
||||
resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(
|
||||
emit = { Result.success(Unit) },
|
||||
start = {},
|
||||
|
|
@ -238,8 +238,10 @@ class DefaultPushHandlerTest {
|
|||
analyticsService = analyticsService,
|
||||
systemClock = systemClock,
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
|
||||
resultProcessor = resultProcessor,
|
||||
syncPendingNotificationsRequestFactory = SyncPendingNotificationsRequestBuilder.Factory {
|
||||
FakeSyncPendingNotificationsRequestBuilder()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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.push.impl.workmanager
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.hasKeyWithValueOfType
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerWorkerType
|
||||
import io.element.android.libraries.workmanager.api.workManagerTag
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DefaultSyncPendingNotificationsRequestBuilderTest {
|
||||
@Test
|
||||
fun `build - success API 33`() = runTest {
|
||||
val request = createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = A_SESSION_ID,
|
||||
sdkVersion = 33,
|
||||
)
|
||||
|
||||
val results = request.build()
|
||||
assertThat(results.isSuccess).isTrue()
|
||||
results.getOrNull()!!.first().let { result ->
|
||||
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
|
||||
result.request.run {
|
||||
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
|
||||
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
|
||||
assertThat(workSpec.hasConstraints()).isTrue()
|
||||
// True in API 33+
|
||||
assertThat(workSpec.expedited).isTrue()
|
||||
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build - success API 32 and lower`() = runTest {
|
||||
val request = createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = A_SESSION_ID,
|
||||
sdkVersion = 32,
|
||||
)
|
||||
|
||||
val results = request.build()
|
||||
assertThat(results.isSuccess).isTrue()
|
||||
|
||||
results.getOrNull()!!.first().let { result ->
|
||||
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
|
||||
result.request.run {
|
||||
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
|
||||
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
|
||||
assertThat(workSpec.hasConstraints()).isTrue()
|
||||
// False before API 33
|
||||
assertThat(workSpec.expedited).isFalse()
|
||||
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build - has NET_CAPABILITY_VALIDATED constraint if not in air-gapped env`() = runTest {
|
||||
val request = createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = A_SESSION_ID,
|
||||
sdkVersion = 33,
|
||||
isInAirGapEnvironment = false,
|
||||
)
|
||||
|
||||
val results = request.build()
|
||||
assertThat(results.isSuccess).isTrue()
|
||||
results.getOrNull()!!.first().let { result ->
|
||||
result.request.run {
|
||||
assertThat(workSpec.hasConstraints()).isTrue()
|
||||
val networkRequest = workSpec.constraints.requiredNetworkRequest
|
||||
assertThat(networkRequest).isNotNull()
|
||||
assertThat(networkRequest!!.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build - does not have NET_CAPABILITY_VALIDATED constraint if in air-gapped env`() = runTest {
|
||||
val request = createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = A_SESSION_ID,
|
||||
sdkVersion = 33,
|
||||
isInAirGapEnvironment = true,
|
||||
)
|
||||
|
||||
val results = request.build()
|
||||
assertThat(results.isSuccess).isTrue()
|
||||
results.getOrNull()!!.first().let { result ->
|
||||
result.request.run {
|
||||
assertThat(workSpec.hasConstraints()).isTrue()
|
||||
val networkRequest = workSpec.constraints.requiredNetworkRequest
|
||||
assertThat(networkRequest).isNotNull()
|
||||
assertThat(networkRequest!!.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build - does not have NET_CAPABILITY_VALIDATED constraint if feature flag is disabled`() = runTest {
|
||||
val request = createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = A_SESSION_ID,
|
||||
sdkVersion = 33,
|
||||
isInAirGapEnvironment = false,
|
||||
featureFlagService = FakeFeatureFlagService(initialState = mapOf(
|
||||
FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching.key to false
|
||||
)),
|
||||
)
|
||||
|
||||
val results = request.build()
|
||||
assertThat(results.isSuccess).isTrue()
|
||||
results.getOrNull()!!.first().let { result ->
|
||||
result.request.run {
|
||||
assertThat(workSpec.hasConstraints()).isTrue()
|
||||
val networkRequest = workSpec.constraints.requiredNetworkRequest
|
||||
assertThat(networkRequest).isNotNull()
|
||||
assertThat(networkRequest!!.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId: SessionId,
|
||||
sdkVersion: Int = 33,
|
||||
isInAirGapEnvironment: Boolean = false,
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
||||
) = DefaultSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = sessionId,
|
||||
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkVersion),
|
||||
networkMonitor = FakeNetworkMonitor().apply { givenIsInAirGappedEnvironment(isInAirGapEnvironment) },
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector 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.push.impl.workmanager
|
||||
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.hasKeyWithValueOfType
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerWorkerType
|
||||
import io.element.android.libraries.workmanager.api.workManagerTag
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class SyncPendingNotificationsRequestBuilderTest {
|
||||
@Test
|
||||
fun `build - success API 33`() = runTest {
|
||||
val request = createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = A_SESSION_ID,
|
||||
sdkVersion = 33,
|
||||
)
|
||||
|
||||
val results = request.build()
|
||||
assertThat(results.isSuccess).isTrue()
|
||||
results.getOrNull()!!.first().let { result ->
|
||||
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
|
||||
result.request.run {
|
||||
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
|
||||
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
|
||||
// True in API 33+
|
||||
assertThat(workSpec.expedited).isTrue()
|
||||
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build - success API 32 and lower`() = runTest {
|
||||
val request = createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId = A_SESSION_ID,
|
||||
sdkVersion = 32,
|
||||
)
|
||||
|
||||
val results = request.build()
|
||||
assertThat(results.isSuccess).isTrue()
|
||||
|
||||
results.getOrNull()!!.first().let { result ->
|
||||
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
|
||||
result.request.run {
|
||||
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
|
||||
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
|
||||
// False before API 33
|
||||
assertThat(workSpec.expedited).isFalse()
|
||||
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSyncPendingNotificationsRequestBuilder(
|
||||
sessionId: SessionId,
|
||||
sdkVersion: Int = 33,
|
||||
) = SyncPendingNotificationsRequestBuilder(
|
||||
sessionId = sessionId,
|
||||
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkVersion),
|
||||
)
|
||||
|
|
@ -21,6 +21,7 @@ dependencies {
|
|||
implementation(projects.libraries.push.impl)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.workmanager.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.coil.compose)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.push.test.workmanager
|
||||
|
||||
import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper
|
||||
|
||||
class FakeSyncPendingNotificationsRequestBuilder(
|
||||
private val build: () -> Result<List<WorkManagerRequestWrapper>> = { Result.success(emptyList()) },
|
||||
) : SyncPendingNotificationsRequestBuilder {
|
||||
override suspend fun build(): Result<List<WorkManagerRequestWrapper>> = build.invoke()
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.libraries.textcomposer.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -17,14 +18,10 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.LinearGradientShader
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.colors.gradientActionColors
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
|
|
@ -33,7 +30,6 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
|
|||
/**
|
||||
* Send button for the message composer.
|
||||
* Figma: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1956-37575&node-type=frame&m=dev
|
||||
* Temporary Figma : https://www.figma.com/design/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?node-id=2274-39944&m=dev
|
||||
*/
|
||||
@Composable
|
||||
internal fun SendButtonIcon(
|
||||
|
|
@ -49,11 +45,16 @@ internal fun SendButtonIcon(
|
|||
isEditing -> 0.dp
|
||||
else -> 2.dp
|
||||
}
|
||||
val backgroundColor = if (canSendMessage) {
|
||||
ElementTheme.colors.bgAccentRest
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.size(36.dp)
|
||||
.buttonBackgroundModifier(canSendMessage)
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
|
|
@ -63,11 +64,7 @@ internal fun SendButtonIcon(
|
|||
// Note: accessibility is managed in TextComposer.
|
||||
contentDescription = null,
|
||||
tint = if (canSendMessage) {
|
||||
if (ElementTheme.colors.isLight) {
|
||||
ElementTheme.colors.iconOnSolidPrimary
|
||||
} else {
|
||||
ElementTheme.colors.iconPrimary
|
||||
}
|
||||
ElementTheme.colors.iconOnSolidPrimary
|
||||
} else {
|
||||
ElementTheme.colors.iconQuaternary
|
||||
}
|
||||
|
|
@ -75,31 +72,6 @@ internal fun SendButtonIcon(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.buttonBackgroundModifier(
|
||||
canSendMessage: Boolean,
|
||||
) = then(
|
||||
if (canSendMessage) {
|
||||
val colors = gradientActionColors()
|
||||
Modifier.drawWithCache {
|
||||
val verticalGradientBrush = ShaderBrush(
|
||||
LinearGradientShader(
|
||||
from = Offset(0f, 0f),
|
||||
to = Offset(0f, size.height),
|
||||
colors = colors,
|
||||
)
|
||||
)
|
||||
onDrawBehind {
|
||||
drawRect(
|
||||
brush = verticalGradientBrush,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SendButtonIconPreview() = ElementPreview {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.textcomposer.model.SuggestionType
|
|||
import io.element.android.libraries.textcomposer.model.aMarkdownTextEditorState
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorStyle
|
||||
import io.element.android.wysiwyg.compose.internal.applyStyleInCompose
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("ModifierMissing")
|
||||
@Composable
|
||||
|
|
@ -149,8 +150,20 @@ fun MarkdownTextInput(
|
|||
|
||||
private fun Editable.checkSuggestionNeeded(): Suggestion? {
|
||||
if (this.isEmpty()) return null
|
||||
val start = Selection.getSelectionStart(this)
|
||||
val end = Selection.getSelectionEnd(this)
|
||||
var start = Selection.getSelectionStart(this)
|
||||
var end = Selection.getSelectionEnd(this)
|
||||
val range = 0..this.length
|
||||
|
||||
if (start !in range || end !in range) {
|
||||
Timber.tag("checkSuggestionNeeded").e("Selection indices are out of bounds: start=$start, end=$end, text length=${this.length}")
|
||||
return null
|
||||
}
|
||||
|
||||
// Make sure the selection order is correct, if not swap them: sometimes we can get the end before the start
|
||||
val tempEnd = end
|
||||
end = maxOf(start, end)
|
||||
start = minOf(start, tempEnd)
|
||||
|
||||
var startOfWord = start
|
||||
while ((startOfWord > 0 || startOfWord == length) && !this[startOfWord - 1].isWhitespace()) {
|
||||
startOfWord--
|
||||
|
|
@ -161,11 +174,16 @@ private fun Editable.checkSuggestionNeeded(): Suggestion? {
|
|||
// If a mention span already exists we don't need suggestions
|
||||
if (getSpans<MentionSpan>(startOfWord, startOfWord + 1).isNotEmpty()) return null
|
||||
|
||||
return if (firstChar in listOf('@', '#', '/')) {
|
||||
return if (firstChar in listOf('@', '#', '/', ':')) {
|
||||
var endOfWord = end
|
||||
while (endOfWord < this.length && !this[endOfWord].isWhitespace()) {
|
||||
endOfWord++
|
||||
}
|
||||
if (startOfWord + 1 > endOfWord) {
|
||||
Timber.tag("checkSuggestionNeeded").e("No need to show suggestions for an invalid range (${startOfWord + 1}..$endOfWord)")
|
||||
return null
|
||||
}
|
||||
|
||||
val text = this.subSequence(startOfWord + 1, endOfWord).toString()
|
||||
val suggestionType = when (firstChar) {
|
||||
'@' -> SuggestionType.Mention
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ private const val versionMonth = 3
|
|||
* Release number in the month. Value must be in [0,99].
|
||||
* Do not update this value. it is updated by the release script.
|
||||
*/
|
||||
private const val versionReleaseNumber = 2
|
||||
private const val versionReleaseNumber = 3
|
||||
|
||||
object Versions {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a776c7cb4a3996e3fdb8366f73201ffdc693eed14bfc05482dce3e1418b25bd8
|
||||
size 400144
|
||||
oid sha256:54298b08251d3bd32c451dbb2076a40f20254f78806c30c0647f1bf062f3df7a
|
||||
size 399342
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6cd92a9a0de70f7f777fb72b966c72d5e2b0443b91a3da45e0b59c5061820d6
|
||||
size 399861
|
||||
oid sha256:02bb9e9de3b0ef480cedbed50483bdd3a899497ecc40ead72491106c6f6b6611
|
||||
size 399030
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:64ed3e00c2134d5f437023c48a7a1e52c7a48b954628ea26dcc3ade0a0193079
|
||||
size 59670
|
||||
oid sha256:6e8aabdc6d15c46ee59ba0e9d2b3b3f19500801c96501b39732d7f9f95e9130f
|
||||
size 59204
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a776c7cb4a3996e3fdb8366f73201ffdc693eed14bfc05482dce3e1418b25bd8
|
||||
size 400144
|
||||
oid sha256:54298b08251d3bd32c451dbb2076a40f20254f78806c30c0647f1bf062f3df7a
|
||||
size 399342
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1c6a64aeca9d6f2b8e774d7b0582cd8d721a40de28e378553fc38606c6ef7fb6
|
||||
size 59541
|
||||
oid sha256:9eba0f1d35c5456b58a09ca8370c93f0d1959e8e9aa503501cc49ecfaf198522
|
||||
size 59075
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:621c2bb29fa03f858a78d141a2482f701ecc265784616c52c61b7876c8ca1642
|
||||
size 86580
|
||||
oid sha256:e078891f5a377bf42cb787f436dcb7471de959db0c4d3afa5d7cacee20b2bf15
|
||||
size 86126
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5619a51f1b95e5372d0d87cb39153455c0ebac8c2eeb679ac5cc0efb6460ec14
|
||||
size 73124
|
||||
oid sha256:a56951545b00dc74fd7780648bb2a508176c47df6a2ce6920f2b8a63d15f58a5
|
||||
size 72675
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e815e96adf359dd888f8a71cc4727b4769948c150935c9a4af4f6158a1bf1155
|
||||
size 405883
|
||||
oid sha256:c0008cd2827cf805678958567bbce2ca86640a5f27e95ebf1c9cdbc0b86edfd0
|
||||
size 405032
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6af0c78bd53ed3ec6a59ee7e0ae01214ae58f2d7d18ee39f134433cc628c039
|
||||
size 83272
|
||||
oid sha256:0b08c65638e961e2fa5f194e93c3a63ead0976b13c1b9821850c3ee865eac0a8
|
||||
size 82767
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8ba3418ff659fcf19c24bade74c233a088e3ce71d83aea78c8aa07b06a87a69d
|
||||
size 132254
|
||||
oid sha256:ba76e7df81874aa6549a5f8ca7987046a4b43a63852c83fece541dc319e839d6
|
||||
size 131727
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08ec28f42cc67c98eb6c3dfedc4664e42929780673f43be94d37d60f116ba84d
|
||||
size 57007
|
||||
oid sha256:b92c8a8283be1efed5faf6fb5f8a091f225dee38fd95e1a1b1914fa06661dc21
|
||||
size 56261
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c3fa5bb746a2a005c9766b9c903d03695c5cc5124c81449519fc7a00fd55fa2
|
||||
size 55831
|
||||
oid sha256:c89a2eeba5fa540b6ab6516da1d8dd7810ee0754149a8a7a07cbae2182d106f5
|
||||
size 55364
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72767c6e523a625e14f84c4ea299552051c09d834480c3587bfcc80acd9c59f7
|
||||
size 58936
|
||||
oid sha256:832b227fc82b946df042658d134f1c699c720d3e153576259fb145ec9d0c4c45
|
||||
size 58441
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a9e9d735218cd7cc43f104aed351dbd1b857cdbb7cf0d581316e077dc3dfb6b9
|
||||
size 49248
|
||||
oid sha256:b14de263a1f2356b3e84b2a633b0ed837bd160d04c1ec8eeb8c55176856e59ce
|
||||
size 48755
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c2f91f9b5cbad3fe3d0e87f79cef24481f758d813f90e6ef5f404860456d8e5d
|
||||
size 60114
|
||||
oid sha256:407301c3ad51be44fcfcd804954a672e677ea1bc59af39e4269100acc4f720d7
|
||||
size 59368
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c7efb7d9e793fc763e183027a01abe282f49087135430cfe7c11760b0d7cb39c
|
||||
size 51622
|
||||
oid sha256:473a4f7d9623a7351399d3fcf98e30adbd31feabb23efacda83fb68460b75e48
|
||||
size 50912
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:78ab1575bfee32b875880bc9928ec45447ff2b7ed6d6761e4d9877ca205ca47e
|
||||
size 56338
|
||||
oid sha256:ccf988123f7fcf7a2d6ad3914d3e1a30dcf99c49ada7fb7dbaaacb64d7f2250e
|
||||
size 55562
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed4c08e8ab1fbf30b0ec52bdf3cd96a1245f55b8a055b0b938db39a550e731d6
|
||||
size 50980
|
||||
oid sha256:1612a4b0e0df958cf99295ab318ed8260d5c12b705f65f5db4ab2341930564b8
|
||||
size 50518
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:661f615c15ce9005273638fd8d86716e186671733e1ca71b06484f87506ace86
|
||||
size 53978
|
||||
oid sha256:f69563c42d5315b9e4203719128340fab045ea3473c494e1a1376fe4cbb3f0d8
|
||||
size 53521
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d49f25567be8b0133318295b94ea519c507d939c86c875dececf99fd37bf9257
|
||||
size 44556
|
||||
oid sha256:539799354ad5b383c308779b1278e879db4542d9d1c870c13a54a0ae927767fb
|
||||
size 44098
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fe5d581d00cb389a432ce36b228da90fd4c120f5c362bb12ba9406fbb53a6b2a
|
||||
size 59073
|
||||
oid sha256:c623703f53cbf1ae42e1d05af88e3c94e5b438cb96a3dc5cd234026b29bf0e0a
|
||||
size 58286
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:214e70ab7a16f9afeb9a27cf6335f238e7509eb60693b1ca2976b6095207c311
|
||||
size 52506
|
||||
oid sha256:ba5ae56e80ca9f5d3e5e2e0d50a2cbda7870c6286f350764dac7817d533d6c18
|
||||
size 51735
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5fecd68ea15af9dd73ad56fc65b65c8b769212307467a190730648b47fe7cc55
|
||||
size 8671
|
||||
oid sha256:93c061fb9d41a37e4736cdbe964fe98f9814bb761e93948ec57b4ff541792f55
|
||||
size 7491
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55b02c8ba432b768df833a7ba2e4745b7a48f47bb1601df3bf423e369f95bb65
|
||||
size 8495
|
||||
oid sha256:4bbd6e9e274341f442af1fb720e8651f8216b9b6d1bb0a2ada2477047ecfc713
|
||||
size 6918
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57ee725dec5b62eeb02751da76ab86a12a3f5f6576b95d8f8f326901f9b75616
|
||||
size 52182
|
||||
oid sha256:35e0601856e81bf396b8457ed26ded904ec3acd005e68908c4e94a969144d0a7
|
||||
size 50343
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2bc4850604b7c1eb79bdb6ae143e149949a9af9c32b19aa49c56ba20987d484e
|
||||
size 50396
|
||||
oid sha256:ad65bc6adeac3a2679b4315db178af99aedb8e40d4952dfa7be10b87d8055dae
|
||||
size 48200
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1aa0f7508a9a4e612ea3698f9343f61bd766429d73477f6607e2cddd2bc7d9cd
|
||||
size 53331
|
||||
oid sha256:30a1ad561aa2bc5dfa95f5494a5f016a7b4a51304765960eb71a466c1d59f3b0
|
||||
size 51396
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43fe0fa0c03fb40467763adec06e21ceef05fe58a31c5098a6facda008693f87
|
||||
size 51570
|
||||
oid sha256:e0f904191bb608dc61c489db76b71bd66aaf9125b8138ee5696e3e2a35a3e623
|
||||
size 49605
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:81e733b6bc899d6bd3a6305751866277aa85b5407e930d9a649060f1a1215599
|
||||
size 44786
|
||||
oid sha256:008f560c95d92128a7978478563d47bbf57bc0ed1d165f69c60f3d088a8b1798
|
||||
size 41741
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:61c882e1d8feb6e4cad63fe10924e8876ccd0f5400f32e5b8db299cba886eac5
|
||||
size 42832
|
||||
oid sha256:1e8f94bfe6ac910599edd6d2a49ed0369e2304dc589cb574cbf095157e2ae507
|
||||
size 39148
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9927b817b7a30b9ae2d372a666c9f0e2ea7e660738eff7fd446fa1b71b99a24b
|
||||
size 52026
|
||||
oid sha256:66cd907bd23752fbdea45a6505eb6548bba630088e1a6c00d8e32de3fc64ae0c
|
||||
size 50427
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8bde0663870910a8f5aa4d10afa501210d0dfc6b63e582432eccbc190d9ff0d2
|
||||
size 50790
|
||||
oid sha256:ce7294f8f9b2d847ed17970142bb54f3bf54c7365661e216d74cad1103e7739c
|
||||
size 48581
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9f2f40629b39b310a6e10541767b9aa6f18652ed98537558a5f7dd54ecf1e1d4
|
||||
size 63109
|
||||
oid sha256:f29a4e1068a8416fb78a394ed87ad195d18928a6f7c58a513f20a6b5e46372ed
|
||||
size 60684
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8602db8694b0aabdfc55cef65fb530ecf807ec0c8a6436085279818cb147a937
|
||||
size 60565
|
||||
oid sha256:c133f56b6ba583b28684ff44e07e19ca7fc75efa6341d3e2413c2cd61a27099d
|
||||
size 58169
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57ee725dec5b62eeb02751da76ab86a12a3f5f6576b95d8f8f326901f9b75616
|
||||
size 52182
|
||||
oid sha256:35e0601856e81bf396b8457ed26ded904ec3acd005e68908c4e94a969144d0a7
|
||||
size 50343
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2bc4850604b7c1eb79bdb6ae143e149949a9af9c32b19aa49c56ba20987d484e
|
||||
size 50396
|
||||
oid sha256:ad65bc6adeac3a2679b4315db178af99aedb8e40d4952dfa7be10b87d8055dae
|
||||
size 48200
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af6ef6548666450633aac6b9449af5084065c630acd16a2d090afc89d3000d3e
|
||||
size 63946
|
||||
oid sha256:6dbcd0f76ceb070d1a7b3f9a22292b37c5bd9fa005915233db7bcb8f45d4ccef
|
||||
size 61775
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:407c7a9cda53a42b58e11691da435ba590108b03871e8d64572950e7260b57a8
|
||||
size 61044
|
||||
oid sha256:335853740399624113c6e83d2cbc411691eb74759bcdcc1575b7fa3d10ad4ef6
|
||||
size 58751
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c277f08b913f6b648a1df737654b341a66e937758b8d1c6d8e98b63ce62c2a9b
|
||||
size 53535
|
||||
oid sha256:c8cf7ee8e457e03a2abad0419fb8466f3f43b85abeabf705dd7af412dc4e4d45
|
||||
size 51250
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ef8e4978b968d09dfe41cedf4341e3f0fa51cce7ce5c9e1b45667a328a122920
|
||||
size 50690
|
||||
oid sha256:a6db26883ef045ff984e8cbf6256a01b8221825efa29b82364187c6b4fcf9d2a
|
||||
size 48653
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0032f4a01bd1fab1a8db4bd3c89bd634e2cec4473844c2dd9463fdfe7faf9755
|
||||
size 73375
|
||||
oid sha256:acd46123208b547517a07f6e00476c40293a6c519e15acab9ef57d44658b2045
|
||||
size 72217
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5695860993dcf2ad2e21262401f400d87d75d75eaa831cc8a143ce4dcfb98f1c
|
||||
size 59982
|
||||
oid sha256:8cfe2ba87059ddd4b442d627380813af85c55f41ee9fca2cffdf6054cee50c30
|
||||
size 58821
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8de4add23d101f0ec5b04bd8d051649c4143e77d9cf41949071f9cd4299a3aa0
|
||||
size 72781
|
||||
oid sha256:c2fc7e9b3c12e1f0ca068a9fe5b53fc0d22926a5ec5fda56d8c5304b9a875b5f
|
||||
size 71626
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c1eb4fac4ebd2544d54828831809b4b116530a86316be88ad26f9ae5313dad14
|
||||
size 81423
|
||||
oid sha256:d45917c3e5cea88a3258044c40b553e7289a85d9b2cc8e3394ca2137081e2220
|
||||
size 80237
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d7e08a93435c4380c040e63daa8e6edac47af9946d3075b80abd7f71e53d5ac
|
||||
size 62489
|
||||
oid sha256:5d50faa01b0567152ca88b187d602f48c260048484ed9576d685d0a635d47c68
|
||||
size 61337
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2b3e1905c73b7561c8400c5426bd7c7d95897d9ced5de3212d1f56d806053e1b
|
||||
size 61416
|
||||
oid sha256:112c9de20f972933b15936fb1f1e4d56457df66d72075845c56f88cf261dba76
|
||||
size 60253
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:91e00c997726832f255ce5b376213e1160279f172b60676d853c981cc4b7afb8
|
||||
size 68346
|
||||
oid sha256:cf49f2a364dc2cc054dbd81a67c9ddafb2cb8711c9300f0ac8f1f9c4e3f77ab4
|
||||
size 67206
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55734a9a9725123cbf1d0ebef46579d3a3939b2898f7144e529b71d447456577
|
||||
size 90320
|
||||
oid sha256:b53e8135e3b07a6a4a89def1735146e860c2d52e35e8a4af694b8e872b38e13a
|
||||
size 89219
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8f73bcb2919e607d76dbc85ce82d8c8bf9029708bc2b3d21f212a5b8f8c187f
|
||||
size 60826
|
||||
oid sha256:21899812bbb9d0cf288911fe681626fcc978a6310527e242610d3392d4a4ef66
|
||||
size 59667
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f36ea905ab1718fbe97a89171c21605cab4396dec6a83a0465b945fa2cf2fa79
|
||||
size 62046
|
||||
oid sha256:1cb7578c689fd3cd81236d25d8853f6203a91d8879600e72f23e80c86d8b68d9
|
||||
size 60860
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6adcff4197af21036e0f3ea834739ce50e700484ba60ae343905df5926b5f0d
|
||||
size 68136
|
||||
oid sha256:d23fdafbd834da43a6fe57d5666fc6a3108359838053961a419a9b90a1f7aa53
|
||||
size 67001
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:212b7c60ce30503bac88dbb14dc2a7343f728039286e4c54e7bc5b82a06dce6b
|
||||
size 60393
|
||||
oid sha256:7f70b173214fb5971f3db853c85217f7600e9333a6bd411c11e3f1aee4c4e932
|
||||
size 59220
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1f27bcfe743451f511f73aa5c096db8a424ce8ecbe199d8cb920010a07037a18
|
||||
size 71114
|
||||
oid sha256:fea3bacf1a1927a05b2b515fde06d176e102485474cbed004a781e8d2d7385eb
|
||||
size 69296
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3314f19a32c7a8f3ca0df6262271a60760f7cecde60879bf54b26e398197801e
|
||||
size 58113
|
||||
oid sha256:7661c233ef056af6e014a117530d55e336935ad071467a75f2d165657f33aa7c
|
||||
size 56339
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:03cf43fba473e09ce73354c3ee5e9fa7d051243610eedcdd576a1fcf8c21d4b0
|
||||
size 70560
|
||||
oid sha256:b8b91059084d9cd410414fcea6dc6b3489969f062184834ad89ac9ad5b9c2ee9
|
||||
size 68744
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:85f12e1a86dd50f69766205b627e7db7c5a6b0f37c0c921c148783a1ebfa9bbf
|
||||
size 78970
|
||||
oid sha256:2367a9ca1ad216529eb5ca70406fa28788cd9ef2c3fed9dd66e9a5e4acf3080c
|
||||
size 77172
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d0d91f146f43abfd271025b586d87e0153aac00803bed1682fb381bc2750d8d
|
||||
size 60652
|
||||
oid sha256:efb5dcb70030f73913dc05ffdb70f757f71b615372e02d9e21bbaa9eff91befd
|
||||
size 58801
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:70a9665d9aa337efdd4ac4beedab6dbaddc1e74b41e43499da8351fb59fdfa1f
|
||||
size 59573
|
||||
oid sha256:f92132cbeeb5543e8c4f1e1221b07da57dd6f3abc119c46ec291f240f1e5a522
|
||||
size 57759
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bd5b3afa248eb6486c3be193a48218ace0c3b62c32961ccc4e769cdd822de612
|
||||
size 66348
|
||||
oid sha256:895ba3794457f13dcb3e59499924be167f8408ce6e3782a7b26b452226bbca6a
|
||||
size 64540
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88b66579a22b2097bad6fb912e646d6e08905fdc187629a21ba6e469faf63883
|
||||
size 87210
|
||||
oid sha256:c1df5a7f329260f6c019207ff66b5df2aab8f9d3fff026ea695111361b46c202
|
||||
size 85539
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f22d7e6659dbe26557cc766d156461ccb93d46c725c7c8acd7f0a268103dcc8
|
||||
size 59008
|
||||
oid sha256:b3f7417d9135f5b502429a7030a8531a60f45044a5518806286d1f28487c3a21
|
||||
size 57198
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3f86b4b4191628ed78d7e5d281eb755e36f59f02160f850cedeafa6d03cd2a2c
|
||||
size 60115
|
||||
oid sha256:316ac7e8b79f52192a8c699e903525be92b9d51c86242157b724a59b39c4dcc6
|
||||
size 58254
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d55e7cacfb2ca44438c9d8e145c35f52966ef9281da0745d5d8806d698b6e9dc
|
||||
size 65958
|
||||
oid sha256:df811a1843d3d88ac021d8df892c2b74f61a679317ed40234438459833b90fa9
|
||||
size 64134
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5351b897a218d2d649c7e6e34e1f187421e786754d69bc007509b77f931b2248
|
||||
size 58580
|
||||
oid sha256:3dddaa9eba56b7c02c7e42add553fcf24b3ded18488bfe75e0decc7270386b29
|
||||
size 56795
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:591e982b4aa78f9302b6e2776a5d8a8f80541397fbf32e4b9bafce44b6b10bb8
|
||||
size 75446
|
||||
oid sha256:cf7b37ce6ac1d6430de6d4ffefc2864f0adc502a395a204b3c516dbd901f9710
|
||||
size 73331
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c75ba9c2d470fb6332b140b798be06364b8c1982f997fc3e8ab5048c5a38f3df
|
||||
size 59060
|
||||
oid sha256:c18604b8f5aaed6e4093d3f792ad112668402541ac09547bdebd6a5adbfca35b
|
||||
size 56876
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue