5 Commits

Author SHA1 Message Date
joel.poyet d6ea73140e docs(release): document self-hosted runner and heap sizing 2026-07-27 18:38:37 +02:00
joel.poyet a7e1217e82 fix(gplay): strip permissions Play Console rejects 2026-07-27 18:35:37 +02:00
joel.poyet 09b6d6341b ci(release): add HealthChat Play Store release pipeline 2026-07-27 18:35:36 +02:00
Sedat Qaja a1866b4b28 feat(branding): add HealthChat launcher icon and logo
Official cross-in-circle icon (same art as the iOS X app); onboarding
title localized for en/de/fr/it.
2026-07-16 14:13:00 +02:00
Sedat Qaja 2994430b88 feat(branding): rebrand app identity to HealthChat
App id ch.healthchat.element, locked matrix.healthchat.ch homeserver.
2026-07-16 11:03:52 +02:00
5980 changed files with 26360 additions and 86839 deletions
+8 -37
View File
@@ -159,31 +159,6 @@ fixing the changelog over relying on the fallback.
- [ ] Internal testers get the update automatically if they're enrolled.
- [ ] AAB + `mapping.txt` are attached to the workflow run, retained 90 days.
**A green "Build Release AAB" step does not mean the release shipped.** The
upload step logs `Successfully uploaded 1 artifacts` before it commits the Play
edit; if the commit fails, the AAB is uploaded but no release exists on the
track. Always check the `Upload to Play Store` step, not just the job's build
steps.
## Troubleshooting: `changesNotSentForReview must not be set`
```
Successfully uploaded 1 artifacts
Committing the Edit
##[error]Changes are sent for review automatically.
The query parameter changesNotSentForReview must not be set.
```
Google flips this per app: once Play sends an app's changes for review
automatically, the Edits API rejects `changesNotSentForReview` instead of
ignoring it. The 26.07.0 release (2026-07-27) committed fine with the flag
set; 26.08.0 (2026-08-03) failed with it. The parameter is no longer passed —
if it ever needs to come back, gate it rather than setting it unconditionally.
Recovery: the failed edit is never committed, so the `versionCode` is not
consumed. Re-run via `workflow_dispatch` with the same version — no tag move
and no version bump needed.
## Required GitHub Actions secrets
The pipeline depends on these repository secrets. Missing any causes the
@@ -261,12 +236,12 @@ compliance metadata review.
- **Production rollout gone wrong** → Play Console → Production → **Halt
rollout** (works during staged rollout), then ship a hotfix as above.
## Known branch divergence when syncing with upstream
## Known branch divergence when merging upstream
Upstream is taken by **rebasing** the HealthChat commits onto the new release
tag, never by merging — the procedure, Git LFS handling and force-push live in
the `syncing-upstream-healthchat` skill. The HealthChat-only facts that must
survive every sync:
When merging a newer upstream Element X release tag into `release/healthchat`
(temporarily add `https://github.com/element-hq/element-x-android` as a
remote, fetch, merge, remove the remote — never keep a permanent upstream
remote):
- **`.github/workflows/healthchat-release-playstore.yml`**,
**`HEALTHCHAT_CHANGES.md`**, and
@@ -279,13 +254,9 @@ survive every sync:
Upstream uses `io.element.android.x` / `Element X` / nulls.
- **`appconfig/.../ApplicationConfig.kt`** — `PRODUCTION_APPLICATION_NAME` /
`DESKTOP_APPLICATION_NAME = "HealthChat"` must survive. Upstream: `Element`.
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** — the
homeserver lock: `accountProviderAllowList() = listOf(Generic("matrix.healthchat.ch"))`,
`canConnectToAnyAccountProvider() = false`, `isAllowedToConnectToAccountProvider()`
= membership in that list (since upstream 26.10.0; before that
`defaultHomeserverList()`). Upstream returns an empty list / `true` / `true`.
`BuildTimeConfig.OAUTH_CLIENT_URL_PATH` must stay `null` (upstream:
`"apps/android"`), or MAS rejects the redirect scheme.
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** —
`defaultHomeserverList() = listOf("matrix.healthchat.ch")` must survive.
Upstream returns an empty list.
- **`app/build.gradle.kts`** — the `healthchat` signing config and the
`appicon/healthchat` dependency must survive; upstream signs release with
the debug keystore and depends on `appicon/element`.
@@ -1,215 +0,0 @@
---
name: syncing-upstream-healthchat
description: Bring the HealthChat fork (release/healthchat) onto a newer element-hq/element-x-android release tag. Use when the user asks to rebase, sync, merge or update to upstream, take a new Element X version, or asks why the fork is behind upstream. Covers the patch-stack model, the rebase procedure, Git LFS, conflicts, verification, the force-push and the rewind.
---
# Syncing the HealthChat fork with upstream
`release/healthchat` = upstream release tag + HealthChat commits on top. Patch stack, same as
healthchat-web and element-x-ios. A sync replaces the stack's base and force-pushes the branch.
That is the one sanctioned history rewrite in this repo; everything else in `AGENTS.md` still
holds. We rebase, we never merge upstream into the branch.
First done v26.07.0 → v26.10.0 (2026-10-16 to 10-19). Numbers below come from that run.
## Before touching anything
- [ ] **Toolchain.** Upstream pins the JDK (`java-version` in `.github/workflows/*.yml`, 21),
`COMPILE_SDK` / `BUILD_TOOLS_VERSION` (`plugins/src/main/kotlin/Versions.kt`), AGP, Kotlin
and Metro (`gradle/libs.versions.toml`). Gradle and AGP install themselves; the Rust SDK
comes from Maven. What does not: the self-hosted incus runners ship no Android SDK — the
`Set up Android SDK` step installs it (see the releasing skill, "Runner and build tuning").
If `COMPILE_SDK` moved, check that step still resolves the platform. 26.10.0 moved nothing.
- [ ] **Dev Mac Gradle.** A warm daemon can hang `assembleDebug` forever (0 % CPU, no log).
`./gradlew --stop` first, then compile-only tasks with `--max-workers=2`, e.g.
`:tests:uitests:compileDebugUnitTestKotlin` compiles every module. Gate scripts on the
exit code — a `rebase --continue` after an unchecked compile committed a broken stop once.
- [ ] **Upstream.** There is no upstream remote by default. Tags are `v<version>`.
`git fetch https://github.com/element-hq/element-x-android.git tag v<new> --no-tags` is
enough for the rebase. LFS needs a named remote:
`git remote add upstream https://github.com/element-hq/element-x-android.git`. After
adding it, `gh` may resolve to upstream — always pass `-R Novaloop-AG/element-x-android`.
- [ ] **Decisions, product owner:** which upstream feature-flag defaults flipped
(`git diff v<old> v<new> -- libraries/featureflag/api/.../FeatureFlags.kt`; 26.10.0:
message search default on, kept on), where fork UI lands when upstream restructures a
screen (26.10.0: settings split, HealthChat entries went to the account section), and
whether fork commits may be squashed (only within one HealthChat release, never across).
## The stack
```bash
git log --oneline --no-merges v<old>..release/healthchat # the fork's commits
git merge-base --is-ancestor v<old> release/healthchat # must be true
```
Upstream files the fork edits — conflicts live here every time:
- `features/enterprise/impl-foss/.../DefaultEnterpriseService.kt` + test — the homeserver
lock (since 26.10.0: `accountProviderAllowList()` = `Generic("matrix.healthchat.ch")`,
`canConnectToAnyAccountProvider() = false`, membership check).
- `plugins/src/main/kotlin/config/BuildTimeConfig.kt` — app id, name, OIDC URLs,
`OAUTH_CLIENT_URL_PATH = null` (MAS: redirect scheme must reverse-match the apex host).
- `plugins/src/main/kotlin/Versions.kt` — version; dropped from the stack, set again at release.
- `features/home/impl` (Members tab: `HomeView`, `HomePresenter`, `HomeState`,
`HomeStatePreviewParam`, `HomeNavigationBarItem`, `HomeTopBar`, `HomeFlowNode`, tests) and
`features/home/api/HomeEntryPoint.kt`, `appnav/.../LoggedInFlowNode.kt` (`navigateToUserProfile`).
- `features/preferences/impl` root node/view + `PreferencesFlowNode` (profile editor replaces
the display-name editor, account status banner).
- `features/userprofile`, `features/roomdetails` (profile fields section).
- `libraries/deeplink/impl`, `libraries/matrix/{api,impl}` permalink (HealthChat hosts),
`features/login/impl/DefaultLoginIntentResolver.kt`.
- `.github/workflows/{tests,quality,maestro-local,recordScreenshots}.yml` (self-hosted
runners, heap, fork guards), `app/src/gplay/AndroidManifest.xml` (permission strip),
`HEALTHCHAT_CHANGES.md`.
## Rebuilding the stack
Measure first: dry-run `git rebase --onto v<new> v<old> HEAD` in a throwaway worktree with a
script that records the conflicting files per commit and resolves blindly, then abort. The
conflict map decides the cleanup. 26.10.0: 11 of 31 commits stopped, 29 hunks, 62 goldens.
**1. Clean the series on the old base** (scratch branch off `release/healthchat`). Interactive
rebase is not available to agents; rebuild with `cherry-pick` (`-n` + `commit -C <orig>` for
edited commits; `cherry-pick` has no `-q`). Drop golden-only commits and the version bump;
strip every PNG, and edits to files upstream deleted, out of feature commits; squash the CI
run-of-commits within one release. `git diff --stat release/healthchat` must show only the
intended removals; compile once.
**2. Plain rebase** — `git rebase --onto v<new> v<old> <scratch>`, resolve per stop, keep each
commit's subject and intent. Upstream API removals cannot be "resolved", they need new code
*inside that commit* (26.10.0: `EnterpriseService` redesign). After a large stop compile its
modules before `--continue`. Cross-commit fallout (renames, moved fakes, signature changes)
goes into ONE trailing `Adapt HealthChat to upstream v<new>` commit, not amended into owners —
every amend replays the rest. A mistake in an already-committed stop: `git commit --fixup=<sha>`,
then `GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <sha>^` (conflict-free when later
commits do not touch the file). Intermediates may not compile; the tip must.
**3. Review** — `git range-diff v<old>..release/healthchat v<new>..<scratch>`. `!` on a commit
that applied cleanly is context drift; check the `-`/`+` lines are not ours.
Recurring fallout to expect: Konsist naming (`*PreviewParam` for `PreviewParameterProvider`
subclasses, `*Event` for sealed interfaces), design-system signature changes (26.10.0:
`ListItem(headlineContent)` → `ListItem(content)`), test fakes moving modules, action pins in
our CI hunks regressing upstream's bumps, exhaustive `when` over `HomeNavigationBarItem`.
zsh: `for c in $VAR` does not split on newlines, a variable named `path` *is* `PATH`, and
`$TAG:file` applies a history modifier — use `while read`, never name a variable `path`,
write `${TAG}:file`.
## Embedded Element Call: test every bump against the iPhone
`gradle/libs.versions.toml` pins `element_call_embedded`, the web app the call screen loads. Upstream
lets Renovate bump it; each bump ships a fresh matrix-js-sdk snapshot, and the Rust SDK on both
HealthChat apps parses the call membership that js-sdk writes. 26.10.0 shipped Element Call 0.28.0,
whose js-sdk dropped `livekit_alias` from `foci_preferred`; ruma requires the field, so the Rust SDK
discarded the membership and the iPhone stopped ringing for Android calls (Android's own
ring-cancel detection broke the same way). Pinned to 0.26.1 in 26.10.1, the version iOS embeds; the
hosted Element Call on the server (`matrix_element_call_version` in the playbook inventory) must stay
on the same version or web and desktop calls stop ringing. 0.27.0 is on Maven without a release. Before lifting the pin or
taking a newer bump: start a call from Android, the current iPhone build must ring, and the room
state must still parse (`has_active_room_call`). Lift only when both apps run a Rust SDK that
accepts memberships without the alias.
## Git LFS — the part that bites
Goldens, compound screenshots, `docs/images-lfs`, and `libraries/mediaupload/impl` test assets
are LFS-tracked (`.gitattributes`, ~4500 files). A fresh clone holds pointers only.
- origin is a standalone repo; upstream's LFS objects are not there. GitHub rejects a push
whose *new commits* reference LFS objects it lacks (`GH008 … unknown Git LFS objects`). The
rebased branch carries every upstream commit since the old tag. Fetch the LFS tree of each
upstream commit that touched LFS paths, then push the objects before the branch:
```bash
git log --format=%H v<old>..v<new> -- '*.png' '*.jpg' '*.gif' '*.mp4' '*.m4a' '*.webp' \
| sort -u | while read c; do git lfs fetch upstream "$c"; done
git lfs push origin <scratch> # must report no "(missing)"; refetch that commit
git push origin <scratch>
```
26.10.0: 210 such commits, 9837 objects, 525 MB locally, 3809 uploaded (174 MB). Run the loop
detached (`nohup`) — an agent's tool call times out at 10 minutes — and keep a done-file so it
resumes. Objects still "(missing)" after the loop are usually goldens from the fork's *own*
commits that a pointer-only clone never held: `git lfs fetch origin <original commit sha>`
for each commit on the old branch that touched them. The dry run lists every object until
the branch itself is pushed; verify presence with the LFS batch API or trust a clean upload.
- `git lfs push <branch>` only covers objects introduced by commits absent from the remote. 26.10.0:
167 objects referenced by the v26.10.0 tree were outside that set and `Validate Git LFS` failed
with "Object does not exist on the server". Read the oids from that job's log and
`git lfs push --object-id origin <oid…>` them (they are local after the fetch loop). Treat a
green `Validate Git LFS` on the PR as the only proof the store is complete.
- The recorder commits as ElementBot, a non-member: its CI runs land in `action_required` and
need `gh api -X POST repos/<repo>/actions/runs/<id>/approve` (or the PR's "Approve and run").
Never push to the branch while a recording runs: the recorder's final `git push` is a plain
push and is rejected once the tip has moved.
- `git lfs checkout` before running `libraries/mediaupload/impl` tests locally; pointer files
fail them. The screenshot tests are never run locally (`AGENTS.md`).
- `validate-lfs.yml` fails a PR that commits a PNG outside LFS.
## Screenshots
Take upstream's goldens on every conflict and never commit locally rendered ones (macOS
renders differ). After the push, add the `Record-Screenshots` label to the PR or
`workflow_dispatch` `recordScreenshots.yml`; the Linux runner records and commits all goldens.
Review only the HealthChat ones. A PR whose base is `release/healthchat` is *conflicting* by
construction (old and new stack touch the same files) and GitHub runs no `pull_request`
workflow on a conflicting PR — open the PR against a branch at the upstream tag
(`upstream/v<new>`, pushed by the owner) so the PR shows the 19-odd stack commits and CI runs.
Retargeting a PR is an `edited` event: tests/quality/build only start on the next push.
The label path needs `persist-credentials: true` on the LFS checkout steps (private fork,
fixed in 26.10.0). If the job dies on the LayoutLib static-initialiser crash,
restore the "one JVM per screenshot test class" workaround (`forkEvery = 1L` in the root
`build.gradle.kts`, dropped in 26.10.0 because upstream's Paparazzi bump fixed it).
## Verification of the tip
Local, all green before handover (26.10.0: 1200 unit tests):
```bash
./gradlew :tests:uitests:compileDebugUnitTestKotlin :app:compileGplayDebugKotlin
./gradlew :features:healthchat:impl:testDebugUnitTest :features:home:impl:testDebugUnitTest \
:features:preferences:impl:testDebugUnitTest :features:enterprise:impl-foss:testDebugUnitTest \
:features:login:impl:testDebugUnitTest :libraries:matrix:impl:testDebugUnitTest :appnav:testDebugUnitTest
./gradlew :tests:konsist:test ktlintCheck detekt lint
```
CI on the branch (tests, quality, screenshots) must be green on the self-hosted runners.
Device matrix (owner, debug and release build): MAS OIDC login on a fresh install — chooser
skipped, client URI `https://healthchat.ch` without appended path — is the gate for everything
else; previous release → new keeps the session; last Classic build → new lands on login without
crashing; member directory, profile editor, invites, status banners; deeplinks on HealthChat
hosts and `to.healthchat.ch` share links; push in background and killed; release build under
R8 exercising the suspend Retrofit calls; whatever upstream switched on (26.10.0: German
whole-word search in an encrypted room).
## Force-push, rewind
```bash
git push origin release/healthchat:refs/heads/backup/release-healthchat-before-<new>
git push --force-with-lease=release/healthchat:<old tip> origin <scratch>:release/healthchat
git checkout release/healthchat && git reset --hard origin/release/healthchat # every clone
```
`release/healthchat` is the default branch and the repository ruleset `healthchat-base`
(id 20385846, rules `deletion` + `non_fast_forward`, no bypass actors) rejects the force-push with
`GH013 … Cannot force-push to this branch`. Classic branch protection is empty, so
`gh api repos/<repo>/branches/<branch>/protection` says "not protected" — check
`gh api repos/<repo>/rules/branches/release%2Fhealthchat` instead. Decision (2026-10-20): disable the
ruleset for the push and re-enable it right after, no permanent bypass:
```bash
gh api -X PUT repos/Novaloop-AG/element-x-android/rulesets/20385846 -f enforcement=disabled
git push --force-with-lease=release/healthchat:<old tip> origin <stack>:release/healthchat
gh api -X PUT repos/Novaloop-AG/element-x-android/rulesets/20385846 -f enforcement=active
gh api repos/Novaloop-AG/element-x-android/rulesets/20385846 -q .enforcement # must print active
```
Old `healthchat-v*` tags keep their commits alive and off the branch line. Rewind =
`git push --force origin healthchat-v<last>^{commit}:release/healthchat`. A rewind does not
remove a tag or Play build made on the new history — tag only after the device tests. Then set
the version, update `HEALTHCHAT_CHANGES.md`, the releasing skill ("Known branch divergence")
and this skill with what changed.
## What not to do
- Merge upstream into `release/healthchat`, or squash the fork into a side branch and merge
upstream there: both duplicate every fork commit and conflict on every touched file.
- Pull on a rewritten branch (merges the old history back in). Fetch and reset.
- Commit goldens from a Mac, or resolve a golden conflict by hand — take upstream's and record.
- Amend compile fixes into owner commits mid-rebase; collect them in the adaptation commit.
- Touch `release/healthchat` before the owner has reviewed the range-diff.
+4 -4
View File
@@ -69,18 +69,18 @@ body:
description: |
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
options:
- 'No'
- 'Yes'
- 'No'
validations:
required: true
- type: dropdown
id: pr
attributes:
label: Will you submit a Pull Request?
label: Are you willing to provide a PR?
description: |
Submitting a Pull Request can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
options:
- 'No'
- 'Yes'
- 'No'
validations:
required: true
+2 -2
View File
@@ -1,11 +1,11 @@
name: Enhancement request
description: Do you have a suggestion or feature request? Please open issue on the repo element-hq/element-meta if the request is global to all Element clients.
description: Do you have a suggestion or feature request?
labels: [T-Enhancement]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please open an issue on the [element-meta repository](https://github.com/element-hq/element-meta/issues/new?template=enhancement.yml) instead.
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/element-hq/element-meta/discussions/new?category=ideas).
- type: textarea
id: usecase
attributes:
-4
View File
@@ -28,10 +28,6 @@ changelog:
labels:
- PR-Dependencies
- title: 💼 Element Pro
labels:
- PR-Element-Pro
- title: Others
labels:
- PR-misc
+1 -22
View File
@@ -7,17 +7,10 @@
"PR-Dependencies",
],
"ignoreDeps": [
"string:app_name",
"gradle",
],
"minimumReleaseAge": "7 days",
"hostRules": [
{
"matchHost": "https://repo1.maven.org/maven2/",
"concurrentRequestLimit": 5,
"maxRequestsPerSecond": 5,
"timeout": 10000,
}
],
"packageRules": [
{
"matchPackageNames": [
@@ -25,20 +18,6 @@
"/^io.element.android/",
],
"minimumReleaseAge": null,
// Max default value is 6, with this we give absolute priority to our own dependencies
"prPriority": 7,
"registryUrls": [ "https://repo1.maven.org/maven2/" ],
},
{
// Fetch Android dependencies from Google's Maven repo instead of Maven Central.
"matchPackageNames": [
"/^androidx\\./",
"/^android\\./",
"/^com\\.android\\.tools/",
"/^com\\.google\\.firebase/",
"/^com\\.google\\.android/",
],
"registryUrls": [ "https://dl.google.com/android/maven2/" ],
},
{
"groupName": "kotlin",
+8 -36
View File
@@ -7,32 +7,18 @@ on:
push:
branches: [ develop ]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
# Enrich gradle.properties for CI/CD
env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners.
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
build:
name: Build APKs
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
permissions:
# A job-level block replaces the workflow-level one, so contents:read has to be
# repeated here or checkout fails on this private fork.
contents: read
# For NejcZdovc/comment-pr
pull-requests: write
strategy:
@@ -45,9 +31,6 @@ jobs:
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -61,19 +44,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs
@@ -91,7 +74,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug APKs
if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-debug
path: |
@@ -121,18 +104,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Compile release sources
if: ${{ matrix.variant == 'release' }}
run: |
./gradlew assembleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Run aapt dump
if: ${{ matrix.variant == 'release' }}
run: |
$ANDROID_HOME/build-tools/37.0.0/aapt dump badging \
./app/build/outputs/apk/gplay/release/app-gplay-universal-release.apk \
> /tmp/currentAaptDump.txt
- name: Check manifest has not changed
if: ${{ matrix.variant == 'release' }}
run: diff /tmp/currentAaptDump.txt ./tools/manifest/gplay/release/aaptDump.txt
run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Compile nightly sources
if: ${{ matrix.variant == 'nightly' }}
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
+7 -21
View File
@@ -7,28 +7,17 @@ on:
push:
branches: [ develop ]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
# Enrich gradle.properties for CI/CD
env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners.
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
build:
name: Build Enterprise APKs
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
strategy:
@@ -41,9 +30,6 @@ jobs:
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -57,7 +43,7 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -70,12 +56,12 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK
@@ -93,7 +79,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug Enterprise APKs
if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-enterprise-debug
path: |
+33
View File
@@ -0,0 +1,33 @@
name: Danger CI
on: [pull_request, merge_group]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
name: Danger main check
# Skip in forks, it doesn't work even with the fallback token
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- 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
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
# Fallback for forks
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -19,16 +19,16 @@ jobs:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Run World screenshots generation script
+3 -2
View File
@@ -13,11 +13,12 @@ jobs:
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -46,26 +46,19 @@ concurrency:
env:
# Upstream's workflows hardcode -Xmx8g / Kotlin daemon -Xmx4g, sized for the
# 4-core/16GB ubuntu-latest that public repos get. On the previous incus
# container the daemon kept getting SIGKILLed mid-build
# (DaemonDisappearedException, no OutOfMemoryError, no heap dump), so these
# values stay well under upstream's ceiling and leave room for the Kotlin
# daemon and the parallel lint workers. Re-check them against the "Report
# runner capacity" output now that jobs land on the persistent org runners,
# whose specs may differ.
# 4-core/16GB ubuntu-latest that public repos get. On this container the
# daemon keeps getting SIGKILLed mid-build (DaemonDisappearedException, no
# OutOfMemoryError, no heap dump), so stay well under upstream's ceiling and
# leave room for the Kotlin daemon and the parallel lint workers.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
release:
name: Build and upload AAB to Play Store
# Persistent org runners (github-runner-01, gha-runner-1..3). The previous
# novaloop-incus-large label pointed at the GARM ephemeral pool, whose freshly
# spawned runners were reaped mid-job — jobs died ~10 minutes in with
# "The operation was canceled" and no other diagnostic.
runs-on: [self-hosted, Linux, X64]
runs-on: novaloop-incus-large
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
@@ -85,13 +78,10 @@ jobs:
- name: Resolve marketing version
id: version
env:
# Passed through the environment, never expanded into the script (zizmor: template-injection).
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
VERSION="$INPUT_VERSION"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#healthchat-v}"
fi
@@ -181,24 +171,25 @@ jobs:
done
- name: Use JDK 21
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
# The Android SDK now comes from the self-hosted runner image, so there is no
# setup-android step here. The image must provide it AND export ANDROID_HOME to
# the job environment — Gradle fails with "SDK location not found" otherwise.
# The image also has to carry the platform matching COMPILE_SDK in
# plugins/src/main/kotlin/Versions.kt (platform packages are minor-versioned, so
# compileSdk 37 means android-37.0) and the build-tools version AGP defaults to
# (SdkConstants.CURRENT_BUILD_TOOLS_VERSION), since nothing sets buildToolsVersion
# here. Both need refreshing in the image when AGP or COMPILE_SDK moves.
- name: Configure gradle
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4
# The self-hosted runner image has no Android SDK (unlike GitHub-hosted
# ubuntu-latest), so install it here.
# - platform: follows COMPILE_SDK in plugins/src/main/kotlin/Versions.kt.
# Platform packages are minor-versioned, so compileSdk 37 is android-37.0.
# - build-tools: nothing sets buildToolsVersion, so AGP picks its own
# default (SdkConstants.CURRENT_BUILD_TOOLS_VERSION). Bump it when AGP is
# upgraded, not when Versions.kt changes.
- name: Set up Android SDK
uses: android-actions/setup-android@v3
with:
# Release builds must not consume a cache another workflow could have poisoned.
cache-disabled: true
packages: 'platforms;android-37.0 build-tools;36.0.0'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
- name: Decode keystore
run: |
@@ -228,7 +219,7 @@ jobs:
- name: Upload workflow artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: healthchat-${{ steps.version.outputs.version }}
path: |
@@ -238,7 +229,7 @@ jobs:
if-no-files-found: warn
- name: Upload to Play Store
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: ch.healthchat.element
@@ -247,8 +238,4 @@ jobs:
status: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_status || 'completed' }}
whatsNewDirectory: fastlane/metadata/android
mappingFile: app/build/outputs/mapping/gplayRelease/mapping.txt
# Do NOT set changesNotSentForReview. Play now sends this app's changes for review
# automatically, and the Edits API rejects the parameter outright when it does:
# "Changes are sent for review automatically. The query parameter
# changesNotSentForReview must not be set." The AAB uploads fine and then the commit
# fails, so the release silently never lands on the track. See SKILL.md.
changesNotSentForReview: true
+11 -44
View File
@@ -5,22 +5,11 @@ on:
workflow_dispatch:
pull_request:
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
# Enrich gradle.properties for CI/CD
env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners.
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
ARCH: x86_64
DEVICE: pixel_7_pro
@@ -30,18 +19,12 @@ env:
jobs:
build-apk:
name: Build APK
runs-on: [self-hosted, Linux, X64]
# Secrets are not available to pull_request runs from forks, so the Maestro job below
# cannot run for them; skip the build too, as its only consumer is that job.
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
concurrency:
group: ${{ format('maestro-build-{0}', github.ref) }}
cancel-in-progress: true
steps:
- name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -55,19 +38,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }}
persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
name: Use JDK 21
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK
@@ -77,7 +60,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- name: Upload APK as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-apk-maestro
path: |
@@ -90,16 +73,13 @@ jobs:
name: Maestro test suite
runs-on: ubuntu-latest
needs: [ build-apk ]
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
# Allow only one to run at a time, since they use the same environment.
# Otherwise, tests running in parallel can break each other.
# queue: max lets several runs wait their turn instead of the default, where a
# newly queued run cancels the one already pending (that run then never gets a verdict).
concurrency:
group: maestro-test
queue: max
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -118,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@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
continue-on-error: true
env:
MAESTRO_USERNAME: maestroelement
@@ -138,21 +118,8 @@ jobs:
disk-size: 3G
script: |
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
- name: Set up Python
if: always()
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: 3.14
- name: End all the remaining sessions of the Maestro account
if: always()
continue-on-error: true
env:
MAS_PASSWORD: ${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
run: |
pip install requests
python3 ./tools/mas/delete_all_sessions.py --homeserver matrix.org --username maestroelement
- name: Upload test results
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results
path: |
+2 -2
View File
@@ -32,11 +32,11 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+9 -9
View File
@@ -37,28 +37,28 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: false
- name: ⚙️ Run unit tests, debug and release
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES --continue
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
- name: 📸 Run screenshot tests
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES --continue
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈 Generate kover report and verify coverage
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: ✅ Upload kover report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: kover-results
path: |
@@ -76,23 +76,23 @@ jobs:
name: Dependency analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
- name: Upload dependency analysis
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependency-analysis
path: build/reports/dependency-check-report.html
-128
View File
@@ -1,128 +0,0 @@
name: PR Checks
on:
# Privilege escalation necessary to comment on pull requests from forks.
# 🚨 We must not check out or execute any code here, and be careful around use of user-controlled inputs.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [ opened, reopened, edited, synchronize, labeled, unlabeled ]
permissions: {}
jobs:
pr-checks:
# Upstream-only: this fork does not use the PR- label convention.
if: github.repository == 'element-hq/element-x-android'
name: PR Checks
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
group: ${{ format('pr-checks-{0}', github.ref) }}
cancel-in-progress: true
permissions:
# Required to comment on the pull request.
pull-requests: write
steps:
- name: Check the pull request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
// Classes that must not be modified anymore. Add the file name suffix here to freeze a class.
const FROZEN_CLASSES = [
];
// Only this user is allowed to edit the translation files, since they come from Localazy.
const TRANSLATION_ALLOW_LIST = ["ElementBot"];
const pr = context.payload.pull_request;
const body = pr.body ?? "";
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
// Deleted files are not interesting for any of the checks below.
const paths = files.filter(file => file.status !== "removed").map(file => file.filename);
const warnings = [];
const failures = [];
if (!body) {
warnings.push("Please provide a description for this PR.");
}
if (paths.length > 50) {
warnings.push("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.");
}
if (pr.title.endsWith("…")) {
failures.push("Please provide a complete title that can be used as a changelog entry.");
}
if (pr.labels.filter(label => label.name.startsWith("PR-")).length !== 1) {
failures.push("Please add a single `PR-` label to categorise the changelog entry.");
}
for (const frozen of FROZEN_CLASSES) {
if (paths.some(path => path.endsWith(frozen))) {
failures.push(`Frozen class \`${frozen}\` has been modified. Please do not modify frozen class.`);
}
}
// Exclude the recorded screenshots, which are legitimately pngs.
if (paths.some(path => path.toLowerCase().endsWith(".png") && !path.includes("snapshots/images/"))) {
warnings.push("You seem to have made changes to some images. Please consider using a vector drawable.");
}
if (!TRANSLATION_ALLOW_LIST.includes(pr.user.login) && paths.some(path => path.endsWith("translations.xml"))) {
failures.push("Some translation files have been edited. Only user `ElementBot` (i.e. translations coming from Localazy) is allowed to do that. " +
"Please read more about translations management [in the doc](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#strings).");
}
warnings.forEach(warning => core.warning(warning));
failures.forEach(failure => core.error(failure));
// Keep a single comment up to date rather than adding a new one on every run.
const marker = "<!-- pr-checks -->";
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});
const existing = comments.find(comment => comment.body.includes(marker));
if (warnings.length || failures.length) {
const lines = [...failures.map(failure => `- ❌ ${failure}`),
...warnings.map(warning => `- ⚠️ ${warning}`)];
const commentBody = `${marker}\n### PR Checks\n\n${lines.join("\n")}`;
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: commentBody
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: commentBody
});
}
} else if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id
});
}
if (failures.length) {
core.setFailed(`${failures.length} check(s) failed, see the comment on the pull request.`);
}
+27 -32
View File
@@ -23,38 +23,33 @@ jobs:
script: |
core.setFailed("PR has been labeled with X-Blocked; it cannot be merged.");
# Disabled on the HealthChat fork. This job asks whether the PR author belongs to the
# element-hq "Vector Core" team, using secrets.ELEMENT_BOT_TOKEN_READ_ORG — a secret
# that only exists upstream. Here it fails on every PR that is opened, and there is no
# community to label anyway. Kept commented rather than deleted so upstream changes to
# it still conflict visibly on merge.
# community-prs:
# name: Label Community PRs
# runs-on: ubuntu-latest
# if: github.event.action == 'opened'
# permissions:
# pull-requests: write
# steps:
# - name: Check membership
# if: github.event.pull_request.user.login != 'renovate[bot]'
# uses: tspascoal/get-user-teams-membership@b2546c5affc730fd8e3d8483ae9ad3621938c2f9 # v4.0.2
# id: teams
# with:
# username: ${{ github.event.pull_request.user.login }}
# organization: element-hq
# team: Vector Core
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }}
# - name: Add label
# if: steps.teams.outputs.isTeamMember == 'false'
# uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# with:
# script: |
# github.rest.issues.addLabels({
# issue_number: context.issue.number,
# owner: context.repo.owner,
# repo: context.repo.repo,
# labels: ['Z-Community-PR']
# });
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
if: github.event.action == 'opened'
permissions:
pull-requests: write
steps:
- name: Check membership
if: github.event.pull_request.user.login != 'renovate[bot]'
uses: tspascoal/get-user-teams-membership@b2546c5affc730fd8e3d8483ae9ad3621938c2f9 # v4.0.2
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
organization: element-hq
team: Vector Core
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }}
- name: Add label
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Z-Community-PR']
});
close-if-fork-develop:
name: Forbid develop branch fork contributions
+61 -226
View File
@@ -7,22 +7,11 @@ on:
push:
branches: [ main, develop ]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
# Enrich gradle.properties for CI/CD
env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners.
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
@@ -31,9 +20,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -47,7 +33,7 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
@@ -65,11 +51,11 @@ jobs:
name: Search for invalid screenshot files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Search for invalid screenshot files
@@ -77,22 +63,22 @@ jobs:
checkDependencies:
name: Search for invalid dependencies
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Search for invalid dependencies
@@ -101,13 +87,13 @@ jobs:
# Code checks
konsist:
name: Konsist tests
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -122,19 +108,19 @@ jobs:
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@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: konsist-report
path: |
@@ -142,22 +128,13 @@ jobs:
compose:
name: Compose tests
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
cancel-in-progress: true
steps:
# Records the container's real limits so heap settings can be read against them
# instead of guessed at. Mirrors healthchat-release-playstore.yml.
- name: Report runner capacity
if: runner.environment == 'self-hosted'
run: |
free -h; nproc; df -h .
cat /sys/fs/cgroup/memory.max 2>/dev/null \
|| cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null \
|| echo "no cgroup memory limit visible"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -172,12 +149,12 @@ jobs:
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@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests
@@ -185,13 +162,13 @@ jobs:
lint:
name: Android lint check
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -206,12 +183,12 @@ jobs:
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@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug
@@ -222,7 +199,7 @@ jobs:
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linting-report
path: |
@@ -230,13 +207,13 @@ jobs:
detekt:
name: Detekt checks
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -251,19 +228,19 @@ jobs:
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@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon --continue
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: detekt-report
path: |
@@ -271,13 +248,13 @@ jobs:
ktlint:
name: Ktlint checks
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -292,19 +269,19 @@ jobs:
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@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES --continue
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ktlint-report
path: |
@@ -318,7 +295,7 @@ jobs:
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -342,7 +319,7 @@ jobs:
name: Check shell scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run shellcheck
@@ -354,181 +331,39 @@ jobs:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
# A job-level block replaces the workflow-level one, so contents:read has to be
# repeated here or checkout fails on this private fork.
contents: read
actions: read # Required for zizmor to read workflow files.
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
- uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
upload_reports:
name: Project Check Suite
runs-on: ubuntu-latest
needs: [konsist, lint, ktlint, detekt]
# Run even if a previous job failed: that is precisely when there are issues to report.
if: ${{ !cancelled() }}
permissions:
# Required to list the files modified by the pull request.
pull-requests: read
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Annotate the lint, ktlint and detekt issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- name: Prepare Danger
if: always()
run: |
npm install --save-dev @babel/core
npm install --save-dev @babel/plugin-transform-flow-strip-types
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8
with:
script: |
// GitHub only surfaces a handful of annotations per level and per step, so cap what we emit
// and let the job summary carry the full list.
const MAX_ANNOTATIONS = 20;
const MAX_SUMMARY_ROWS = 200;
function unescapeXml(value) {
return value
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, "\"")
.replace(/&apos;/g, "'")
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
.replace(/&amp;/g, "&");
}
function attribute(tag, name) {
const match = new RegExp(`\\b${name}="([^"]*)"`).exec(tag);
return match ? unescapeXml(match[1]) : undefined;
}
// Paths in the reports are absolute paths on the runner which generated them, i.e.
// /home/runner/work/<repo>/<repo>/<path>.
const workspace = `/work/${context.repo.repo}/${context.repo.repo}/`;
function repoRelative(path) {
const index = path.indexOf(workspace);
return index === -1 ? path : path.slice(index + workspace.length);
}
// Checkstyle XML, as produced by ktlint and detekt:
// <file name="…"><error line=".." column=".." severity=".." message=".." source=".."/></file>
function parseCheckstyle(content) {
const issues = [];
const fileRegex = /<file\s+([^>]*?)(\/>|>([\s\S]*?)<\/file>)/g;
let file;
while ((file = fileRegex.exec(content)) !== null) {
const path = attribute(file[1], "name");
if (!path) continue;
const errorRegex = /<error\s+([^>]*?)\/?>/g;
let error;
while ((error = errorRegex.exec(file[3] ?? "")) !== null) {
issues.push({
file: repoRelative(path),
line: Number(attribute(error[1], "line")) || 1,
column: Number(attribute(error[1], "column")) || undefined,
severity: (attribute(error[1], "severity") ?? "warning").toLowerCase(),
rule: attribute(error[1], "source"),
message: attribute(error[1], "message") ?? "",
});
}
}
return issues;
}
// Android lint XML, which is not Checkstyle:
// <issue id=".." severity=".." message=".."><location file=".." line=".." column=".."/></issue>
function parseAndroidLint(content) {
const issues = [];
const issueRegex = /<issue\s+([^>]*?)(\/>|>([\s\S]*?)<\/issue>)/g;
let issue;
while ((issue = issueRegex.exec(content)) !== null) {
const location = /<location\s+([^>]*?)\/?>/.exec(issue[3] ?? "");
if (!location) continue;
const path = attribute(location[1], "file");
if (!path) continue;
issues.push({
file: repoRelative(path),
line: Number(attribute(location[1], "line")) || 1,
column: Number(attribute(location[1], "column")) || undefined,
severity: (attribute(issue[1], "severity") ?? "warning").toLowerCase(),
rule: attribute(issue[1], "id"),
message: attribute(issue[1], "message") ?? "",
});
}
return issues;
}
const reports = await (await glob.create("**/reports/**/*.xml")).glob();
core.info(`Found ${reports.length} XML report(s).`);
const fs = require("fs");
let issues = [];
for (const report of reports) {
const content = fs.readFileSync(report, "utf8");
if (content.includes("<checkstyle")) {
issues.push(...parseCheckstyle(content));
} else if (content.includes("<issues")) {
issues.push(...parseAndroidLint(content));
}
}
// Only report on the files touched by the pull request.
const pr = context.payload.pull_request;
if (pr) {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
const changed = new Set(files.map(file => file.filename));
issues = issues.filter(issue => changed.has(issue.file));
}
const seen = new Set();
issues = issues.filter(issue => {
const key = `${issue.file}:${issue.line}:${issue.rule}:${issue.message}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
if (issues.length === 0) {
core.info("No issue found in the files modified by this pull request.");
return;
}
const level = severity => {
if (severity === "error" || severity === "fatal") return "error";
if (severity === "warning") return "warning";
return "notice";
};
const rank = { error: 0, warning: 1, notice: 2 };
issues.sort((a, b) =>
rank[level(a.severity)] - rank[level(b.severity)] ||
a.file.localeCompare(b.file) ||
a.line - b.line
);
issues.slice(0, MAX_ANNOTATIONS).forEach(issue => {
const annotation = {
title: issue.rule,
file: issue.file,
startLine: issue.line,
startColumn: issue.column,
};
core[level(issue.severity)](issue.message, annotation);
});
core.summary.addHeading(`${issues.length} code quality issue(s)`, 3);
core.summary.addTable([
[{ data: "Severity", header: true }, { data: "File", header: true }, { data: "Rule", header: true }, { data: "Message", header: true }],
...issues.slice(0, MAX_SUMMARY_ROWS).map(issue => [
level(issue.severity),
`${issue.file}:${issue.line}`,
issue.rule ?? "",
issue.message,
]),
]);
if (issues.length > MAX_SUMMARY_ROWS) {
core.summary.addRaw(`…and ${issues.length - MAX_SUMMARY_ROWS} more, see the uploaded reports.`, true);
}
await core.summary.write();
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
# Fallback for forks
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+11 -37
View File
@@ -5,27 +5,12 @@ on:
pull_request:
types: [ labeled ]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
# Enrich gradle.properties for CI/CD
env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners.
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
# Kept character-identical to tests.yml so the record path and the verify path cannot drift
# apart again. These only affect the Gradle daemon — Paparazzi renders in forked test workers
# configured entirely by build.gradle.kts — but keeping them aligned removes a whole class of
# "did record and verify run the same way?" questions.
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache --max-workers=4
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true
CI_GRADLE_ARG_PROPERTIES: --no-configuration-cache
jobs:
record:
@@ -34,14 +19,11 @@ jobs:
pull-requests: write
contents: write
name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }}
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots'
steps:
- name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -60,29 +42,25 @@ jobs:
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0
with:
labels: Record-Screenshots
# Private fork: the action checks out and then runs `git lfs pull` on a cache miss. Upstream
# can do that anonymously because it is public; here the pull needs the job token, so the
# credentials must stay configured. Without this the step dies with
# `could not read Username for 'https://github.com'`.
- name: ⏬ Checkout with LFS (PR)
if: github.event.label.name == 'Record-Screenshots'
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 zizmor: ignore[artipacked]
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: true
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 zizmor: ignore[artipacked]
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: true
persist-credentials: false
- name: ☕️ Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots
@@ -90,9 +68,5 @@ jobs:
run: ./.github/workflows/scripts/recordScreenshots.sh
env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }}
# Must be the `github.repository` context, not a secret. There is no
# GITHUB_REPOSITORY secret on this fork, so `secrets.GITHUB_REPOSITORY` expands to ""
# and overrides the built-in env var of the same name — recordScreenshots.sh then
# exits 1 with "No repo specified" before Gradle ever runs.
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }}
GRADLE_ARGS: ${{ env.CI_GRADLE_ARG_PROPERTIES }}
+46 -12
View File
@@ -34,18 +34,16 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check Matrix Rust SDK is not a nightly version
run: ./tools/release/check_no_nightly_sdk.sh
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Create app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -59,12 +57,50 @@ jobs:
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-app-gplay-bundle-unsigned
path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
enterprise:
name: Create App Bundle Enterprise
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
concurrency:
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Create Enterprise app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-enterprise-app-gplay-bundle-unsigned
path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
fdroid:
name: Create APKs (FDroid)
runs-on: ubuntu-latest
@@ -86,18 +122,16 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check Matrix Rust SDK is not a nightly version
run: ./tools/release/check_no_nightly_sdk.sh
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Create APKs
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -105,7 +139,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload apks as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-app-fdroid-apks-unsigned
path: |
@@ -51,13 +51,18 @@ if [[ -z ${REPO} ]]; then
exit 1
fi
# Note: the previous screenshots are deliberately kept, so that Paparazzi can leave the ones that only
# differ by less than `maxPercentDifference` untouched instead of rewriting their Git LFS blob. The
# screenshots of previews that no longer exist are pruned afterwards by `pruneObsoleteSnapshots`, which
# `recordPaparazziDebug` is finalized by.
echo "Deleting previous screenshots"
./gradlew removeOldSnapshots --stacktrace --warn $GRADLE_ARGS
echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS
echo "Deleting previous screenshots"
./gradlew removeOldScreenshots --stacktrace --warn $GRADLE_ARGS
echo "Record screenshots (Compound)"
./gradlew :libraries:compound:recordRoborazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn $GRADLE_ARGS
echo "Committing changes"
git config http.sslVerify false
@@ -73,10 +78,6 @@ else
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi
git add -A
if git diff --cached --quiet; then
echo "Screenshots are already up to date, nothing to commit."
exit 0
fi
git commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
+6 -20
View File
@@ -7,38 +7,24 @@ on:
push:
branches: [ main, develop ]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
# Enrich gradle.properties for CI/CD
env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners.
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true --no-configuration-cache
GROUP: ${{ format('sonar-{0}', github.ref) }}
jobs:
sonar:
name: Sonar Quality Checks
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ format('sonar-{0}', github.ref) }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
steps:
- name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -52,19 +38,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
only-labels: "X-Needs-Info"
days-before-issue-stale: 30
@@ -1,85 +0,0 @@
name: Sync Enterprise submodule
on:
workflow_dispatch:
push:
branches: [ develop ]
paths: [ enterprise ]
schedule:
# At 06:00 UTC every day
- cron: '0 6 * * *'
permissions: {}
jobs:
sync-enterprise-submodule:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'element-hq/element-x-android'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Check enterprise submodule reference
id: check
env:
EVENT_NAME: ${{ github.event_name }}
run: |
current=$(git ls-tree HEAD enterprise | awk '{print $3}')
# Do not init the submodule: the current commit may be on a deleted branch.
git init -q "$RUNNER_TEMP/enterprise"
git -C "$RUNNER_TEMP/enterprise" fetch -q git@github.com:element-hq/element-android-enterprise.git main
main=$(git -C "$RUNNER_TEMP/enterprise" rev-parse FETCH_HEAD)
if git -C "$RUNNER_TEMP/enterprise" merge-base --is-ancestor "$current" "$main" 2>/dev/null; then
on_main=true
else
on_main=false
fi
echo "Current: $current, main: $main, current is on main: $on_main"
if [ "$current" = "$main" ]; then
echo "Submodule is already up to date."
echo "update=false" >> "$GITHUB_OUTPUT"
elif [ "$EVENT_NAME" = "push" ] && [ "$on_main" = "true" ]; then
echo "Submodule targets a commit on main, nothing to do."
echo "update=false" >> "$GITHUB_OUTPUT"
else
echo "update=true" >> "$GITHUB_OUTPUT"
fi
{
echo "body<<EOF"
if [ "$on_main" = "true" ]; then
# Do not list the commit messages, the enterprise repository is private.
count=$(git -C "$RUNNER_TEMP/enterprise" rev-list --count "$current..$main")
echo "Update the \`enterprise\` submodule from \`$current\` to the latest commit of \`main\` (\`$main\`), $count new commit(s)."
else
echo "The \`enterprise\` submodule targets \`$current\`, which is not on the \`main\` branch."
echo
echo "Update it to the latest commit of \`main\` (\`$main\`)."
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"
if [ "$current" != "$main" ]; then
# Update the submodule reference without cloning it.
git update-index --cacheinfo "160000,$main,enterprise"
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -q -m "Update enterprise submodule to latest main" -m "Update to $main"
fi
- name: Create Pull Request to update the enterprise submodule
# On schedule and manual runs, also run when there is nothing to update, so that a stale PR gets closed.
if: ${{ steps.check.outputs.update == 'true' || github.event_name != 'push' }}
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
title: Update enterprise submodule to latest main
body: ${{ steps.check.outputs.body }}
branch: sync-enterprise-submodule
base: develop
delete-branch: true
labels: PR-Element-Pro
+4 -8
View File
@@ -7,29 +7,26 @@ on:
permissions: {}
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
jobs:
sync-localazy:
runs-on: ubuntu-latest
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Setup Localazy
@@ -42,7 +39,6 @@ jobs:
./tools/localazy/downloadStrings.sh --all
./tools/localazy/importSupportedLocalesFromLocalazy.py
./tools/test/generateAllScreenshots.py
./tools/manifest/aaptDump.sh
- name: Create Pull Request for Strings
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
+2 -2
View File
@@ -14,11 +14,11 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
# No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Install Prerequisite dependencies
+13 -57
View File
@@ -7,53 +7,24 @@ on:
push:
branches: [ main, develop ]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
# Enrich gradle.properties for CI/CD
env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners.
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
# --max-workers caps how many test tasks run at once. build.gradle.kts sets
# maxParallelForks = cores/2 and screenshot tests fork at maxHeapSize=2g, so on these
# 8-core containers the default (workers = cores = 8) allows 8 x 4 x 2g = 64 GB of test
# JVMs against a 48 GiB cgroup limit, with no swap — the daemon gets OOM-killed and
# Gradle reports "the daemon has disappeared". GitHub's 4-core hosted runners reach only
# 2 x 2 x 2g = 16 GB and additionally have the 8 GB swapfile added above.
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache --max-workers=4
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs:
tests:
name: Runs unit tests
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
cancel-in-progress: true
steps:
# Records the container's real limits so heap settings can be read against them
# instead of guessed at. Mirrors healthchat-release-playstore.yml.
- name: Report runner capacity
if: runner.environment == 'self-hosted'
run: |
free -h; nproc; df -h .
cat /sys/fs/cgroup/memory.max 2>/dev/null \
|| cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null \
|| echo "no cgroup memory limit visible"
- name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -70,9 +41,6 @@ jobs:
# Increase swapfile size to prevent screenshot tests getting terminated
# https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749
- name: 💽 Increase swapfile size
# /mnt/swapfile is a GitHub runner-image path and this needs passwordless sudo,
# so it only applies to the hosted VMs.
if: runner.environment == 'github-hosted'
run: |
sudo swapoff -a
sudo fallocate -l 8G /mnt/swapfile
@@ -95,31 +63,25 @@ jobs:
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@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Run unit tests
run: ./gradlew :app:testGplayDebugUnitTest testDebugUnitTest -x :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --continue
- name: ⚙️ Run screenshot tests
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: ⚙️ Generate kover coverage report
run: ./gradlew :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll -x testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES
- name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: 🚫 Upload kover failed coverage reports
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: kover-error-report
path: |
build/reports/kover
app/build/reports/kover
- name: ✅ Upload kover report (disabled)
if: always()
@@ -127,11 +89,12 @@ jobs:
- name: 🚫 Upload test results on error
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tests-and-screenshot-tests-results
path: |
**/build/paparazzi/failures/
**/build/roborazzi/failures/
**/build/reports/tests/*UnitTest/
- name: 🚫 Modify summary on error
@@ -144,15 +107,8 @@ jobs:
echo "---" >> $GITHUB_STEP_SUMMARY
# https://github.com/codecov/codecov-action
# This fork has no CODECOV_TOKEN, and Codecov rejects tokenless uploads from private
# repositories ("Token required - not valid tokenless upload"). With
# fail_ci_if_error: true that failed the whole job *after* the tests had passed, so
# skip the upload unless a token is configured. Adding the secret re-enables it.
- name: ☂️ Upload coverage reports to codecov
if: env.CODECOV_TOKEN != ''
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
+1 -5
View File
@@ -2,11 +2,7 @@ name: Validate Git LFS
on: [pull_request, merge_group]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
permissions: {}
jobs:
build:
+3
View File
@@ -31,6 +31,9 @@ __pycache__/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
-34
View File
@@ -1,39 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+1 -1
View File
@@ -1,6 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="© &amp;#36;today.year Element Creations Ltd.&#10;&#10;SPDX-License-Identifier: LicenseRef-Element-Commercial&#10;SPDX-License-Identifier: LicenseRef-Element-Proprietary" />
<option name="notice" value="© &amp;#36;today.year Element Creations Ltd.&#10;&#10;Element Creations Ltd, Element Software SARL, Element Software Inc.,&#10;and Element Software GmbH (the &quot;Element Group&quot;) only make this file available&#10;under a proprietary license model.&#10;&#10;Without a proprietary license with us, you cannot use this file. The terms of&#10;the proprietary license agreement between you and any member of the Element Group&#10;shall always apply to your use of this file. Unauthorised use, copying, distribution,&#10;or modification of this file, via any medium, is strictly prohibited.&#10;&#10;For details about the licensing terms, you must either visit our website or contact&#10;a member of our sales team." />
<option name="myName" value="Element Enterprise" />
</copyright>
</component>
+1
View File
@@ -16,6 +16,7 @@
<w>posthog</w>
<w>rageshake</w>
<w>securebackup</w>
<w>showkase</w>
<w>snackbar</w>
<w>spdx</w>
<w>swipeable</w>
+2 -2
View File
@@ -2,6 +2,6 @@
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.4.20" />
<option name="version" value="2.4.0" />
</component>
</project>
</project>
+21
View File
@@ -0,0 +1,21 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "login-change_server"
- takeScreenshot: build/maestro/200-ChangeServer
- tapOn: "matrix.org"
- tapOn:
id: "login-change_server"
- tapOn: "Other"
- tapOn:
id: "change_server-server"
- inputText: "element"
- hideKeyboard
- extendedWaitUntil:
visible: "element.io"
timeout: 10000
- tapOn: "element.io"
# Revert to matrix.org
- tapOn:
id: "login-change_server"
- tapOn: "matrix.org"
+2 -8
View File
@@ -3,17 +3,11 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Sign in manually"
- runFlow: ../assertions/assertLoginDisplayed.yaml
- takeScreenshot: build/maestro/100-SignIn
# The account provider field defaults to matrix.org, so continue straight to the MAS login.
- runFlow: changeServer.yaml
- runFlow: ../assertions/assertLoginDisplayed.yaml
- tapOn:
id: "login-continue"
## MAS page
# Click on "Use another account" if MAS is already logged in:
- runFlow:
when:
visible: "Use another account"
commands:
- tapOn:
text: "Use another account"
- retry:
maxRetries: 3
commands:
+1 -10
View File
@@ -6,19 +6,10 @@ appId: ${MAESTRO_APP_ID}
- takeScreenshot: build/maestro/900-SignOutScreen
- back
- tapOn: "Remove this device"
# Ensure pressing back cancels
- back
- tapOn: "Remove this device"
# Ensure cancel cancels
- tapOn: "Cancel"
- tapOn: "Remove this device"
- tapOn:
id: "sign-out-submit"
# Cancel the dialog cancels the logout
- tapOn:
id: "dialog-negative"
- tapOn:
id: "sign-out-submit"
- tapOn: "Remove this device"
- tapOn:
id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml
+1 -1
View File
@@ -16,4 +16,4 @@ appId: ${MAESTRO_APP_ID}
- extendedWaitUntil:
visible: "Device verified"
timeout: 30000
- tapOn: "Done"
- tapOn: "Continue"
@@ -1,5 +1,5 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Enter your account provider"
visible: "Change account provider"
timeout: 10000
@@ -1,21 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
# Clean up the room list if it was left in a bad state by previous tests
- runFlow:
when:
visible: 'aRoomName'
commands:
- longPressOn: 'aRoomName'
- tapOn:
text: "Leave room"
index: 0
- tapOn: "Leave"
- runFlow:
when:
visible: ${MAESTRO_INVITEE1_MXID}
commands:
- longPressOn: ${MAESTRO_INVITEE1_MXID}
- tapOn:
text: "Leave room"
index: 0
- tapOn: "Leave"
+1 -10
View File
@@ -7,16 +7,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn:
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- runFlow:
when:
visible: 'Start a chat with this new contact?'
commands:
- tapOn: 'Continue'
- runFlow:
when:
visible: 'Send invite?'
commands:
- tapOn: 'Send invite'
- tapOn: "Continue"
- takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2"
- scroll
@@ -8,12 +8,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Add description…"
- inputText: "aRoomTopic"
- tapOn: "Create"
# It can take ages to create room on matrix.org
- extendedWaitUntil:
timeout: 30000
visible: "Invite people"
- takeScreenshot: build/maestro/320-createAndDeleteRoom
- tapOn: "Search for someone"
- inputText: ${MAESTRO_INVITEE1_MXID}
@@ -21,12 +15,6 @@ appId: ${MAESTRO_APP_ID}
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- tapOn: "Finish"
- runFlow:
when:
visible: 'Invite new contact to this room?'
commands:
- tapOn:
id: "confirm_invite_unknown"
- tapOn: "aRoomName"
- tapOn: "Invite"
# assert there's 1 member and 1 invitee
@@ -35,10 +23,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn:
text: ${MAESTRO_INVITEE2_MXID}
index: 1
# Hide keyboard by tapping on an external element in case it's displayed, otherwise it will 'eat' the back button press
- tapOn:
# Navigation title
text: "Invite people"
- tapOn: "Invite"
- runFlow:
when:
@@ -46,6 +30,10 @@ appId: ${MAESTRO_APP_ID}
commands:
- tapOn:
id: "confirm_invite_unknown"
# Close the keyboard if it's still open
- tapOn: "Back"
# Go back to the room details screen
- tapOn: "Back"
- scrollUntilVisible:
direction: DOWN
element:
+1 -2
View File
@@ -1,7 +1,6 @@
appId: ${MAESTRO_APP_ID}
---
- runFlow: cleanUpRoomList.yaml
#- runFlow: searchRoomList.yaml
- runFlow: searchRoomList.yaml
- takeScreenshot: build/maestro/300-RoomList
- runFlow: timeline/timeline.yaml
- runFlow: roomContextMenu.yaml
@@ -3,5 +3,5 @@ appId: ${MAESTRO_APP_ID}
- takeScreenshot: build/maestro/520-Timeline
- tapOn: "Add attachment"
- tapOn: "Share location"
- tapOn: "Share my location"
- tapOn: "Share selected location"
- takeScreenshot: build/maestro/521-Timeline
+8 -61
View File
@@ -4,12 +4,9 @@ appId: ${MAESTRO_APP_ID}
id: "home_screen-settings"
- assertVisible: "Settings"
- takeScreenshot: build/maestro/600-Settings
## Account settings
- tapOn:
id: "settings-user_profile"
- assertVisible: "Edit profile"
- takeScreenshot: build/maestro/610-AccountEditProfile
text: "Analytics"
- assertVisible: "Share analytics data"
- back
- tapOn:
@@ -17,27 +14,16 @@ appId: ${MAESTRO_APP_ID}
- assertVisible: "Enable notifications on this device"
- back
- tapOn:
text: "Encryption"
- assertVisible: "Key storage"
- back
- tapOn:
text: "Moderation & safety"
- assertVisible: "Your appearance"
- back
- tapOn:
text: "Report a problem"
- assertVisible: "Report a problem"
- back
## App settings section
- assertVisible: "Appearance"
- tapOn:
text: "Media upload quality"
- assertVisible: "Media upload quality"
text: "About"
- assertVisible: "Copyright"
- assertVisible: "Acceptable use policy"
- assertVisible: "Privacy policy"
- back
- tapOn:
@@ -46,53 +32,14 @@ appId: ${MAESTRO_APP_ID}
- hideKeyboard
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "Location sharing"
- tapOn:
text: "Location sharing"
- assertVisible: "Live location updates"
text: "Advanced settings"
- assertVisible: "View source"
- back
## General section
- scrollUntilVisible:
direction: DOWN
element:
text: "Analytics"
- tapOn:
text: "Analytics"
- assertVisible: "Share analytics data"
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "Labs"
- tapOn:
text: "Labs"
- assertVisible: "Feeling experimental?"
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "About"
- tapOn:
text: "About"
- assertVisible: "Copyright"
- assertVisible: "Acceptable use policy"
- assertVisible: "Privacy policy"
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "Developer options"
- tapOn:
text: "Developer options"
- assertVisible: "Feature flags"
- assertVisible: "View source"
- back
- back
+3 -5
View File
@@ -37,9 +37,8 @@ PRs must meet these rules.
- Create previews for **all main states** of a Composable.
- Use `@PreviewsDayNight` for consistency.
- Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states. Always add new states to existing providers at the end of the sequence.
- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states.
- Wrap previews in `ElementPreview { ... }`.
- When writing tests, never try to record the screenshots, the CI will do it.
---
@@ -50,10 +49,9 @@ PRs must meet these rules.
- Title = changelog entry — descriptive, no "Fixes #…".
- Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette).
- Screenshots/videos for visual changes.
- 500 additions max into production code. Test code can be larger — split large changes.
- 500 additions max — split large changes.
- Commits need a title and description; no tiny or massive commits.
- No history rewrites.
- When creating a pull request and if changes contain new or update(s) of the Composable Previews, add the label "Record-Screenshots" to the PR so that the CI will record the screenshots.
---
@@ -90,7 +88,7 @@ We use [Appyx](https://bumble-tech.github.io/appyx/) for navigation and [Molecul
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
| `FooState.kt` | Data class representing the immutable UI state. |
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
| `FooStatePreviewParam.kt` | Provides sample states for Previews and Screenshot tests. |
| `FooStateProvider.kt` | Provides sample states for Previews and Screenshot tests. |
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
---
-634
View File
@@ -1,637 +1,3 @@
Changes in Element X v26.09.4
=============================
## Hotfix version:
Fixes an issue with the room list not displaying all activity from unread messages.
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.09.3...v26.09.4
Changes in Element X v26.09.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.09.3 -->
## What's Changed
### ✨ Features
* Integrate the new timeline renderer behind a feature flag by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7722
* Add headers (`h1-h6`) tag support for the new timeline renderer by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7754
### 🙌 Improvements
* Add smooth animation to voice messages play progress by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7698
* Add linkification to the new timeline renderer by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7731
### 🐛 Bugfixes
* Make global search use a `Joined` filter for room results by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7695
* Fix flaky test in `TimelinePresenterTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7702
* Close the threaded timelines created to create a poll, share a location or send media by @bmarty in https://github.com/element-hq/element-x-android/pull/7705
* Start sync manually after completing the auth flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7734
* Fix forwarding an event not working in threads by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7743
* Fix blank screen at launch when the homeserver expires the sliding sync session by @bmarty in https://github.com/element-hq/element-x-android/pull/7692
* Fix unread notification count in room list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7749
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7699
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7736
### 🧱 Build
* Reduce parallel JVMs spawned for tests by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7701
* Replace git flow with regular git commands in the release script V2 by @bmarty in https://github.com/element-hq/element-x-android/pull/7707
* Remove Roborazzi, use Paparazzi for Compound by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7735
* Fix regex for invalid placeholders by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7738
* Add a script to install and launch an APK given a PR URL by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7752
### 🚧 In development 🚧
* New timeline event renderer: add new modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7721
* Add recent search results to global search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7742
### Dependency upgrades
* Update dependency io.sentry:sentry-android to v8.55.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7685
* Update coil to v3.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7693
* Update kotlinpoet to v2.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7709
* Update plugin sonarqube to v7.5.0.8588 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7671
* Update roborazzi to v1.74.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7712
* Update dependency io.element.android:element-call-embedded to v0.26.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7715
* Use patched Konsist version (forked, 0.18.0) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7719
* Update dependency net.zetetic:sqlcipher-android to v4.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7716
* Update metro to v1.4.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7711
* Update actions/setup-java action to v6.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7728
* Update dependency androidx.compose:compose-bom to v2026.09.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7730
* Update dependency androidx.sqlite:sqlite-ktx to v2.7.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7729
* Update dependencyAnalysis to v3.19.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7755
* Update media3 to v1.11.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7756
* Upgrade SDK bindings to `v26.09.26` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7780
### Others
* Let the user give a reason when removing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7538
* Iterate on client creation by @bmarty in https://github.com/element-hq/element-x-android/pull/7591
* Iteration on Acount provider by @bmarty in https://github.com/element-hq/element-x-android/pull/7703
* Do not report detekt LargeClass issues on test classes by @bmarty in https://github.com/element-hq/element-x-android/pull/7713
* Let the user give an optional reason when removing a message by @bmarty in https://github.com/element-hq/element-x-android/pull/7679
* Make NotificationResultProcessor.emit wait until the batch is processed by @Johennes in https://github.com/element-hq/element-x-android/pull/7640
* Settings iteration: move user status and update multi-account section by @bmarty in https://github.com/element-hq/element-x-android/pull/7741
* Update remove message confirmation dialog. by @bmarty in https://github.com/element-hq/element-x-android/pull/7757
* Add a script to end all the sessions of a Matrix account using MAS by @bmarty in https://github.com/element-hq/element-x-android/pull/7762
* Settings iteration: split account settings and application settings. by @bmarty in https://github.com/element-hq/element-x-android/pull/7750
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.09.2...v26.09.3
Changes in Element X v26.09.2
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.09.2 -->
## What's Changed
### 🙌 Improvements
* Keep the push foreground service alive while notifications are fetched by @Johennes in https://github.com/element-hq/element-x-android/pull/7639
* Stop clearing the remote config when clearing the cache by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7662
### 🐛 Bugfixes
* Fix fetched remote config being cached with the wrong key by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7626
* Make sure `enforce_element_pro` is taken into account in FOSS by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7631
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7628
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7657
### 🧱 Build
* Allow retrying finding the release assets in release script by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7619
* Improve detection of invalid placeholders in localazy strings by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7624
* Add `GRADLE_OPTS` for `sync-localazy.yml` flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7629
* Clean up ProGuard keep rules files by @bmarty in https://github.com/element-hq/element-x-android/pull/7647
* Replace Danger with actions/github-script by @bmarty in https://github.com/element-hq/element-x-android/pull/7649
* Remove Showkase and its in-app component browser by @bmarty in https://github.com/element-hq/element-x-android/pull/7678
* Improve Renovate configuration to fix issues with Maven Central by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7677
### Dependency upgrades
* Update dependency com.posthog:posthog-android to v3.60.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7616
* Update dependency org.jsoup:jsoup to v1.23.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7620
* Update dependency io.element.android:element-call-embedded to v0.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7622
* Update dependencyAnalysis to v3.19.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7623
* Update wysiwyg to v2.42.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7627
* Update android.gradle.plugin to v9.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7603
* Update actions/setup-java action to v6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7614
* Update danger/danger-js action to v14 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7602
* Update camera to v1.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7632
* Update coil to v3.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7630
* Update dependency io.sentry:sentry-android to v8.54.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7636
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7650
* Bump SDK bindings to `v26.09.3` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7637
* Update zizmorcore/zizmor-action action to v0.6.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7658
* Update coil to v3.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7664
* Update dependency org.matrix.rustcomponents:sdk-android to v26.09.08 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7668
* Update dependency org.matrix.rustcomponents:sdk-android to v26.09.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7683
### 💼 Element Pro
* Fix remote config getting refreshed with `Origin.Cache` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7666
### Others
* Move knock and gallery to labs by @bmarty in https://github.com/element-hq/element-x-android/pull/7625
* Say so when this session has no secrets to verify another one with by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7551
* Expose the timeline item long press and swipe to reply as TalkBack actions by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7458
* Bump PR priority for our dependencies in Renovate by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7642
* Import string from Localazy and remove temporary file. by @bmarty in https://github.com/element-hq/element-x-android/pull/7645
* Revert "Say so when this session has no secrets to verify another one with" by @bmarty in https://github.com/element-hq/element-x-android/pull/7644
* Maestro (local): queue pending runs in the maestro-test concurrency group instead of cancelling them by @andreBurnt in https://github.com/element-hq/element-x-android/pull/7656
* Maestro (local): skip the lane on pull requests from forks by @andreBurnt in https://github.com/element-hq/element-x-android/pull/7655
* Update maplibre compose to 0.15 by @ganfra in https://github.com/element-hq/element-x-android/pull/7663
* Try improving renovate issues with Maven Central rate limiting by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7667
## New Contributors
* @andreBurnt made their first contribution in https://github.com/element-hq/element-x-android/pull/7656
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.09.1...v26.09.2
Changes in Element X v26.09.1
=============================
## What's Changed
### ✨ Features
* Support creating polls with multiple answers and send response with multiple answers by @bmarty in https://github.com/element-hq/element-x-android/pull/7613
### 🙌 Improvements
* Only track read receipts on message-like events by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7361
* Stop rescanning message bodies for emoji only on every recomposition by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7531
### 🐛 Bugfixes
* Ensure that noisy notifications are still noisy when the app has a PIN code. by @bmarty in https://github.com/element-hq/element-x-android/pull/7584
* Ensure that sessionVerifiedStatus is updated when the crypto is ready. by @bmarty in https://github.com/element-hq/element-x-android/pull/7604
* Fix invalid placeholders in translations by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7612
### 🧱 Build
* Do not open the editor when finishing the release by @bmarty in https://github.com/element-hq/element-x-android/pull/7583
* [release] Check that there is no unmerged PR with the label "Z-NextRelease", else exit by @bmarty in https://github.com/element-hq/element-x-android/pull/7606
### Dependency upgrades
* Update dependency net.zetetic:sqlcipher-android to v4.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7552
* Update dependency com.google.firebase:firebase-bom to v34.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7581
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7535
* Update dependency com.google.guava:guava to v33.7.1-android by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7534
* Update dependency org.unifiedpush.android:connector to v3.3.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7586
* Update dependency org.unifiedpush.android:connector to v3.3.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7595
* Update roborazzi to v1.73.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7618
### 💼 Element Pro
* Implement a hook allowing enterprise modules to perform actions on startup by @richvdh in https://github.com/element-hq/element-x-android/pull/7585
### Others
* Don't hide the thread of a deleted message inside the removed messages group by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7571
* Name a sender the timeline has no profile for from the room member list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7539
* Offer conversations in the system share sheet by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7574
* Rank the mention suggestions by how well they match what was typed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7536
* Log the SDK git Sha at application startup by @bmarty in https://github.com/element-hq/element-x-android/pull/7589
* Limit usage of nullable callback. by @bmarty in https://github.com/element-hq/element-x-android/pull/7588
* Fix crash due to JSON.stringify on a cirular structure in injectes js. by @toger5 in https://github.com/element-hq/element-x-android/pull/7600
* Support multi-select polls by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6477
* Remove ExternalSession, fallback login with Element Web is gone. by @bmarty in https://github.com/element-hq/element-x-android/pull/7608
* Fall back to the message body when the formatted body has nothing to render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7523
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.4...v26.09.1
Changes in Element X v26.08.4
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.4 -->
## What's Changed
### 🧱 Build
* Add releaseV2.sh, a release script automating the GitHub release by @bmarty in https://github.com/element-hq/element-x-android/pull/7579
* Update submodule ref. by @bmarty in https://github.com/element-hq/element-x-android/pull/7582
### Others
* Keep the formatting of a caption that is being edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7572
* Say when the server cannot be reached instead of showing nothing by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7567
* Keep saving files to Downloads once 32 files share the same name by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7344
* Fix removed messages hiding its thread on deletion by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7480
* Drop the sender's name from the room list preview of two-person rooms by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7562
* Document how the Element Call base URL developer option is used by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7546
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.3...v26.08.4
Changes in Element X v26.08.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.3 -->
## What's Changed
### ✨ Features
* Let users enter their full Matrix user ID on the account provider screen by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7500
* Enable user status feature and hide status text in the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/7517
### 🐛 Bugfixes
* Fix cold start with a mobile deep link by @bmarty in https://github.com/element-hq/element-x-android/pull/7488
* Make the account provider screen scrollable so its description is not clipped by the keyboard by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7521
### 🧱 Build
* Update Gradle Wrapper from 9.7.0 to 9.7.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7485
* Fix flaky test in `DefaultVoiceMessageComposerPresenterTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7487
* Fix warning about deprecated Gradle `keepRules.file` API by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7498
* Update Enterprise Copyright in idea settings, so that new files get an expected header. by @bmarty in https://github.com/element-hq/element-x-android/pull/7501
* Remove workaround for Renovate. by @bmarty in https://github.com/element-hq/element-x-android/pull/7502
### 🚧 In development 🚧
* Make PIP react to camera orientation changes in the other participant's device by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6951
* Feature: global search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7410
### Dependency upgrades
* Update dependency org.maplibre.gl:android-sdk to v13.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7486
* Update dependency androidx.webkit:webkit to v1.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7484
* Update dependency io.sentry:sentry-android to v8.53.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7481
* Update dependency io.element.android:element-call-embedded to v0.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7474
* Update roborazzi to v1.72.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7489
* Update metro to v1.4.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7493
* Update dependency com.squareup.okhttp3:okhttp-bom to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7512
* Update dependency com.google.guava:guava to v33.7.0-android by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7516
* Update sdk to version 26.08.25 by @bmarty in https://github.com/element-hq/element-x-android/pull/7550
### 💼 Element Pro
* Add `ClientEnterpriseHook` and `ClientBuilderEnterpriseHook` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7495
* Remove dependency on enterprise module. by @bmarty in https://github.com/element-hq/element-x-android/pull/7505
### Others
* Show the right image when opening one that has not finished sending by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7359
* Move strings to the login module by @bmarty in https://github.com/element-hq/element-x-android/pull/7494
* Allow empty problem description if the GitHub issue number is present for rageshakes by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7497
* Apply the system theme when switching back to it from a forced light or dark theme by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7453
* Show dialog with reason when user clicks on a message with a failure by @bmarty in https://github.com/element-hq/element-x-android/pull/7490
* Keep a focused timeline focused until the event has been rendered by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7463
* Do not render in-chat key verification requests in the timeline by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7443
* Share a file whose mime type is not one of the supported ones by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7435
* Show reactions in the pinned messages list by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6676
* Send a concrete mime type for media shared with a wildcard one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7449
* Rename module and ensure that naming convention is followed by @bmarty in https://github.com/element-hq/element-x-android/pull/7514
* Fix lint issue, ensure that PiP methods are not called on old devices not supporting them. by @bmarty in https://github.com/element-hq/element-x-android/pull/7515
* Rename PreviewParameterProvider subclasses to the PreviewParam suffix by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7468
* Update Ref. by @bmarty in https://github.com/element-hq/element-x-android/pull/7518
* Remove the reference to the decommissioned OIDC playground by @sandhose in https://github.com/element-hq/element-x-android/pull/7519
* Hide the encryption badge when key storage is set up on the server by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7447
* Rename remaining events sealed interface by @bmarty in https://github.com/element-hq/element-x-android/pull/7533
* Stop waiting on a verification request the other session never accepts by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7457
* Point the proxy debugging doc at the method that builds the clients by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7543
* Show the build date in the screenshot gallery footer by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7544
* Run the database migrations again when a database is upgraded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7570
* Update fonts on the headings by @bmarty in https://github.com/element-hq/element-x-android/pull/7520
* Add a "Push rules" entry in the developer options by @bmarty in https://github.com/element-hq/element-x-android/pull/7577
## New Contributors
* @sandhose made their first contribution in https://github.com/element-hq/element-x-android/pull/7519
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.2...v26.08.3
Changes in Element X v26.08.2
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.2 -->
**Note: this release in an internal release, it will not be pushed to the stores.**
## What's Changed
### ✨ Features
* Remove the legacy web-based account creation for matrix.org by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7412
### 🙌 Improvements
* Move the custom tile server URL config to the Element wellknown content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7416
* Explain a rate limiting push gateway in the notification troubleshooter by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7448
* Move the `:libraries:wellknown` logic into `enterprise` modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7429
* Refresh the room members when a mention is started by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7438
* Close the invite screen once the invite has been declined by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7440
* Use Profiles sliding sync extension check for own profile subscription by @ganfra in https://github.com/element-hq/element-x-android/pull/7403
* Simplify choosing the account provider during sign in, with autocomplete of previously used ones by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7394
### 🐛 Bugfixes
* Fix wrong TextComposer placeholder displayed when in a thread. by @bmarty in https://github.com/element-hq/element-x-android/pull/7413
* Fix bug reports failing to send after the app has crashed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7395
* Fix outgoing session verification stuck with incomplete recovery setup by @hardcore-sushi in https://github.com/element-hq/element-x-android/pull/7433
* Keep transparency when an image is shared into the app with an imprecise mime type by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7452
* Don't restart the sync service if the client is shutting down by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7472
* Normalize the applicationName used in user agent by @bmarty in https://github.com/element-hq/element-x-android/pull/7477
* Keep playing a voice message when another app plays a short sound by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7454
* Keep the send button when the composer is emptied while editing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7456
* Do not open the room again when the app is resumed from the recents list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7436
* Stop waiting for a room alias that the remote server never resolves by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7446
### 🧱 Build
* build-rust-sdk: print the command we run by @richvdh in https://github.com/element-hq/element-x-android/pull/7407
* Remove license check on Pro modules. by @bmarty in https://github.com/element-hq/element-x-android/pull/7418
* Improve build time config and update the Firebase configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/7473
* Make the PUSHER_APP_ID configurable at build time. by @bmarty in https://github.com/element-hq/element-x-android/pull/7478
### 📄 Documentation
* Add KDoc to all the interfaces of the api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/7426
### 🚧 In development 🚧
* [LinkNewDevice] Ensure the notice screen is also protected by the timeout. by @bmarty in https://github.com/element-hq/element-x-android/pull/7428
### Dependency upgrades
* Update kotlin to v2.3.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7396
* Update plugin sonarqube to v7.4.0.8496 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7415
* Update metro to v1.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7421
* Update roborazzi to v1.71.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7423
* Update dependency io.sentry:sentry-android to v8.52.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7414
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7420
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7431
* Update dependency org.maplibre.compose:maplibre-compose to v0.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7432
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.17 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7460
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7479
* Update media3 to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7427
* Update dependency androidx.compose:compose-bom to v2026.08.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7483
### 💼 Element Pro
* Add hooks to allow providing additional feature flags in EX pro by @richvdh in https://github.com/element-hq/element-x-android/pull/7419
* Use the right base URL for the ESS config/well known endpoints by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7469
### Others
* Do not display pinned events that the client cannot render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7379
* Hide the forward action on live location shares by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7343
* Do not fail the screenshot recording job when there is nothing to commit by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7355
* Show the account a UnifiedPush registration belongs to in the distributor by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7345
* Stop repeating messages that are already shown in a room notification by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7370
* Turn a matrix: URI in a message into a pill by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7354
* Send a message with Enter on a physical keyboard, Shift+Enter for a new line by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7353
* Do not group timeline messages sent more than five minutes apart by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7349
* Linkify URLs whose host is a bracketed IPv6 address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7347
* Add missing tests for `RustTemporaryMatrixClient` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7417
* Revert "Keep manually revealed media visible when re-entering a room" by @bmarty in https://github.com/element-hq/element-x-android/pull/7424
* Fix outdated documentation. by @bmarty in https://github.com/element-hq/element-x-android/pull/7430
* Sync strings - New language supported: Arabic by @bmarty in https://github.com/element-hq/element-x-android/pull/7461
* Keep the focused login field visible above the keyboard by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7455
* Relax image compression so photos and screenshots keep more detail by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7451
* Open the user profile of a user id that contains a slash by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7445
* Remove the F-Droid dex patching step from the release script by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7442
* Convert markdown to HTML when a caption is edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7441
* Exclude carriage returns from the login and password fields by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7439
* Show an error when the room to leave cannot be found by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7437
* Give the room name in the timeline top bar a 48dp tap target by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7450
* Model the fake voice recorder on a cancellable recording job by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7466
## New Contributors
* @hardcore-sushi made their first contribution in https://github.com/element-hq/element-x-android/pull/7433
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.1...v26.08.2
Changes in Element X v26.08.1
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.1 -->
## What's Changed
### 🙌 Improvements
* Render push notifications for live location share starts (MSC4505) by @ara4n in https://github.com/element-hq/element-x-android/pull/7203
* [Task] Show active call participants in the people list #7199 by @Eklavya47 in https://github.com/element-hq/element-x-android/pull/7300
### 🐛 Bugfixes
* Fix overlap issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/7331
* Announce checked/unchecked state for user rows in invite screen by @bmarty in https://github.com/element-hq/element-x-android/pull/7136
* Display 'add reactions' menu in the threads screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7388
* Expand the emoji picker to full height when the keyboard is shown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7382
* UI fixes by @bmarty in https://github.com/element-hq/element-x-android/pull/7409
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7389
### 🧱 Build
* Don't check enterprise previews in `KonsistPreviewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7297
* CI: split unit tests, screenshots and coverage in `tests.yml` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7296
* Update Gradle Wrapper from 9.6.1 to 9.7.0 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7335
### 🚧 In development 🚧
* QR code login iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6719
* User status last pieces by @ganfra in https://github.com/element-hq/element-x-android/pull/7328
* [Link new device] Add timeout by @bmarty in https://github.com/element-hq/element-x-android/pull/7338
### Dependency upgrades
* Update dependency org.maplibre.compose:maplibre-compose to v0.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7291
* Update android.gradle.plugin to v9.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7248
* Update dependency io.sentry:sentry-android to v8.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7298
* Update zizmorcore/zizmor-action action to v0.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7306
* Update android.gradle.plugin to v9.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7304
* Update dependency com.posthog:posthog-android to v3.56.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7311
* Update dependencyAnalysis to v3.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7318
* Update actions/stale action to v11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7317
* Update dependency io.github.zxing-cpp:android to v3.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7322
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.05 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7324
* Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7329
* Update reactivecircus/android-emulator-runner action to v2.38.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7189
* Update dependency io.sentry:sentry-android to v8.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7301
* Update dependency org.maplibre.gl:android-sdk to v13.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7305
* Update dependency io.element.android:element-call-embedded to v0.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7323
* Update metro to v1.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7334
* Update dependency com.google.firebase:firebase-bom to v34.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7333
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7175
* Update dependency org.jsoup:jsoup to v1.23.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7332
* Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7330
* Update zizmorcore/zizmor-action action to v0.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7383
* Update actions/setup-java action to v5.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7339
* Update gradle/actions action to v6.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7384
* Update plugin dependencycheck to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7386
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7402
### 💼 Element Pro
* Add support for checking the ESS config in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7302
### Others
* Add extra vertical padding to `ReplyToInvalidContent` and `ReplyToNotFoundContent` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7288
* Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7249
* Bump enterprise module to the latest commit by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7307
* Bump enterprise module to the latest commit by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7309
* Move deviceId and git details to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/7241
* Show a Message not found placeholder when a replied to message cannot be loaded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7376
* Update in reply to message when message is not found. by @bmarty in https://github.com/element-hq/element-x-android/pull/7387
* Add default `OAUTH_CLIENT_URL_PATH` alternative by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7385
* Fix the feature module template writing the impl build script into the api module by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7374
* Let users enter their full Matrix user ID when searching for a homeserver by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7375
* Keep manually revealed media visible when re-entering a room by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7378
* Copy a blocked user's Matrix ID by long pressing them by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7372
* Do not draw the media loading progress bar under the status bar by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7366
* Do not notify about an event sent by another account on the same device by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7360
* Keep the opened image on screen when the media gallery fails to load by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7362
* Do not linkify the server part of a fediverse handle as an email address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7356
* Expose global account data get and set on MatrixClient by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7373
* Show the alias or the direct message user's Matrix ID in the room picker by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7371
* Use a dedicated string for the Mute room notification mode by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7367
* Keep playing a voice message when the screen turns off by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7365
* Let a voice message without a waveform be seeked by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7363
* Ignore a second voice recording start while one is already running by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7346
* Do not offer to start a call in a DM with no other member by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7352
* Do not notify about a redaction whose target event is unknown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7350
* Ignore a room list filter that is incompatible with an already selected one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7342
* Stop a right to left sender name from reordering a room list preview by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7348
* Do not append the application MapTiler key to a custom map style url by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7340
* Hide space children you have left that can only be joined by invitation by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7369
* Never re-encode a video at a higher bitrate than the source by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7351
* Display push notifications for knocks by @ara4n in https://github.com/element-hq/element-x-android/pull/7207
## New Contributors
* @hayaksi1 made their first contribution in https://github.com/element-hq/element-x-android/pull/7249
* @ara4n made their first contribution in https://github.com/element-hq/element-x-android/pull/7203
* @Eklavya47 made their first contribution in https://github.com/element-hq/element-x-android/pull/7300
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.0...v26.08.1
Changes in Element X v26.08.0
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.0 -->
## What's Changed
### ✨ Features
* feat(call): Active call timeline rendering by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/7051
### 🙌 Improvements
* Announce expanded/collapsed state on the timeline group header by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7226
* Allow adding extra elements to the developer settings from external modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7230
* Update keyboard visibility to track hasFocus by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7284
### 🐛 Bugfixes
* Live location sharing timeout by @ganfra in https://github.com/element-hq/element-x-android/pull/7184
* Fix logging via QR code when using MDM-configured homeserver by @mxandreas in https://github.com/element-hq/element-x-android/pull/7228
* Fix #7181: Remove back arrow from device verified screen and use 'Done' text by @thirumani-vihaan in https://github.com/element-hq/element-x-android/pull/7222
* Fix padding for captions of media in 'not found' state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7247
* Fix banned mime type errors in `DefaultContentScannerService` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7255
* Fix `ContentErrorView` not calculating its text size synchronously by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7281
* Fix media viewer for medias not related to events displaying invalid state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7280
* Try making `HeaderFooterView` more robust by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7267
* Fix insets in several screens by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7285
### ⚠️ API Changes
* Add a password strength estimator wrapping the SDK zxcvbn API by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7186
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7277
### 🧱 Build
* CI: fix 'Update Gradle Wrapper' by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7238
* Update Gradle Wrapper from 9.5.1 to 9.6.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7240
* Localazy: improve search, parallelize checks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7261
* Run `Konsist` tests sequentially by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7293
### 📄 Documentation
* Group Element Pro changes into a dedicated release notes section by @manuroe in https://github.com/element-hq/element-x-android/pull/7236
### 🚧 In development 🚧
* Plug emoji picker for custom user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7257
* WIP : display user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7232
### Dependency upgrades
* Update roborazzi to v1.67.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7188
* Update dependency io.sentry:sentry-android to v8.48.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7217
* Update dependency com.google.crypto.tink:tink-android to v1.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7229
* Update kotlin to v2.3.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7234
* Update dependency net.zetetic:sqlcipher-android to v4.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7218
* Update dependency com.google.firebase:firebase-bom to v34.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7237
* Update dependencyAnalysis to v3.16.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7212
* Update actions/setup-java action to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7214
* Update dependency io.element.android:element-call-embedded to v0.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7161
* Update kotlin to v2.4.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7244
* Update roborazzi to v1.68.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7245
* Update metro to v1.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7243
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7258
* Update zizmorcore/zizmor-action action to v0.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7256
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7262
* Update dependency io.sentry:sentry-android to v8.49.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7263
* Update dependencyAnalysis to v3.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7265
* Update actions/setup-java action to v5.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7268
* Update dependency org.jetbrains.kotlinx:kover-gradle-plugin to v0.9.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7273
* Update actions/setup-python action to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7278
* Update roborazzi to v1.70.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7274
* Update actions/checkout action to v7.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7282
* Import Compound tokens from `v10.2.3` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7272
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7287
* Update dependency org.maplibre.gl:android-sdk to v13.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7290
### 💼 Element Pro
* Content scanner (Element Pro): scan timeline events for unsafe content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7159
* Add content scanner support to media gallery and viewer (Element Pro) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7233
* Enforce HS disabling E2EE in DM creation too (Element Pro) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7292
* Allow adding a path suffix to the OAuth client URI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7286
### Others
* [A11y] Improve rendering on focused UI items by @bmarty in https://github.com/element-hq/element-x-android/pull/7182
* Add a button role to clickable icons and text on the login/onboarding screens by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7225
* Expose formatting toggle state to TalkBack by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7227
* Add warning to the 'Scan QR code' screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7264
* Refactor SearchBarResultState.Initial/NoResultsFound to data objects by @ganfra in https://github.com/element-hq/element-x-android/pull/7266
* Remove no longer temporary strings by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7260
* Use the right insets in the home screen UI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7271
* Make settings section separators visible in black theme by @mvanhorn in https://github.com/element-hq/element-x-android/pull/7039
## New Contributors
* @thirumani-vihaan made their first contribution in https://github.com/element-hq/element-x-android/pull/7222
* @mvanhorn made their first contribution in https://github.com/element-hq/element-x-android/pull/7039
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.07.1...v26.08.0
Changes in Element X v26.07.1
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.07.1 -->
## What's Changed
### ✨ Features
* Scroll to unread messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7024
### 🙌 Improvements
* Collapse runs of deleted messages in the timeline by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7195
* Allow homeserver to disable encryption in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7198
### 🐛 Bugfixes
* Fix jump-to-unread requiring two taps for an out-of-window read marker by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6943
* Add content description to video preview for screen readers by @bmarty in https://github.com/element-hq/element-x-android/pull/7137
* Fix accessibility for message composer input and read receipts button by @bmarty in https://github.com/element-hq/element-x-android/pull/7135
* Add accessibility label to search input by @bmarty in https://github.com/element-hq/element-x-android/pull/7133
* Fix timeline reading order distorted when TalkBack is active by @bmarty in https://github.com/element-hq/element-x-android/pull/7138
* Add programmatic label and focus indicator to PIN entry field by @bmarty in https://github.com/element-hq/element-x-android/pull/7132
* Fix timelines not loading after marking a room as read from notification when app was killed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7202
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7192
### 🧱 Build
* Bump the default Gradle daemon heap to match what CI already uses by @PolynomialDivision in https://github.com/element-hq/element-x-android/pull/7152
* Update screenshots by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7155
* Update GitHub templates by @bmarty in https://github.com/element-hq/element-x-android/pull/7170
* Add check on merged manifest by @bmarty in https://github.com/element-hq/element-x-android/pull/7172
* Fix AAPT dump missing and reordering some permissions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7193
* Try fixing DM creation in Maestro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7204
### 🚧 In development 🚧
* Support gallery messages by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6519
* Support gallery messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7112
* Initial content scanner integration by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7139
* WIP : Set user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7094
### Dependency upgrades
* Update dependency io.sentry:sentry-android to v8.44.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7093
* Update dependency org.maplibre.gl:android-sdk to v13.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7129
* Update actions/setup-java action to v5.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7143
* Update danger/danger-js action to v13.0.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7142
* Update dependency io.sentry:sentry-android to v8.46.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7140
* Update dependencyAnalysis to v3.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7144
* Update plugin gms_google_services to v4.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7086
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha21 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7007
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7158
* Update metro to v1.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7168
* Update dependency io.sentry:sentry-android to v8.47.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7178
* Update dependency androidx.compose:compose-bom to v2026.06.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7174
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7180
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7201
* Update dependency io.github.zxing-cpp:android to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7206
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7194
* Update dependency androidx.sqlite:sqlite-ktx to v2.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7177
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.15 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7216
### Others
* Timeline: jump-to-unread FAB and mark-as-read shortcut by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6694
* Iterate on visual list by @bmarty in https://github.com/element-hq/element-x-android/pull/7141
* Fix wrong avatar shown in the thread list's top bar by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7044
* Update onboarding screen text style by @bmarty in https://github.com/element-hq/element-x-android/pull/7157
* Add full SVG support by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7020
* Add skin tone picker to the emoji picker by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6917
* Fix the message edit being lost when attaching media by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7090
* Cleanup AndroidManifest.xml files. by @bmarty in https://github.com/element-hq/element-x-android/pull/7171
* Handle local network permission - Android 17 by @ganfra in https://github.com/element-hq/element-x-android/pull/7165
* Add item count text as the placeholder for in reply to box for galleries by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7176
* Default to dark theme for EC by @toger5 in https://github.com/element-hq/element-x-android/pull/7162
## New Contributors
* @PolynomialDivision made their first contribution in https://github.com/element-hq/element-x-android/pull/7152
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.07.0...v26.07.1
Changes in Element X v26.07.0
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.07.0 -->
## What's Changed
### ✨ Features
* Add read all messages feature by @hghgrtut in https://github.com/element-hq/element-x-android/pull/7049
### 🐛 Bugfixes
* Detect RTL text in events and adjust the text rendering by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6994
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7071
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7115
### 🧱 Build
* Fix Sonar with AGP 9 (some workarounds) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7055
* Disable periodically generating the screenshot gallery by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7110
* Try a different workaround for flaky `ScanQrCodeViewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7109
### 🚧 In development 🚧
* Protect link new device by @bmarty in https://github.com/element-hq/element-x-android/pull/6992
### Dependency upgrades
* Update metro to v1.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7057
* Update gradle/actions action to v6.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7063
* Update roborazzi to v1.64.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7068
* Update rnkdsh/action-upload-diawi action to v1.5.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7067
* Update dependency io.element.android:element-call-embedded to v0.20.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7075
* Update dependency com.github.matrix-org:matrix-analytics-events to v0.37.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7079
* Update actions/setup-java action to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7088
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7052
* Update firebaseAppDistribution to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7096
* Update dependency com.google.firebase:firebase-bom to v34.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7089
* Update dependency com.google.crypto.tink:tink-android to v1.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7105
* Update actions/checkout action to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7098
* Update lifecycle to v2.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7097
* Update danger/danger-js action to v13.0.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7104
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7107
* Update dependency androidx.compose:compose-bom to v2026.06.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7095
* Update dependency io.element.android:element-call-embedded to v0.20.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7117
* Update zizmorcore/zizmor-action action to v0.5.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7116
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.30 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7127
* Update dependency com.posthog:posthog-android to v3.51.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7128
### Others
* Fix small code smell by @bmarty in https://github.com/element-hq/element-x-android/pull/7058
* Hide message notification content when the app is locked by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6902
## New Contributors
* @hghgrtut made their first contribution in https://github.com/element-hq/element-x-android/pull/7049
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.06.4...v26.07.0
Changes in Element X v26.06.4
=============================
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem 'danger'
-33
View File
@@ -6,39 +6,6 @@ top section matching the released version — the heading must have the exact
form `## Changes in <version> (<date>)` and the whole section must stay under
500 characters. See `.claude/skills/releasing-healthchat/SKILL.md`.
## Changes in 26.10.1 (2026-10-21)
🐛 Fehlerbehebungen
- Anrufe aus der Android-App lassen das iPhone wieder klingeln.
- Abgebrochene Anrufe werden auf Android wieder sofort beendet.
## Changes in 26.10.0 (2026-10-20)
🎉 Neue Funktionen
- Nachrichtensuche: Chats durchsuchen, auch in verschlüsselten Räumen.
- Umfragen mit Mehrfachauswahl.
🙌 Verbesserungen
- Basis aktualisiert auf Element X 26.10.0 mit zahlreichen Fehlerbehebungen.
- Einstellungen neu gegliedert in Konto- und App-Einstellungen.
## Changes in 26.08.0 (2026-08-06)
🎉 Neue Funktionen
- Mitgliederverzeichnis: Alle Mitglieder durchsuchen und direkt einen Chat starten.
- Profil bearbeiten: Angaben und Profilbild direkt in der App pflegen.
- Neue Mitglieder per E-Mail einladen.
- Statushinweise zeigen den Stand der Kontofreigabe.
🙌 Verbesserungen
- HealthChat-Links öffnen sich neu direkt in der App statt im Browser.
- Geteilte Profillinks verweisen neu auf HealthChat statt auf Element.
## Changes in 26.07.0 (2026-07-16)
🎉 Komplett neue App-Generation
@@ -14,16 +14,16 @@ import kotlin.reflect.KClass
* Adds Node to the specified component graph.
* Equivalent to the following declaration:
*
* ```
* @BindingContainer
* @ContributesTo(Scope::class)
* interface YourNodeBindingContainer {
* @Binds
* @IntoMap
* @NodeKey(YourNode::class)
* fun bindYourNodeAssistedFactory(factory: YourNodeAssistedFactory): AssistedNodeFactory<*>
* }
* ```
* abstract class YourNodeModule {
* @Binds
* @IntoMap
* @NodeKey(YourNode::class)
* abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*>
*}
*/
@Target(AnnotationTarget.CLASS)
annotation class ContributesNode(
+30 -48
View File
@@ -21,7 +21,6 @@ import extension.allFeaturesImpl
import extension.allLibrariesImpl
import extension.allServicesImpl
import extension.buildConfigFieldStr
import extension.environmentOrGradleValue
import extension.locales
import extension.setupDependencyInjection
import extension.testCommonDependencies
@@ -51,9 +50,6 @@ android {
// Keep abiFilter for the universalApk
ndk {
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
// HealthChat fork: ship the native symbol tables (Rust SDK, LiveKit) in the bundle so Play Console
// can symbolicate native crashes and ANRs. Play strips them from the APKs it serves.
debugSymbolLevel = "SYMBOL_TABLE"
}
// Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
@@ -92,14 +88,13 @@ android {
storePassword = "android"
}
register("nightly") {
keyAlias = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_KEYID", "signing.element.nightly.keyId")
keyPassword = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD", "signing.element.nightly.keyPassword")
storeFile = file(
environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_STOREFILE", "signing.element.nightly.storeFile")
?.ifBlank { null }
?: "./signature/nightly.keystore"
)
storePassword = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD", "signing.element.nightly.storePassword")
keyAlias = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYID")
?: project.property("signing.element.nightly.keyId") as? String?
keyPassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD")
?: project.property("signing.element.nightly.keyPassword") as? String?
storeFile = file("./signature/nightly.keystore")
storePassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD")
?: project.property("signing.element.nightly.storePassword") as? String?
}
// HealthChat Play Store upload key. The keystore is not in the repo:
// CI decodes it from the RELEASE_KEYSTORE_BASE64 secret (see
@@ -150,11 +145,24 @@ android {
optimization {
enable = true
keepRules {
// Equivalent of adding `getDefaultProguardFile("proguard-android-optimize.txt")` (this is the default value).
includeDefault = true
files.add(File(projectDir, "common-proguard-rules.pro"))
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
val flavorProguardFile = if (isEnterpriseBuild) {
// Custom rules for enterprise builds
File(projectDir, "enterprise-proguard-rules.pro")
} else {
// These default rules prevent the OSS app from being obfuscated
File(projectDir, "default-proguard-rules.pro")
}
if (flavorProguardFile.exists()) {
files.add(flavorProguardFile)
} else {
logger.warn("Proguard file ${flavorProguardFile.absolutePath} does not exist")
}
}
// Our custom keep rules are registered as `keepRules` source folders in the `androidComponents` block below,
// as the former `keepRules.files` DSL is deprecated since AGP 9.
}
}
@@ -239,30 +247,6 @@ androidComponents {
)
onVariants { variant ->
// Register the R8 keep rules source folders for optimized build types (release, nightly).
// Replaces the deprecated `optimization.keepRules.files` DSL (AGP 9+).
if (variant.buildType != "debug") {
variant.sources.keepRules?.let { keepRules ->
// Common rules, always applied.
keepRules.addStaticSourceDirectory("proguard/common")
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
val flavorProguardDir = if (isEnterpriseBuild) {
// Custom rules for enterprise builds
"../enterprise/proguard"
} else {
// Custom fules for FOSS builds
"proguard/foss"
}
if (File(projectDir, flavorProguardDir).exists()) {
keepRules.addStaticSourceDirectory(flavorProguardDir)
} else {
logger.warn("Proguard folder ${File(projectDir, flavorProguardDir).absolutePath} does not exist")
}
}
}
// Assigns a different version code for each output APK
// other than the universal APK.
variant.outputs.forEach { output ->
@@ -301,6 +285,7 @@ dependencies {
implementation(projects.features.migration.api)
implementation(projects.appnav)
implementation(projects.appconfig)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.compose)
if (ModulesConfig.pushProvidersConfig.includeFirebase) {
@@ -310,21 +295,22 @@ dependencies {
implementation(projects.libraries.pushproviders.unifiedpush)
}
// Google Play Services fused location backend. Discovered at runtime via ServiceLoader by the
// location feature.
"gplayImplementation"(libs.maplibre.compose.location.runtime.gms)
implementation(libs.appyx.core)
implementation(libs.androidx.splash)
implementation(libs.androidx.core)
implementation(libs.androidx.corektx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.startup)
implementation(libs.androidx.preference)
implementation(libs.coil)
implementation(platform(libs.network.okhttp.bom))
implementation(libs.network.okhttp.logging)
implementation(libs.serialization.json)
implementation(libs.matrix.emojibase.bindings)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
@@ -346,7 +332,6 @@ licensee {
allow("BSD-3-Clause")
allow("EPL-1.0")
allowUrl("https://opensource.org/license/bsd-3-clause")
allowUrl("https://opensource.org/license/bsd-2-clause")
allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/")
@@ -357,9 +342,6 @@ licensee {
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
// Ignore dependency that are not third-party licenses to us.
ignoreDependencies(groupId = "io.element.android")
// The rustls-platform-verifier dependency does not provide a license file in their maven repo, but there are MIT and Apache licenses in
// the source code repo: see https://github.com/rustls/rustls-platform-verifier
ignoreDependencies(groupId = "org.rustls", artifactId = "rustls-platform-verifier")
}
fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
+2
View File
@@ -0,0 +1,2 @@
# Don't obfuscate anything for non-enterprise builds
-dontobfuscate
@@ -1,9 +0,0 @@
# Don't obfuscate anything for non-enterprise builds
-dontobfuscate
# Strip PostHog's Firebase push token integration. We don't use it and it breaks F-Droid.
# Removing the contents of this method will mark the Firebase-related classes as unused,
# which will allow R8 to remove them from the final build.
-assumevalues class com.posthog.android.PostHogAndroidConfig {
boolean getCapturePushNotificationSubscriptions() return false;
}
+31 -21
View File
@@ -8,6 +8,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- To be able to install APK from the application -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".ElementXApplication"
android:allowBackup="false"
@@ -49,14 +52,13 @@
Using launchMode singleTask to avoid multiple instances of the Activity
when the app is already open. This is important for incoming share (see
https://github.com/element-hq/element-x-android/issues/4074) and for opening
the application from a permalink.
the application from a mobile.element.io link.
-->
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|uiMode"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true"
android:launchMode="singleTask"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.ElementX.Splash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@@ -70,7 +72,7 @@
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="open"
android:scheme="healthchat" />
android:scheme="elementx" />
</intent-filter>
<!--
OAuth redirection
@@ -84,9 +86,7 @@
<data android:scheme="@string/login_redirect_scheme" />
</intent-filter>
<!--
HealthChat web links
Note: hosts are listed one by one on purpose, a "*.healthchat.ch" wildcard would
also intercept the homeserver and authentication domains.
Element web links
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
@@ -95,12 +95,28 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- HealthChat web client permalinks, example: https://app.healthchat.ch/#/room/#room:healthchat.ch -->
<!-- Matching asset file: https://app.healthchat.ch/.well-known/assetlinks.json -->
<data android:host="app.healthchat.ch" />
<!-- HealthChat matrix.to deployment, example: https://to.healthchat.ch/#/#room:healthchat.ch -->
<!-- Matching asset file: https://to.healthchat.ch/.well-known/assetlinks.json -->
<data android:host="to.healthchat.ch" />
<!-- Note: we can't use "*.element.io" here because it'll intercept the "mas.element.io" domain too. -->
<!-- Matching asset file: https://app.element.io/.well-known/assetlinks.json -->
<data android:host="app.element.io" />
<!-- Matching asset file: https://develop.element.io/.well-known/assetlinks.json -->
<data android:host="develop.element.io" />
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
<data android:host="staging.element.io" />
</intent-filter>
<!--
Element mobile links
Example: https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Matching asset file: https://mobile.element.io/.well-known/assetlinks.json -->
<data android:host="mobile.element.io" />
<data android:path="/element/" />
</intent-filter>
<!--
matrix: links
@@ -114,7 +130,7 @@
<data android:scheme="matrix" />
</intent-filter>
<!--
links from the HealthChat matrix.to website, example: healthchat://room/%23room%3Ahealthchat.ch
links from matrix.to website
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -122,16 +138,10 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="healthchat" />
<data android:scheme="element" />
<data android:host="user" />
<data android:host="room" />
</intent-filter>
<!--
Conversation shortcuts offered in the Direct Share row of the system share sheet.
-->
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<!-- Incoming share simple -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -9,19 +9,14 @@
package io.element.android.x
import android.app.Application
import android.os.StrictMode
import androidx.compose.material3.ComposeMaterial3Flags.isAnchoredDraggableComponentsStrictOffsetCheckEnabled
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.startup.AppInitializer
import androidx.work.Configuration
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.createGraphFactory
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.SdkMetadata
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.x.di.AppGraph
import io.element.android.x.di.ApplicationBindings
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CacheCleanerInitializer
import io.element.android.x.initializer.CrashInitializer
@@ -34,37 +29,19 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config
.setWorkerFactory(MetroWorkerFactory(graph.workerProviders))
.build()
@Inject lateinit var sdkMetadata: SdkMetadata
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate() {
super.onCreate()
enableStrictMode()
AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java)
initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java)
}
bindings<ApplicationBindings>().inject(this)
logApplicationInfo(this, sdkMetadata.sdkGitSha)
logApplicationInfo(this)
// Disable the strict offset check for anchored draggable components, as it can cause issues with bottom sheets.
// Remove once https://issuetracker.google.com/issues/477038695 is fixed.
isAnchoredDraggableComponentsStrictOffsetCheckEnabled = false
}
private fun enableStrictMode() {
// Workaround to avoid NetworkOnMainThreadException when using the network on the main thread.
// This is a temporary solution to rustls doing the certificate verification using blocking network requests.
// It should be removed once the underlying issue is fixed.
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
@@ -61,26 +61,6 @@ class MainActivity : NodeActivity() {
setContent {
MainContent(appBindings)
}
val activity = this
appBindings.appStartupHooks().forEach {
lifecycleScope.launch {
it.onAppStartup(activity)
}
}
}
/**
* The pre-Android 12 way into picture-in-picture for a native call.
*
* Everything else about it is installed where the call is drawn, which knows whose call it is.
* This cannot be: an Activity override has no session in scope. From Android 12 the system is told
* up front that this Activity would like to shrink, and the call handles it without ever coming
* through here.
*/
override fun onUserLeaveHint() {
super.onUserLeaveHint()
appBindings.nativeCallPip().onUserLeaveHint(this)
}
@Composable
@@ -10,13 +10,10 @@ package io.element.android.x.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Multibinds
import io.element.android.features.callnative.api.NativeCallPip
import io.element.android.features.enterprise.api.AppStartupHook
import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.migration.api.MigrationEntryPoint
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -53,15 +50,5 @@ interface AppBindings {
fun buildMeta(): BuildMeta
/**
* For picture-in-picture. Only an Activity is told it is being left, and only the call knows
* whether that is worth shrinking for, so `MainActivity` needs this much of the native call and
* no more.
*/
fun nativeCallPip(): NativeCallPip
fun sentrySdkDsn(): SentrySdkDsn?
@Multibinds(allowEmpty = true)
fun appStartupHooks(): Set<AppStartupHook>
}
@@ -18,7 +18,7 @@ import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ApplicationConfig
import io.element.android.features.enterprise.api.IsEnterpriseBuild
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta
@@ -28,6 +28,8 @@ import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
import io.element.android.x.BuildConfig
import io.element.android.x.R
import kotlinx.coroutines.CoroutineName
@@ -38,7 +40,7 @@ import java.io.File
@BindingContainer
@ContributesTo(AppScope::class)
object AppBindingContainer {
object AppModule {
@Provides
@BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File {
@@ -74,18 +76,17 @@ object AppBindingContainer {
fun providesBuildMeta(
@ApplicationContext context: Context,
buildType: BuildType,
isEnterpriseBuild: IsEnterpriseBuild,
enterpriseService: EnterpriseService,
): BuildMeta {
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
val isEnterprise = isEnterpriseBuild()
return BuildMeta(
isDebuggable = BuildConfig.DEBUG,
buildType = buildType,
applicationName = applicationName,
productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
applicationId = BuildConfig.APPLICATION_ID,
isEnterpriseBuild = isEnterprise,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME,
@@ -111,7 +112,13 @@ object AppBindingContainer {
@Provides
@SingleIn(AppScope::class)
fun providesSnackbarDispatcher(): SnackbarDispatcher {
fun provideSnackbarDispatcher(): SnackbarDispatcher {
return SnackbarDispatcher()
}
@Provides
@SingleIn(AppScope::class)
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
return DefaultEmojibaseProvider(context)
}
}
@@ -1,17 +0,0 @@
/*
* 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.x.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import io.element.android.x.ElementXApplication
@ContributesTo(AppScope::class)
interface ApplicationBindings {
fun inject(application: ElementXApplication)
}
@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
@ContributesBinding(AppScope::class)
class DefaultSessionGraphFactory(
private val appGraph: AppGraph,
private val appGraph: AppGraph
) : SessionGraphFactory {
override fun create(client: MatrixClient): Any {
return appGraph.sessionGraphFactory.create(client)
@@ -10,22 +10,32 @@ package io.element.android.x.info
import android.content.Context
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.log.logger.wrapInBox
import io.element.android.x.BuildConfig
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun logApplicationInfo(context: Context, sdkGitSha: String) {
val appVersion = "${BuildConfig.VERSION_NAME} (${context.getVersionCodeFromManifest()}) - ${BuildConfig.BUILD_TYPE} / ${BuildConfig.FLAVOR}"
fun logApplicationInfo(context: Context) {
val appVersion = buildString {
append(BuildConfig.VERSION_NAME)
append(" (")
append(context.getVersionCodeFromManifest())
append(") - ")
append(BuildConfig.BUILD_TYPE)
append(" / ")
append(BuildConfig.FLAVOR)
}
// TODO Get SDK version somehow
val sdkVersion = "SDK VERSION (TODO)"
val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())
listOf(
" Application version: $appVersion",
" Element X: https://github.com/element-hq/element-x-android/commit/${BuildConfig.GIT_REVISION}",
" SDK : https://github.com/matrix-org/matrix-rust-sdk/commit/$sdkGitSha",
" Local time: $date",
)
.wrapInBox(minBoxInsideWidth = 80)
.forEach { Timber.d(it) }
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------")
Timber.d(" Application version: $appVersion")
Timber.d(" Git SHA: ${BuildConfig.GIT_REVISION}")
Timber.d(" SDK version: $sdkVersion")
Timber.d(" Local time: $date")
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------\n\n\n\n")
}
@@ -8,14 +8,11 @@
package io.element.android.x.initializer
import android.annotation.SuppressLint
import android.content.Context
import android.system.Os
import android.util.Log
import androidx.startup.Initializer
import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.x.di.AppBindings
@@ -27,15 +24,6 @@ private const val ELEMENT_X_TARGET = "elementx"
class PlatformInitializer : Initializer<Unit> {
override fun create(context: Context) {
// Init the SDK with the Android Context, so it can be used with rustls and other Android specific features
runCatchingExceptions {
org.matrix.rustsdk.Android.init(context)
}.onFailure {
// We can't log this to Timber since the logs aren't initialized yet, so we log it to Logcat instead
@SuppressLint("LogNotTimber")
Log.e("PlatformInitializer", "Failed to init the SDK with an Android Context. The app probably won't work properly.", it)
}
val appBindings = context.bindings<AppBindings>()
val tracingService = appBindings.tracingService()
val platformService = appBindings.platformService()
@@ -53,8 +41,6 @@ class PlatformInitializer : Initializer<Unit> {
sdkSentryDsn = appBindings.sentrySdkDsn()?.value?.takeIf { it.isNotBlank() },
)
bugReporter.setCurrentTracingLogLevel(logLevel.name)
// Init the tracing service with the configuration so we have logging enabled as soon as possible
platformService.init(tracingConfiguration)
// Also set env variable for rust back trace
Os.setenv("RUST_BACKTRACE", "1", true)
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="screen_onboarding_welcome_title">"Willkommen bei HealthChat"</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="screen_onboarding_welcome_title">"Bienvenue chez HealthChat"</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="screen_onboarding_welcome_title">"Benvenuto su HealthChat"</string>
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- HealthChat override of the Element onboarding title ("Be in your element").
Overridden here app-side instead of editing the Localazy-managed login
strings, so upstream merges stay clean. Only the main HealthChat locales
are overridden; other locales fall back to their Element translation. -->
<string name="screen_onboarding_welcome_title" tools:ignore="MissingTranslation">"Welcome to HealthChat"</string>
</resources>
-1
View File
@@ -1,6 +1,5 @@
<!-- File generated by importSupportedLocalesFromLocalazy.py, do not edit -->
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar"/>
<locale android:name="be"/>
<locale android:name="bg"/>
<locale android:name="ca"/>
@@ -30,384 +30,5 @@
<domain includeSubdomains="true">home</domain>
<domain includeSubdomains="true">lan</domain>
<domain includeSubdomains="true">localdomain</domain>
<!-- Domains below are imported from rustls-platform-verifier 0.2.0 by tools/dependencies/update_rustls_platform_verifier.py -->
<!-- They are needed to fetch the certificate revocation lists over cleartext -->
<domain includeSubdomains="true">abitabdv.crl.certum.pl</domain>
<domain includeSubdomains="true">abitabev.crl.certum.pl</domain>
<domain includeSubdomains="true">abitabov.crl.certum.pl</domain>
<domain includeSubdomains="true">alpiro.crl.sectigo.com</domain>
<domain includeSubdomains="true">anticdv.crl.certum.pl</domain>
<domain includeSubdomains="true">antrustovsslg2r30ca.crl.certum.pl</domain>
<domain includeSubdomains="true">autontrustprodvtlsg2r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">bitcert.crl.sectigo.com</domain>
<domain includeSubdomains="true">btdvtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">c.cf-b.ssl.com</domain>
<domain includeSubdomains="true">c.cf-i.ssl.com</domain>
<domain includeSubdomains="true">c.pki.goog</domain>
<domain includeSubdomains="true">ca1.agid.gov.it</domain>
<domain includeSubdomains="true">cdn.d-trust-cloudcrl.net</domain>
<domain includeSubdomains="true">cdp.disig.sk</domain>
<domain includeSubdomains="true">cdp.elektronicznypodpis.pl</domain>
<domain includeSubdomains="true">cdp.geotrust.com</domain>
<domain includeSubdomains="true">cdp.rapidssl.com</domain>
<domain includeSubdomains="true">cdp.thawte.com</domain>
<domain includeSubdomains="true">cdp1.disig.sk</domain>
<domain includeSubdomains="true">cdp1.pca.dfn.de</domain>
<domain includeSubdomains="true">cdpa.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpb.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpc.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpd.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpe.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpf.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpg.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdph.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpi.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">certcloud.crl.trust-provider.com</domain>
<domain includeSubdomains="true">certdata.crl.sectigo.com</domain>
<domain includeSubdomains="true">certera.crl.sectigo.com</domain>
<domain includeSubdomains="true">certificates.godaddy.com</domain>
<domain includeSubdomains="true">certum-crl.wt.trustasia.com</domain>
<domain includeSubdomains="true">certum.crl.sheca.com</domain>
<domain includeSubdomains="true">certumdvtlsg2e39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumdvtlsg2r39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumevtlsg2e39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumevtlsg2r39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumovtlsg2e39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumovtlsg2r39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcadveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcadvrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaeveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaevrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaoveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaovrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">cnssldvtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">cnsslovtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">crl-01.r1.ica.amazontrust.com</domain>
<domain includeSubdomains="true">crl-01.r1.ica.amznts.eu</domain>
<domain includeSubdomains="true">crl-c.emsign.com</domain>
<domain includeSubdomains="true">crl-pro.litessl.com</domain>
<domain includeSubdomains="true">crl.anf.es</domain>
<domain includeSubdomains="true">crl.apple.com</domain>
<domain includeSubdomains="true">crl.businessid.telesec.de</domain>
<domain includeSubdomains="true">crl.buypass.no</domain>
<domain includeSubdomains="true">crl.catrust.oemssl.cn</domain>
<domain includeSubdomains="true">crl.certigna.fr</domain>
<domain includeSubdomains="true">crl.certsign.ro</domain>
<domain includeSubdomains="true">crl.certum.pl</domain>
<domain includeSubdomains="true">crl.cfca.com.cn</domain>
<domain includeSubdomains="true">crl.cntrus.oemssl.cn</domain>
<domain includeSubdomains="true">crl.cnwebtrust.oemssl.cn</domain>
<domain includeSubdomains="true">crl.comodoca.com</domain>
<domain includeSubdomains="true">crl.crlocsp.cn</domain>
<domain includeSubdomains="true">crl.cybertrust.ne.jp</domain>
<domain includeSubdomains="true">crl.d-trust.net</domain>
<domain includeSubdomains="true">crl.dhimyotis.com</domain>
<domain includeSubdomains="true">crl.digicert.cn</domain>
<domain includeSubdomains="true">crl.digicert.eu</domain>
<domain includeSubdomains="true">crl.e-szigno.hu</domain>
<domain includeSubdomains="true">crl.e2m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m01.eu.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s14.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s15.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s16.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s17.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s18.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s19.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s20.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s21.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m01.eu.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s22.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s23.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s24.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s25.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s26.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s27.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s28.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s29.amazontrust.com</domain>
<domain includeSubdomains="true">crl.emsign.com</domain>
<domain includeSubdomains="true">crl.ensuredca.com</domain>
<domain includeSubdomains="true">crl.firmaprofesional.com</domain>
<domain includeSubdomains="true">crl.gdca.com.cn</domain>
<domain includeSubdomains="true">crl.global.sheca.com</domain>
<domain includeSubdomains="true">crl.globalsign.com</domain>
<domain includeSubdomains="true">crl.globalsign.net</domain>
<domain includeSubdomains="true">crl.godaddy.com</domain>
<domain includeSubdomains="true">crl.harica.gr</domain>
<domain includeSubdomains="true">crl.izenpe.com</domain>
<domain includeSubdomains="true">crl.litessl.com</domain>
<domain includeSubdomains="true">crl.netsolssl.com</domain>
<domain includeSubdomains="true">crl.oem.trustca.net</domain>
<domain includeSubdomains="true">crl.pcsp.telesec.de</domain>
<domain includeSubdomains="true">crl.pki.goog</domain>
<domain includeSubdomains="true">crl.quovadisglobal.com</domain>
<domain includeSubdomains="true">crl.r1001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m01.eu.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s18.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s19.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s20.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s21.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s22.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s23.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s24.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s25.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r3001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.root-e1.certainly.com</domain>
<domain includeSubdomains="true">crl.root-r1.certainly.com</domain>
<domain includeSubdomains="true">crl.rootca1.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rootca2.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rootca3.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rootca4.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rztrust.oemssl.cn</domain>
<domain includeSubdomains="true">crl.sbca.telesec.de</domain>
<domain includeSubdomains="true">crl.sectigo.com</domain>
<domain includeSubdomains="true">crl.sectigochina.com</domain>
<domain includeSubdomains="true">crl.serverid.telesec.de</domain>
<domain includeSubdomains="true">crl.starfieldtech.com</domain>
<domain includeSubdomains="true">crl.swisssign.ch</domain>
<domain includeSubdomains="true">crl.trust-provider.cn</domain>
<domain includeSubdomains="true">crl.trust-provider.com</domain>
<domain includeSubdomains="true">crl.tuntrust.tn</domain>
<domain includeSubdomains="true">crl.usertrust.com</domain>
<domain includeSubdomains="true">crl.wt.trustasia.com</domain>
<domain includeSubdomains="true">crl.xinchacha.oemssl.cn</domain>
<domain includeSubdomains="true">crl.zw.oemssl.cn</domain>
<domain includeSubdomains="true">crl05.actalis.it</domain>
<domain includeSubdomains="true">crl06.actalis.it</domain>
<domain includeSubdomains="true">crl09.actalis.it</domain>
<domain includeSubdomains="true">crl1.ecert.gov.hk</domain>
<domain includeSubdomains="true">crl13.actalis.it</domain>
<domain includeSubdomains="true">crl14.actalis.it</domain>
<domain includeSubdomains="true">crl15.actalis.it</domain>
<domain includeSubdomains="true">crl2.microsoft.com</domain>
<domain includeSubdomains="true">crl3.digicert.com</domain>
<domain includeSubdomains="true">crl4.digicert.com</domain>
<domain includeSubdomains="true">crls.certainly.com</domain>
<domain includeSubdomains="true">crls.ssl.com</domain>
<domain includeSubdomains="true">cyberfolks2.crl.certum.pl</domain>
<domain includeSubdomains="true">depo.kamusm.gov.tr</domain>
<domain includeSubdomains="true">digitaltrust.crl.sectigo.com</domain>
<domain includeSubdomains="true">dnencrypt.crl.sectigo.com</domain>
<domain includeSubdomains="true">dvtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">dvtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">e5.c.lencr.org</domain>
<domain includeSubdomains="true">e6.c.lencr.org</domain>
<domain includeSubdomains="true">e7.c.lencr.org</domain>
<domain includeSubdomains="true">e8.c.lencr.org</domain>
<domain includeSubdomains="true">ec2ca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2ca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2ca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2sslca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2sslca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2sslca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3ca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3ca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3ca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3sslca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3sslca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3sslca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eca.hinet.net</domain>
<domain includeSubdomains="true">edvtlsca2023-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">edvtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">edvtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">emudhra.crl.sectigo.com</domain>
<domain includeSubdomains="true">eosslca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eosslca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eosslca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eovtlsca2023-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eovtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eovtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eqca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqcpca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqcpca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqcpca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqoca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqoca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqoca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2018-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2018-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2018-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2023-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">esafer.crl.sectigo.com</domain>
<domain includeSubdomains="true">eue2m1.crl.root.amznts.eu</domain>
<domain includeSubdomains="true">eue3m1.crl.root.amznts.eu</domain>
<domain includeSubdomains="true">eur2m1.crl.root.amznts.eu</domain>
<domain includeSubdomains="true">eveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">evrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcadv.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcaev.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcaov.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcatrustauthdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcatrustauthevtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcatrustauthovtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">geant.crl.sectigo.com</domain>
<domain includeSubdomains="true">genious.crl.sectigo.com</domain>
<domain includeSubdomains="true">geossldvtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">geosslevtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">geosslovtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">globaltrust.crl.sectigo.com</domain>
<domain includeSubdomains="true">globessl.crl.sectigo.com</domain>
<domain includeSubdomains="true">gogetssldv.crl.certum.pl</domain>
<domain includeSubdomains="true">gogetsslev.crl.certum.pl</domain>
<domain includeSubdomains="true">gogetsslov.crl.certum.pl</domain>
<domain includeSubdomains="true">homepldvtlsg2r35ca.crl.certum.pl</domain>
<domain includeSubdomains="true">homepldvtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">homeplovtlsg2r35ca.crl.certum.pl</domain>
<domain includeSubdomains="true">homeplovtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">httpcrl.trust.telia.com</domain>
<domain includeSubdomains="true">ica.navercloudtrust.com</domain>
<domain includeSubdomains="true">ica.navercorp.com</domain>
<domain includeSubdomains="true">issauth.crl.sectigo.com</domain>
<domain includeSubdomains="true">itrusdv2.crl.certum.pl</domain>
<domain includeSubdomains="true">itrusdvtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">itrusov2.crl.certum.pl</domain>
<domain includeSubdomains="true">itrusovtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">itso.crl.sectigo.com</domain>
<domain includeSubdomains="true">joyssldvtlsg2r33ca.crl.certum.pl</domain>
<domain includeSubdomains="true">joysslovtlsg2r33ca.crl.certum.pl</domain>
<domain includeSubdomains="true">kingnettechdv.crl.certum.pl</domain>
<domain includeSubdomains="true">kingnettechev.crl.certum.pl</domain>
<domain includeSubdomains="true">kingnettechov.crl.certum.pl</domain>
<domain includeSubdomains="true">kr.crl.digicert.com</domain>
<domain includeSubdomains="true">lh2.crl.certum.pl</domain>
<domain includeSubdomains="true">namecheap.crl.sectigo.com</domain>
<domain includeSubdomains="true">nazwassldvtlsg2e29ca.crl.certum.pl</domain>
<domain includeSubdomains="true">nazwassldvtlsg2r29ca.crl.certum.pl</domain>
<domain includeSubdomains="true">netartcadv2.crl.certum.pl</domain>
<domain includeSubdomains="true">netartssldvtlsg2e31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">nijimo.crl.sectigo.com</domain>
<domain includeSubdomains="true">nyalabscadv.crl.certum.pl</domain>
<domain includeSubdomains="true">nyatworkdv.crl.certum.pl</domain>
<domain includeSubdomains="true">omitsecurity.crl.sectigo.com</domain>
<domain includeSubdomains="true">onesign.crl.sectigo.com</domain>
<domain includeSubdomains="true">osslca2016-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">osslca2016-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">osslca2016-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ovtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">ovtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">pki-crl.atos.net</domain>
<domain includeSubdomains="true">pki.telesec.de</domain>
<domain includeSubdomains="true">pkipro.certsign.ro</domain>
<domain includeSubdomains="true">positiwise.crl.sectigo.com</domain>
<domain includeSubdomains="true">psw.crl.sectigo.com</domain>
<domain includeSubdomains="true">public.wisekey.com</domain>
<domain includeSubdomains="true">qiduodv.crl.certum.pl</domain>
<domain includeSubdomains="true">qtlsca2018-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">qtlsca2018-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">qtlsca2018-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">qtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">r10.c.lencr.org</domain>
<domain includeSubdomains="true">r11.c.lencr.org</domain>
<domain includeSubdomains="true">r12.c.lencr.org</domain>
<domain includeSubdomains="true">r13.c.lencr.org</domain>
<domain includeSubdomains="true">rca.navercloudtrust.com</domain>
<domain includeSubdomains="true">rca.navercorp.com</domain>
<domain includeSubdomains="true">repo.pubcert.jprs.jp</domain>
<domain includeSubdomains="true">repo1.secomtrust.net</domain>
<domain includeSubdomains="true">repository.secomtrust.net</domain>
<domain includeSubdomains="true">repository.tls.hinet.net</domain>
<domain includeSubdomains="true">rootca.twca.com.tw</domain>
<domain includeSubdomains="true">rootglobaldv.crl.certum.pl</domain>
<domain includeSubdomains="true">rootnetworksdv2.crl.certum.pl</domain>
<domain includeSubdomains="true">shenzhendv.crl.certum.pl</domain>
<domain includeSubdomains="true">shenzhenev.crl.certum.pl</domain>
<domain includeSubdomains="true">shenzhenov.crl.certum.pl</domain>
<domain includeSubdomains="true">shoperdvtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">shuididv.crl.certum.pl</domain>
<domain includeSubdomains="true">shuidiev.crl.certum.pl</domain>
<domain includeSubdomains="true">shuidiov.crl.certum.pl</domain>
<domain includeSubdomains="true">soomadv.crl.certum.pl</domain>
<domain includeSubdomains="true">ssl2ca2016-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ssl2ca2016-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ssl2ca2016-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">sslca2014-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">sslca2014-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">sslca2014-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">sslcom.crl.certum.pl</domain>
<domain includeSubdomains="true">ssllimiteddv.crl.certum.pl</domain>
<domain includeSubdomains="true">sslserver.twca.com.tw</domain>
<domain includeSubdomains="true">subca.crl.certum.pl</domain>
<domain includeSubdomains="true">titrust.crl.sectigo.com</domain>
<domain includeSubdomains="true">tlcdvtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">tlcevtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">tlcovtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">tlsrootca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">trustasia.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiadv.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiadvtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaev.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaevtlse35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaevtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaov.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaovtlse35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaovtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustocean.crl.certum.pl</domain>
<domain includeSubdomains="true">tstlser20.crl.telesec.de</domain>
<domain includeSubdomains="true">tstlsrr23.crl.telesec.de</domain>
<domain includeSubdomains="true">unitedtrustov.crl.certum.pl</domain>
<domain includeSubdomains="true">validation.identrust.com</domain>
<domain includeSubdomains="true">verokey.crl.sectigo.com</domain>
<domain includeSubdomains="true">vtrusdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">vtrusovtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">webnic.crl.sectigo.com</domain>
<domain includeSubdomains="true">whitessldvtlsg2e36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">wosign.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus-dvca.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus-evca.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus-ovca.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus.crl.sectigo.com</domain>
<domain includeSubdomains="true">wtca-cafiles.itrus.com.cn</domain>
<domain includeSubdomains="true">wtca-crl.itrus.com.cn</domain>
<domain includeSubdomains="true">www.accv.es</domain>
<domain includeSubdomains="true">www.cert.fnmt.es</domain>
<domain includeSubdomains="true">www.d-trust.net</domain>
<domain includeSubdomains="true">www.microsoft.com</domain>
<domain includeSubdomains="true">x1.c.lencr.org</domain>
<domain includeSubdomains="true">x2.c.lencr.org</domain>
<domain includeSubdomains="true">xcctrustdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xcctrustevtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xcctrustovtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchacha2dv.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchacha2ov.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchachatrustdvtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchachatrustevtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchachatrustovtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinnetdvtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinnetevtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinnetovtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">ye.c.lencr.org</domain>
<domain includeSubdomains="true">ye1.c.lencr.org</domain>
<domain includeSubdomains="true">ye2.c.lencr.org</domain>
<domain includeSubdomains="true">yektadv.crl.certum.pl</domain>
<domain includeSubdomains="true">yektaov.crl.certum.pl</domain>
<domain includeSubdomains="true">yr.c.lencr.org</domain>
<domain includeSubdomains="true">yr1.c.lencr.org</domain>
<domain includeSubdomains="true">yr2.c.lencr.org</domain>
<domain includeSubdomains="true">zerossl.crl.sectigo.com</domain>
<domain includeSubdomains="true">ziwit.crl.sectigo.com</domain>
<domain includeSubdomains="true">zycatrustdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">zycatrustevtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">zycatrustovtlsg3r31ca.crl.certum.pl</domain>
</domain-config>
</network-security-config>
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<!-- Ref: https://developer.android.com/training/articles/security-config.html -->
<!-- By default, do not allow clearText traffic -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
<certificates
src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
<!-- Allow clearText traffic on some specified host -->
<domain-config cleartextTrafficPermitted="true">
<!-- Localhost -->
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<!-- Localhost for Android emulator -->
<domain includeSubdomains="true">10.0.2.2</domain>
<!-- Onion services -->
<domain includeSubdomains="true">onion</domain>
<!-- Domains that are used for LANs -->
<!-- These are IANA recognized special use domain names, see https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml -->
<domain includeSubdomains="true">home.arpa</domain>
<domain includeSubdomains="true">local</domain> <!-- Note this has been reserved for use with mDNS -->
<domain includeSubdomains="true">test</domain>
<!-- These are observed in the wild either by convention or RFCs that have not been accepted, and are not currently TLDs -->
<domain includeSubdomains="true">home</domain>
<domain includeSubdomains="true">lan</domain>
<domain includeSubdomains="true">localdomain</domain>
</domain-config>
</network-security-config>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!--
Lets the system offer the conversation shortcuts published by
DefaultNotificationConversationService in the Direct Share row of the share sheet.
The category must match SHARE_TARGET_CATEGORY in that class.
-->
<share-target android:targetClass="io.element.android.x.MainActivity">
<data android:mimeType="*/*" />
<category android:name="io.element.android.category.SHARE_TARGET" />
</share-target>
</shortcuts>
+11 -4
View File
@@ -22,13 +22,19 @@ android {
defaultConfig {
buildConfigFieldStr(
name = "URL_POLICY",
// HealthChat fork: a configured value wins for every build type, not only enterprise ones.
value = BuildTimeConfig.URL_POLICY ?: if (isEnterpriseBuild) "" else "https://element.io/cookie-policy",
value = if (isEnterpriseBuild) {
BuildTimeConfig.URL_POLICY ?: ""
} else {
"https://element.io/cookie-policy"
},
)
buildConfigFieldStr(
name = "BUG_REPORT_URL",
// HealthChat fork: a configured value wins for every build type, not only enterprise ones.
value = BuildTimeConfig.BUG_REPORT_URL ?: if (isEnterpriseBuild) "" else "https://rageshakes.element.io/api/submit",
value = if (isEnterpriseBuild) {
BuildTimeConfig.BUG_REPORT_URL ?: ""
} else {
"https://rageshakes.element.io/api/submit"
},
)
buildConfigFieldStr(
name = "BUG_REPORT_APP_NAME",
@@ -42,6 +48,7 @@ android {
}
dependencies {
implementation(libs.coroutines.core)
implementation(libs.androidx.annotationjvm)
implementation(libs.androidx.corektx)
implementation(projects.libraries.matrix.api)
@@ -9,8 +9,15 @@
package io.element.android.appconfig
object AuthenticationConfig {
const val MATRIX_ORG_URL = "https://matrix.org"
/**
* URL with some docs that explain what's sliding sync and how to add it to your home server.
*/
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
/**
* Force a sliding sync proxy url, if not null, the proxy url in the .well-known file will be ignored.
*/
val SLIDING_SYNC_PROXY_URL: String? = null
}
@@ -9,10 +9,9 @@
package io.element.android.appconfig
object LearnMoreConfig {
// HealthChat fork: every "learn more" link points at the HealthChat site instead of element.io/help.
const val ENCRYPTION_URL: String = "https://www.healthchat.ch"
const val DEVICE_VERIFICATION_URL: String = "https://www.healthchat.ch"
const val SECURE_BACKUP_URL: String = "https://www.healthchat.ch"
const val IDENTITY_CHANGE_URL: String = "https://www.healthchat.ch"
const val HISTORY_VISIBLE_URL: String = "https://www.healthchat.ch"
const val ENCRYPTION_URL: String = "https://element.io/help#encryption"
const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification"
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
const val HISTORY_VISIBLE_URL: String = "https://element.io/en/help#e2ee-history-sharing"
}
@@ -10,19 +10,5 @@ package io.element.android.appconfig
object MatrixConfiguration {
const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
/**
* HealthChat's own matrix.to deployment (https://github.com/Novaloop-AG/healthchat-matrix.to).
* It uses the exact same URL format as matrix.to, so links can be translated by swapping the
* base URL. Used both to recognise incoming links and to build links shared outside of Matrix.
*/
const val HEALTHCHAT_TO_PERMALINK_BASE_URL: String = "https://to.healthchat.ch/#/"
/**
* Base URL of the HealthChat web client, which is what the healthchat:// custom scheme
* handed out by [HEALTHCHAT_TO_PERMALINK_BASE_URL] points to.
*/
const val HEALTHCHAT_WEB_BASE_URL: String = "https://app.healthchat.ch/#/"
val clientPermalinkBaseUrl: String? = null
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.appconfig
object PushConfig {
/**
* Note: pusher_app_id cannot exceed 64 chars.
*/
const val PUSHER_APP_ID: String = "im.vector.app.android"
}
@@ -35,11 +35,4 @@ object RageshakeConfig {
* The maximum number of log lines a rageshake can contain.
*/
const val MAX_LOG_LINES_SIZE = 1_000_000
/**
* The maximum size of the description. The bug report server copies it verbatim into a GitHub issue body,
* which GitHub rejects above 65536 characters. The remaining budget is left to the other reported fields,
* mainly the list of the omitted log file names.
*/
const val MAX_DESCRIPTION_SIZE = 40 * 1024
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

-10
View File
@@ -34,7 +34,6 @@ dependencies {
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.oauth.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api)
@@ -49,7 +48,6 @@ dependencies {
implementation(libs.coil)
implementation(projects.features.announcement.api)
implementation(projects.features.callnative.api)
implementation(projects.features.ftue.api)
implementation(projects.features.linknewdevice.api)
implementation(projects.features.share.api)
@@ -61,23 +59,15 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.accountselect.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.matrixmedia.test)
testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.announcement.test)
testImplementation(projects.features.forward.test)
testImplementation(projects.features.messages.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.preferences.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.signedout.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
@@ -8,15 +8,9 @@
package io.element.android.appnav
import com.bumble.appyx.core.navigation.NavKey
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.BackStackElement
import com.bumble.appyx.navmodel.backstack.BackStackElements
import com.bumble.appyx.navmodel.backstack.operation.BackStackOperation
import com.bumble.appyx.navmodel.backstack.operation.NewRoot
import com.bumble.appyx.navmodel.backstack.operation.Remove
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
/**
* Don't process NewRoot if the nav target already exists in the stack.
@@ -38,45 +32,3 @@ fun <T : Any> BackStack<T>.removeLast(element: T) {
} ?: return
accept(Remove(lastExpectedNavElement.key))
}
/**
* Replace the root element of the backstack, and keep the elements on top of it, contrary to [safeRoot] which removes all the elements.
*/
fun <T : Any> BackStack<T>.replaceRoot(element: T) {
accept(ReplaceRoot(element))
}
/**
* Replace the root element of the backstack, keeping the elements on top of it.
*
* Operation:
*
* [A, B, C] + ReplaceRoot(D) = [D, B, C]
* [A] + ReplaceRoot(D) = [D]
*/
@Parcelize
internal data class ReplaceRoot<T : Any>(
private val element: @RawValue T
) : BackStackOperation<T> {
override fun isApplicable(elements: BackStackElements<T>): Boolean =
elements.rootElement()?.key?.navTarget != element
override fun invoke(elements: BackStackElements<T>): BackStackElements<T> {
val root = elements.rootElement() ?: return elements
if (root.targetState == BackStack.State.ACTIVE) {
// Nothing on top of the root, so this is the same as NewRoot
return NewRoot(element).invoke(elements)
}
// The root is stashed below other elements: drop it, as Remove does for a non active element,
// and add the new root, stashed, in place of it.
val newRoot = BackStackElement(
key = NavKey(element),
fromState = BackStack.State.STASHED,
targetState = BackStack.State.STASHED,
operation = this,
)
return elements.map { if (it == root) newRoot else it }
}
private fun BackStackElements<T>.rootElement() = firstOrNull { it.targetState != BackStack.State.DESTROYED }
}
@@ -33,8 +33,7 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import kotlinx.parcelize.Parcelize
/**
@@ -60,7 +59,6 @@ class LoggedInAppScopeFlowNode(
interface Callback : Plugin {
fun navigateToBugReport()
fun navigateToAddAccount()
fun navigateToSession(sessionId: SessionId)
}
private val callback: Callback = callback()
@@ -93,10 +91,6 @@ class LoggedInAppScopeFlowNode(
override fun navigateToAddAccount() {
callback.navigateToAddAccount()
}
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
}
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
}
@@ -8,51 +8,28 @@
package io.element.android.appnav
import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import dev.zacsweers.metro.Inject
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@AssistedInject
@Inject
class LoggedInEventProcessor(
@Assisted private val snackbarDispatcher: SnackbarDispatcher,
private val snackbarDispatcher: SnackbarDispatcher,
private val roomMembershipObserver: RoomMembershipObserver,
private val sessionId: SessionId,
private val syncService: SyncService,
private val notificationCleaner: NotificationCleaner,
private val dispatchers: CoroutineDispatchers
) {
@AssistedFactory
interface Factory {
fun create(snackbarDispatcher: SnackbarDispatcher): LoggedInEventProcessor
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var currentChildScope: CoroutineScope? = null
private var observingJob: Job? = null
fun observeEvents(coroutineScope: CoroutineScope) {
if (currentChildScope != null) return
val childScope = coroutineScope.childScope(dispatchers.computation, "LoggedInEventProcessor")
.also { currentChildScope = it }
roomMembershipObserver.updates
observingJob = roomMembershipObserver.updates
.filter { !it.isUserInRoom }
.distinctUntilChanged()
.onEach { roomMemberShipUpdate ->
@@ -71,19 +48,12 @@ class LoggedInEventProcessor(
else -> Unit
}
}
.launchIn(childScope)
// Use the sync service heartbeat as a 'heartbeat' update to check existing notifications
syncService.heartBeat
.onEach {
notificationCleaner.clearReadRoomsNotifications(sessionId)
}
.launchIn(childScope)
.launchIn(coroutineScope)
}
fun stopObserving() {
currentChildScope?.cancel()
currentChildScope = null
observingJob?.cancel()
observingJob = null
}
private fun displayMessage(message: Int) {
@@ -11,7 +11,6 @@ package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -47,7 +46,6 @@ import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.callnative.api.NativeCallHost
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService
@@ -56,8 +54,6 @@ import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.features.location.api.LocalMapTilerConfig
import io.element.android.features.location.api.LocationService
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
@@ -66,6 +62,8 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@@ -85,16 +83,15 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import io.element.android.libraries.ui.common.nodes.EmptyState
import io.element.android.libraries.ui.common.nodes.emptyNode
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
@@ -108,10 +105,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.time.Duration
import java.time.Instant
import java.util.Optional
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent
// The maximum number of room nodes that should be kept in the backstack at the same time.
@@ -135,9 +135,9 @@ class LoggedInFlowNode(
private val sessionCoroutineScope: CoroutineScope,
private val ftueService: FtueService,
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues,
private val nativeCallHost: NativeCallHost,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
private val sessionEnterpriseService: SessionEnterpriseService,
@@ -153,8 +153,6 @@ class LoggedInFlowNode(
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
private val locationService: LocationService,
private val loggedInEventProcessorFactory: LoggedInEventProcessor.Factory,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
@@ -170,26 +168,44 @@ class LoggedInFlowNode(
interface Callback : Plugin {
fun navigateToBugReport()
fun navigateToAddAccount()
fun navigateToSession(sessionId: SessionId)
}
private val callback: Callback = callback()
private val loggedInFlowProcessor = loggedInEventProcessorFactory.create(snackbarDispatcher)
private val loggedInFlowProcessor = LoggedInEventProcessor(
snackbarDispatcher = snackbarDispatcher,
roomMembershipObserver = matrixClient.roomMembershipObserver,
)
/**
* Display the incoming verification request, the app is expected to be in foreground.
*/
fun onIncomingVerificationRequest(verificationRequest: VerificationRequest.Incoming) {
lifecycleScope.launch {
// Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it
// Otherwise, the RoomList UI may be incorrectly displayed on top
withTimeout(5.seconds) {
backstack.elements.first { elements ->
elements.any { it.key.navTarget == NavTarget.Home }
private val verificationListener = object : SessionVerificationServiceListener {
override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) {
// Without this launch the rendering and actual state of this Appyx node's children gets out of sync, resulting in a crash.
// This might be because this method is called back from Rust in a background thread.
lifecycleScope.launch {
val receivedAt = Instant.now()
// Wait until the app is in foreground to display the incoming verification request
appNavigationStateService.appNavigationState.first { it.isInForeground }
// TODO there should also be a timeout for > 10 minutes elapsed since the request was created, but the SDK doesn't expose that info yet
val now = Instant.now()
val elapsedTimeSinceReceived = Duration.between(receivedAt, now).toKotlinDuration()
// Discard the incoming verification request if it has timed out
if (elapsedTimeSinceReceived > 2.minutes) {
Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.")
return@launch
}
}
backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest))
// Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it
// Otherwise, the RoomList UI may be incorrectly displayed on top
withTimeout(5.seconds) {
backstack.elements.first { elements ->
elements.any { it.key.navTarget == NavTarget.Home }
}
}
backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest))
}
}
}
@@ -204,6 +220,7 @@ class LoggedInFlowNode(
analyticsRoomListStateWatcher.start()
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
matrixClient.sessionVerificationService.setListener(verificationListener)
mediaPreviewConfigMigration()
sessionCoroutineScope.launch {
// Wait for the network to be connected before pre-fetching the max file upload size
@@ -232,6 +249,7 @@ class LoggedInFlowNode(
onDestroy = {
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
matrixClient.sessionVerificationService.setListener(null)
analyticsRoomListStateWatcher.stop()
}
)
@@ -292,15 +310,16 @@ class LoggedInFlowNode(
@Parcelize
data object RoomDirectory : NavTarget
@Parcelize
data class IncomingShare(val shareIntentData: ShareIntentData) : NavTarget
@Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
// Rendered while the FtueState is Unknown. Render a loading state rather than nothing at all,
// since there is no guarantee that this state will be left.
NavTarget.Placeholder -> emptyNode(buildContext, state = EmptyState(delayBeforeShowingContent = 500.milliseconds))
NavTarget.Placeholder -> emptyNode(buildContext)
NavTarget.LoggedInPermanent -> {
val callback = object : LoggedInNode.Callback {
override fun navigateToNotificationTroubleshoot() {
@@ -311,11 +330,11 @@ class LoggedInFlowNode(
}
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) {
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
clearBackstack = false,
)
}
@@ -354,19 +373,6 @@ class LoggedInFlowNode(
override fun navigateToBugReport() {
callback.navigateToBugReport()
}
override fun navigateToAddAccount() {
callback.navigateToAddAccount()
}
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
// HealthChat fork: open a user profile from the member directory tab.
override fun navigateToUserProfile(userId: UserId) {
backstack.push(NavTarget.UserProfile(userId))
}
}
homeEntryPoint.createNode(
parentNode = this,
@@ -459,10 +465,6 @@ class LoggedInFlowNode(
callback.navigateToAddAccount()
}
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
override fun navigateToLinkNewDevice() {
backstack.push(NavTarget.LinkNewDevice)
}
@@ -576,6 +578,30 @@ class LoggedInFlowNode(
},
)
}
is NavTarget.IncomingShare -> {
shareEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
callback = object : ShareEntryPoint.Callback {
override fun onDone(roomIds: List<RoomId>) {
// Remove the incoming share screen
backstack.pop()
// Navigate to the room if the text/media was shared to a single one
roomIds.singleOrNull()?.let { roomId ->
lifecycleScope.launch {
// Wait until the incoming share screen is removed
backstack.elements.first { it.lastOrNull()?.key?.navTarget !is NavTarget.IncomingShare }
// Then attach the room
attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false)
}
}
}
},
)
}
is NavTarget.IncomingVerificationRequest -> {
incomingVerificationEntryPoint.createNode(
parentNode = this,
@@ -591,15 +617,6 @@ class LoggedInFlowNode(
}
}
/**
* Wait for the home screen to be attached, i.e. for the FTUE to be complete.
*/
internal suspend fun waitForHome() {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
}
suspend fun attachRoom(
roomIdOrAlias: RoomIdOrAlias,
serverNames: List<String> = emptyList(),
@@ -644,14 +661,22 @@ class LoggedInFlowNode(
}
}
internal suspend fun attachIncomingShare(shareIntentData: ShareIntentData) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
attachChild<Node> {
backstack.push(
NavTarget.IncomingShare(shareIntentData)
)
}
}
@Composable
override fun View(modifier: Modifier) {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
}.collectAsState(SemanticColorsLightDark.default)
val mapTilerConfig by locationService.mapTilerConfig.collectAsState()
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
featureFlagService = featureFlagService,
@@ -659,22 +684,16 @@ class LoggedInFlowNode(
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
CompositionLocalProvider(LocalMapTilerConfig provides mapTilerConfig) {
val isOnline by syncService.isOnline.collectAsState()
// Outside the connectivity indicator so the two strips stack rather than fight: an
// offline banner during a call belongs under the call bar, not over it.
nativeCallHost.Render(modifier = modifier) { callContentModifier ->
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = callContentModifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
}
val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = modifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
}
}
@@ -13,7 +13,6 @@ package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import com.bumble.appyx.core.lifecycle.subscribe
@@ -25,8 +24,6 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.architecture.BackstackView
@@ -36,11 +33,8 @@ import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
import io.element.android.libraries.designsystem.utils.ScreenOrientation
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
@@ -51,8 +45,6 @@ class NotLoggedInFlowNode(
private val loginEntryPoint: LoginEntryPoint,
private val imageLoaderHolder: ImageLoaderHolder,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
private val featureFlagService: FeatureFlagService,
private val announcementService: AnnouncementService,
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
@@ -76,13 +68,6 @@ class NotLoggedInFlowNode(
override fun onBuilt() {
super.onBuilt()
analyticsColdStartWatcher.whenLoggingIn()
lifecycleScope.launch {
// Users who enter the login flow while the feature is available, either for the first time or to add
// another account, do not need to be informed about the multi-account feature.
if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) {
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
}
}
lifecycle.subscribe(
onResume = {
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
@@ -32,6 +32,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.room.RoomFlowNode
@@ -39,16 +40,10 @@ import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.appnav.verification.IncomingVerificationRequestData
import io.element.android.appnav.verification.IncomingVerificationRequestObserver
import io.element.android.appnav.verification.OtherSessionIncomingVerificationNode
import io.element.android.appnav.verification.OtherSessionVerificationNode
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
@@ -57,20 +52,17 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.coroutine.withPreviousValue
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.accountprovider.AccountProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oauth.api.OAuthActionFlow
import io.element.android.libraries.sessionstorage.api.LoggedInState
@@ -83,16 +75,11 @@ import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@ContributesNode(AppScope::class)
@AssistedInject
@@ -113,8 +100,6 @@ class RootFlowNode(
private val announcementService: AnnouncementService,
private val analyticsService: AnalyticsService,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
private val incomingVerificationRequestObserver: IncomingVerificationRequestObserver,
private val shareEntryPoint: ShareEntryPoint,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : BaseFlowNode<RootFlowNode.NavTarget>(
backstack = BackStack(
@@ -124,13 +109,6 @@ class RootFlowNode(
buildContext = buildContext,
plugins = plugins
) {
/**
* Login params coming from a launch or new [Intent], waiting to be consumed by the not logged in flow.
* Kept here so that the root nav target can be computed from both the logged in state and the pending
* login params, whatever the order in which the intent and the first nav state emission are processed.
*/
private var pendingLoginParams: LoginParams? = null
override fun onBuilt() {
analyticsColdStartWatcher.start()
appCoroutineScope.launch {
@@ -142,8 +120,6 @@ class RootFlowNode(
observeNavState(false)
}
}
observeIncomingVerificationRequests()
observeRemovedSessions()
super.onBuilt()
}
@@ -176,7 +152,7 @@ class RootFlowNode(
}
}
LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(pendingLoginParams)
switchToNotLoggedInFlow(null)
}
}
}
@@ -218,93 +194,16 @@ class RootFlowNode(
}
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
pendingLoginParams = null
setRoot(NavTarget.LoggedInFlow(sessionId, navId))
restoreAllSessions()
}
/**
* Restore all the sessions with a valid token, so that they sync and can receive incoming verification requests.
*/
private fun restoreAllSessions() = lifecycleScope.launch {
sessionStore.getAllSessions()
.filter { it.isTokenValid }
.forEach { sessionData ->
matrixSessionCache.getOrRestore(SessionId(sessionData.userId)).onFailure {
Timber.e(it, "Failed to restore session ${sessionData.userId}")
}
}
}
/**
* Remove from the cache the sessions which have been removed from the session store, for instance after a logout.
*/
private fun observeRemovedSessions() {
sessionStore.sessionsFlow()
.map { sessions -> sessions.map { SessionId(it.userId) }.toSet() }
.withPreviousValue()
.onEach { (previous, current) ->
(previous.orEmpty() - current).forEach { sessionId ->
// The verification of a removed session cannot continue, for instance if the user has signed out from it
backstack.removeLast(NavTarget.OtherAccountSessionVerification(sessionId))
Timber.d("Session $sessionId removed, remove it from the cache")
matrixSessionCache.remove(sessionId)
}
}
.launchIn(lifecycleScope)
}
private fun observeIncomingVerificationRequests() {
incomingVerificationRequestObserver.incomingVerificationRequests()
.onEach(::onIncomingVerificationRequest)
.launchIn(lifecycleScope)
}
private fun onIncomingVerificationRequest(data: IncomingVerificationRequestData) = lifecycleScope.launch {
// Wait for a logged in flow to be displayed
val currentSessionId = withTimeoutOrNull(5.seconds) {
backstack.elements.map { elements ->
elements.firstNotNullOfOrNull { (it.key.navTarget as? NavTarget.LoggedInFlow)?.sessionId }
}.filterNotNull().first()
}
if (currentSessionId == null) {
Timber.w("Incoming verification request ${data.verificationRequest.details.flowId} discarded, no logged in flow.")
return@launch
}
if (currentSessionId == data.sessionId) {
waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == currentSessionId
}
.attachSession()
.onIncomingVerificationRequest(data.verificationRequest)
} else {
// Display the request on top of the current session, without switching session
backstack.push(NavTarget.OtherAccountIncomingVerificationRequest(data.sessionId, data.verificationRequest))
}
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
}
private fun switchToNotLoggedInFlow(params: LoginParams?) {
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
}
private fun switchToSignedOutFlow(sessionId: SessionId) {
pendingLoginParams = null
setRoot(NavTarget.SignedOutFlow(sessionId))
}
/**
* Set the root of the backstack. If an incoming share is displayed, keep it on top of the new root, so that the user can
* finish sharing, for instance with another account if the current one has been signed out.
*/
private fun setRoot(navTarget: NavTarget) {
val hasIncomingShare = backstack.elements.value.any { it.key.navTarget is NavTarget.IncomingShare }
if (hasIncomingShare) {
backstack.replaceRoot(navTarget)
} else {
backstack.safeRoot(navTarget)
}
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
}
private suspend fun restoreSessionIfNeeded(
@@ -368,7 +267,8 @@ class RootFlowNode(
@Parcelize data class AccountSelect(
val currentSessionId: SessionId,
val permalinkData: PermalinkData,
val shareIntentData: ShareIntentData?,
val permalinkData: PermalinkData?,
) : NavTarget
@Parcelize data class NotLoggedInFlow(
@@ -384,20 +284,6 @@ class RootFlowNode(
) : NavTarget
@Parcelize data object BugReport : NavTarget
@Parcelize data class IncomingShare(
val sessionId: SessionId,
val shareIntentData: ShareIntentData,
) : NavTarget
@Parcelize data class OtherAccountIncomingVerificationRequest(
val sessionId: SessionId,
val verificationRequest: VerificationRequest.Incoming,
) : NavTarget
@Parcelize data class OtherAccountSessionVerification(
val sessionId: SessionId,
) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -416,12 +302,6 @@ class RootFlowNode(
override fun navigateToAddAccount() {
backstack.push(NavTarget.NotLoggedInFlow(null))
}
override fun navigateToSession(sessionId: SessionId) {
lifecycleScope.launch {
sessionStore.setLatestSession(sessionId.value)
}
}
}
val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap)
val buildContext = if (savedNavState != null) {
@@ -439,7 +319,6 @@ class RootFlowNode(
}
override fun onDone() {
pendingLoginParams = null
backstack.pop()
}
}
@@ -458,73 +337,6 @@ class RootFlowNode(
)
}
NavTarget.SplashScreen -> emptyNode(buildContext)
is NavTarget.OtherAccountIncomingVerificationRequest -> {
if (matrixSessionCache.getOrNull(navTarget.sessionId) == null) {
Timber.w("Couldn't find session ${navTarget.sessionId} for the incoming verification request")
lifecycleScope.launch { backstack.pop() }
return emptyNode(buildContext)
}
val inputs = OtherSessionIncomingVerificationNode.Inputs(
sessionId = navTarget.sessionId,
verificationRequest = navTarget.verificationRequest,
)
val callback = object : OtherSessionIncomingVerificationNode.Callback {
override fun onDone() {
backstack.pop()
}
}
createNode<OtherSessionIncomingVerificationNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.OtherAccountSessionVerification -> {
if (matrixSessionCache.getOrNull(navTarget.sessionId) == null) {
Timber.w("Couldn't find session ${navTarget.sessionId} to verify")
lifecycleScope.launch { backstack.pop() }
return emptyNode(buildContext)
}
val inputs = OtherSessionVerificationNode.Inputs(sessionId = navTarget.sessionId)
val callback = object : OtherSessionVerificationNode.Callback {
override fun onDone() {
backstack.pop()
}
}
createNode<OtherSessionVerificationNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.IncomingShare -> {
val callback = object : ShareEntryPoint.Callback {
override fun onDone(sessionId: SessionId, roomIds: List<RoomId>) {
// Remove the incoming share screen
backstack.pop()
lifecycleScope.launch {
// Switch to the session which has been used to share the data, if necessary,
// and navigate to the room if the data was shared to a single one.
val loggedInFlowNode = attachSession(sessionId)
roomIds.singleOrNull()?.let { roomId ->
loggedInFlowNode.attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false)
}
}
}
override fun onCancel() {
backstack.pop()
}
override fun onSessionVerificationRequired(sessionId: SessionId) {
val navTarget = NavTarget.OtherAccountSessionVerification(sessionId)
if (backstack.elements.value.lastOrNull()?.key?.navTarget != navTarget) {
backstack.push(navTarget)
}
}
}
shareEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = ShareEntryPoint.Params(
sessionId = navTarget.sessionId,
shareIntentData = navTarget.shareIntentData,
),
callback = callback,
)
}
NavTarget.BugReport -> {
val callback = object : BugReportEntryPoint.Callback {
override fun onDone() {
@@ -546,7 +358,13 @@ class RootFlowNode(
// Do not pop when the account is changed to avoid a UI flicker.
backstack.pop()
}
attachSession(sessionId).attachPermalinkData(navTarget.permalinkData)
attachSession(sessionId).apply {
if (navTarget.shareIntentData != null) {
attachIncomingShare(navTarget.shareIntentData)
} else if (navTarget.permalinkData != null) {
attachPermalinkData(navTarget.permalinkData)
}
}
}
}
@@ -581,8 +399,7 @@ class RootFlowNode(
}
private suspend fun onLoginLink(params: LoginParams) {
val accountProvider = AccountProvider.Generic(params.accountProvider)
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(accountProvider)) {
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
// Is there a session already?
val sessions = sessionStore.getAllSessions()
if (sessions.isNotEmpty()) {
@@ -601,7 +418,6 @@ class RootFlowNode(
Timber.w("Login link ignored, multi account is disabled")
}
} else {
pendingLoginParams = params
switchToNotLoggedInFlow(params)
}
} else {
@@ -616,12 +432,21 @@ class RootFlowNode(
// No session, open login
switchToNotLoggedInFlow(null)
} else {
// Wait for the current session to be restored and for the FTUE to be complete, so that the session is verified,
// then display the incoming share on top of it.
// The incoming share does not belong to the current session: in case of multiple accounts, the user can switch account
// from the room select screen, and the share is kept if the current session is signed out in the meantime.
attachSession(latestSessionId).waitForHome()
backstack.push(NavTarget.IncomingShare(latestSessionId, shareIntentData))
// wait for the current session to be restored
val loggedInFlowNode = attachSession(latestSessionId)
if (sessionStore.numberOfSessions() > 1) {
// Several accounts, let the user choose which one to use
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
shareIntentData = shareIntentData,
permalinkData = null,
)
)
} else {
// Only one account, directly attach the incoming share node.
loggedInFlowNode.attachIncomingShare(shareIntentData)
}
}
}
@@ -644,6 +469,7 @@ class RootFlowNode(
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
shareIntentData = null,
permalinkData = permalinkData,
)
)
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.session
package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap
@@ -21,9 +21,6 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.AnalyticsUserData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@@ -44,12 +41,6 @@ class MatrixSessionCache(
private val analyticsService: AnalyticsService,
) : MatrixClientProvider {
private val sessionIdsToMatrixSession = ConcurrentHashMap<SessionId, InMemoryMatrixSession>()
private val _matrixClients = MutableStateFlow<List<MatrixClient>>(emptyList())
/**
* The [MatrixClient]s currently in the cache.
*/
val matrixClients: StateFlow<List<MatrixClient>> = _matrixClients.asStateFlow()
private val restoreMutex = Mutex()
init {
@@ -60,12 +51,10 @@ class MatrixSessionCache(
fun removeAll() {
sessionIdsToMatrixSession.clear()
updateMatrixClients()
}
fun remove(sessionId: SessionId) {
sessionIdsToMatrixSession.remove(sessionId)
updateMatrixClients()
}
override fun getOrNull(sessionId: SessionId): MatrixClient? {
@@ -126,20 +115,15 @@ class MatrixSessionCache(
private fun onNewMatrixClient(matrixClient: MatrixClient) {
val syncOrchestrator = syncOrchestratorFactory.create(
matrixClient = matrixClient,
syncService = matrixClient.syncService,
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
)
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
matrixClient = matrixClient,
syncOrchestrator = syncOrchestrator,
)
updateMatrixClients()
syncOrchestrator.start()
}
private fun updateMatrixClients() {
_matrixClients.value = sessionIdsToMatrixSession.values.map { it.matrixClient }
}
}
private data class InMemoryMatrixSession(
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.session
package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted
@@ -16,7 +16,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction
@@ -28,20 +28,16 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@AssistedInject
class SyncOrchestrator(
@Assisted private val matrixClient: MatrixClient,
@Assisted private val syncService: SyncService,
@Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
@@ -51,25 +47,17 @@ class SyncOrchestrator(
@AssistedFactory
interface Factory {
fun create(
matrixClient: MatrixClient,
syncService: SyncService,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator
}
private val tag = "SyncOrchestrator"
private val syncService = matrixClient.syncService
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
/**
* Number of consecutive attempts at restarting a sync service which stopped on its own.
* Reset once the sync service has been running for [RESTART_BACKOFF_RESET_DELAY].
*/
private val restartAttempt = AtomicInteger(0)
/**
* Starting observing the app state and network state to start/stop the sync service.
*
@@ -101,23 +89,12 @@ class SyncOrchestrator(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun observeStates() = coroutineScope.launch {
Timber.tag(tag).d("start observing the app and network state")
// Reset the restart backoff once the sync service has been steadily running for a while. Note that
// SyncService.startSync() moves the state to Running right away, so resetting on any Running value
// would defeat the backoff when the sync service dies again immediately after each restart.
syncService.syncState
.debounce(RESTART_BACKOFF_RESET_DELAY)
.onEach { syncState ->
if (syncState == SyncState.Running) {
restartAttempt.set(0)
}
}
.launchIn(this)
val isAppActiveFlows = listOf(
appForegroundStateService.isInForeground,
appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent,
appForegroundStateService.hasRingingCall,
appForegroundStateService.isSharingLiveLocation,
appForegroundStateService.isSharingLiveLocation
)
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
combine(
@@ -126,11 +103,6 @@ class SyncOrchestrator(
networkMonitor.connectivity,
isAppActiveFlow,
) { syncState, networkState, isAppActive ->
if (matrixClient.isShuttingDown) {
Timber.tag(tag).d("Matrix client is shutting down, no need to start the sync service again")
return@combine SyncStateAction.NoOp
}
val isNetworkAvailable = networkState == NetworkStatus.Connected
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
@@ -138,26 +110,14 @@ class SyncOrchestrator(
SyncStateAction.StopSync
} else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) {
SyncStateAction.StartSync
} else if (syncState == SyncState.Error && isAppActive && isNetworkAvailable) {
// The sync service has given up, for instance because the homeserver has expired our sliding sync
// session and answered 400 M_UNKNOWN_POS. Nothing else will ever restart it, so do it from here,
// else the application is stuck until its data is cleared.
SyncStateAction.RestartSync
} else {
SyncStateAction.NoOp
}
}
.distinctUntilChanged()
.debounce { action ->
when (action) {
// Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often
SyncStateAction.StopSync -> 3.seconds
// Back off before restarting a dead sync service, to avoid hammering a failing homeserver.
// A state change in the meantime cancels the pending restart.
SyncStateAction.RestartSync -> restartDelay(restartAttempt.get())
SyncStateAction.StartSync,
SyncStateAction.NoOp -> 0.seconds
}
// Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often
if (action == SyncStateAction.StopSync) 3.seconds else 0.seconds
}
.onCompletion {
Timber.tag(tag).d("has been stopped")
@@ -167,11 +127,6 @@ class SyncOrchestrator(
SyncStateAction.StartSync -> {
syncService.startSync()
}
SyncStateAction.RestartSync -> {
val attempt = restartAttempt.incrementAndGet()
Timber.tag(tag).w("sync service is dead, restarting it (attempt $attempt)")
syncService.startSync()
}
SyncStateAction.StopSync -> {
syncService.stopSync()
}
@@ -181,35 +136,8 @@ class SyncOrchestrator(
}
}
@VisibleForTesting
internal val FIRST_RESTART_DELAY = 1.seconds
/**
* Exponential backoff, from [FIRST_RESTART_DELAY], doubling on every attempt, up to [FIRST_RESTART_DELAY * 32].
*/
@VisibleForTesting
internal val RESTART_BACKOFF_DELAYS = listOf(
FIRST_RESTART_DELAY,
FIRST_RESTART_DELAY * 2,
FIRST_RESTART_DELAY * 4,
FIRST_RESTART_DELAY * 8,
FIRST_RESTART_DELAY * 16,
FIRST_RESTART_DELAY * 32,
)
/**
* Duration during which the sync service has to stay running before the restart backoff is reset.
*/
@VisibleForTesting
internal val RESTART_BACKOFF_RESET_DELAY = 30.seconds
private fun restartDelay(attempt: Int): Duration {
return RESTART_BACKOFF_DELAYS[attempt.coerceIn(0, RESTART_BACKOFF_DELAYS.lastIndex)]
}
private enum class SyncStateAction {
StartSync,
RestartSync,
StopSync,
NoOp,
}
@@ -76,7 +76,6 @@ class IntentResolver(
}
private fun Intent.canBeIgnored(): Boolean {
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
return action == Intent.ACTION_MAIN &&
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
}
@@ -8,11 +8,8 @@
package io.element.android.appnav.loggedin
sealed interface LoggedInEvent {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvent
data object CheckSlidingSyncProxyAvailability : LoggedInEvent
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvent
data object DismissLocalNetworkPermissionPrompt : LoggedInEvent
data object RequestLocationNetworkPermission : LoggedInEvent
sealed interface LoggedInEvents {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
}
@@ -8,7 +8,6 @@
package io.element.android.appnav.loggedin
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -16,7 +15,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
@@ -38,10 +36,6 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService
@@ -64,12 +58,7 @@ class LoggedInPresenter(
private val encryptionService: EncryptionService,
private val buildMeta: BuildMeta,
private val networkMonitor: NetworkMonitor,
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter<LoggedInState> {
private val localNetworkPermissionsPresenter: PermissionsPresenter =
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
@Composable
override fun present(): LoggedInState {
val coroutineScope = rememberCoroutineScope()
@@ -105,18 +94,9 @@ class LoggedInPresenter(
}
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
val isOnline by syncService.isOnline.collectAsState()
val hasNetwork by networkMonitor.connectivity.collectAsState()
val syncIndicatorState by remember {
val showSyncSpinner by remember {
derivedStateOf {
when {
isOnline -> if (syncIndicator == RoomListService.SyncIndicator.Show) {
SyncIndicatorState.Syncing
} else {
SyncIndicatorState.Hidden
}
hasNetwork == NetworkStatus.Connected -> SyncIndicatorState.ServerUnreachable
else -> SyncIndicatorState.Hidden
}
isOnline && syncIndicator == RoomListService.SyncIndicator.Show
}
}
var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
@@ -129,12 +109,6 @@ class LoggedInPresenter(
}.launchIn(this)
}
LaunchedEffect(Unit) {
// Keep automatic call status (m.call) in sync with homeserver support.
val enabled = matrixClient.isUserStatusSupported().getOrDefault(false)
matrixClient.enableAutomaticCallStatus(enabled)
}
val networkConnectivity by networkMonitor.connectivity.collectAsState()
LaunchedEffect(networkConnectivity) {
if (networkConnectivity == NetworkStatus.Connected) {
@@ -143,24 +117,9 @@ class LoggedInPresenter(
}
}
val localNetworkPermissionState = localNetworkPermissionsPresenter.present()
var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) }
val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) {
value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl)
}
LaunchedEffect(localNetworkPermissionState.permissionGranted) {
if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false
}
val localNetworkPermissionDialog = when {
!advisorRequestsPrompt -> LocalNetworkPermissionDialog.None
localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None
localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale
else -> LocalNetworkPermissionDialog.Settings
}
fun handleEvent(event: LoggedInEvent) {
fun handleEvent(event: LoggedInEvents) {
when (event) {
is LoggedInEvent.CloseErrorDialog -> {
is LoggedInEvents.CloseErrorDialog -> {
pusherRegistrationState.value = AsyncData.Uninitialized
if (event.doNotShowAgain) {
coroutineScope.launch {
@@ -168,34 +127,22 @@ class LoggedInPresenter(
}
}
}
LoggedInEvent.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
forceNativeSlidingSyncMigration = matrixClient.needsForcedNativeSlidingSyncMigration().getOrDefault(false)
}
LoggedInEvent.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
// Force the logout since Native Sliding Sync is already enforced by the SDK
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
}
LoggedInEvent.DismissLocalNetworkPermissionPrompt -> {
localNetworkPromptDismissedThisSession = true
}
LoggedInEvent.RequestLocationNetworkPermission -> {
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
} else {
localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
localNetworkPromptDismissedThisSession = true
}
}
}
return LoggedInState(
syncIndicatorState = syncIndicatorState,
showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
appName = buildMeta.applicationName,
localNetworkPermissionDialog = localNetworkPermissionDialog,
eventSink = ::handleEvent,
)
}
@@ -9,20 +9,12 @@
package io.element.android.appnav.loggedin
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
enum class SyncIndicatorState {
Hidden,
Syncing,
ServerUnreachable,
}
data class LoggedInState(
val syncIndicatorState: SyncIndicatorState,
val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean,
val appName: String,
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
val eventSink: (LoggedInEvent) -> Unit,
val eventSink: (LoggedInEvents) -> Unit,
)
@@ -10,34 +10,28 @@ package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.push.api.PusherRegistrationFailure
open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState>
get() = sequenceOf(
aLoggedInState(),
aLoggedInState(syncIndicatorState = SyncIndicatorState.Syncing),
aLoggedInState(syncIndicatorState = SyncIndicatorState.ServerUnreachable),
aLoggedInState(showSyncSpinner = true),
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
aLoggedInState(forceNativeSlidingSyncMigration = true),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings),
)
}
fun aLoggedInState(
syncIndicatorState: SyncIndicatorState = SyncIndicatorState.Hidden,
showSyncSpinner: Boolean = false,
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
forceNativeSlidingSyncMigration: Boolean = false,
appName: String = "Element X",
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
) = LoggedInState(
syncIndicatorState = syncIndicatorState,
showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState,
ignoreRegistrationError = false,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
appName = appName,
localNetworkPermissionDialog = localNetworkPermissionDialog,
eventSink = {},
)

Some files were not shown because too many files have changed in this diff Show More