Browse Source

Merge branch 'release/0.2.0' into main

pull/1368/head v0.2.0
Benoit Marty 1 year ago
parent
commit
141cac7d53
  1. 2
      .github/workflows/build.yml
  2. 2
      .github/workflows/danger.yml
  3. 2
      .github/workflows/gradle-wrapper-update.yml
  4. 2
      .github/workflows/gradle-wrapper-validation.yml
  5. 2
      .github/workflows/maestro.yml
  6. 2
      .github/workflows/nightly.yml
  7. 2
      .github/workflows/nightlyReports.yml
  8. 10
      .github/workflows/quality.yml
  9. 2
      .github/workflows/release.yml
  10. 14
      .github/workflows/sonar.yml
  11. 2
      .github/workflows/sync-localazy.yml
  12. 10
      .github/workflows/tests.yml
  13. 2
      .maestro/tests/account/changeServer.yaml
  14. 2
      .maestro/tests/assertions/assertAnalyticsDisplayed.yaml
  15. 2
      .maestro/tests/assertions/assertHomeDisplayed.yaml
  16. 2
      .maestro/tests/assertions/assertInitDisplayed.yaml
  17. 2
      .maestro/tests/assertions/assertLoginDisplayed.yaml
  18. 2
      .maestro/tests/assertions/assertRoomListSynced.yaml
  19. 2
      .maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml
  20. 3
      .maestro/tests/roomList/timeline/messages/text.yaml
  21. 39
      CHANGES.md
  22. 1
      app/build.gradle.kts
  23. 10
      app/src/debug/res/drawable/ic_launcher_background.xml
  24. 3
      app/src/main/AndroidManifest.xml
  25. BIN
      app/src/main/ic_launcher-playstore.png
  26. 27
      app/src/main/kotlin/io/element/android/x/icon/IconPreview.kt
  27. 3
      app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt
  28. 20
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  29. 20
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  30. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  31. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  32. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_background.png
  33. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_background.webp
  34. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  35. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
  36. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp
  37. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  38. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  39. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  40. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  41. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_background.png
  42. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_background.webp
  43. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  44. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
  45. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp
  46. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  47. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  48. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  49. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  50. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
  51. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
  52. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  53. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
  54. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp
  55. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  56. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  57. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  58. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  59. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
  60. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
  61. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  62. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
  63. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp
  64. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  65. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  66. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  67. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  68. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
  69. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
  70. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  71. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
  72. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp
  73. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  74. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  75. 11
      app/src/main/res/xml/backup_rules.xml
  76. 16
      app/src/main/res/xml/data_extraction_rules.xml
  77. 10
      app/src/nightly/res/drawable/ic_launcher_background.xml
  78. 2
      app/src/release/res/drawable/ic_launcher_background.xml
  79. 2
      appnav/build.gradle.kts
  80. 5
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  81. 12
      appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
  82. 38
      appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
  83. 3
      appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
  84. 2
      appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
  85. 9
      appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
  86. 5
      appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
  87. 15
      appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
  88. 22
      build.gradle.kts
  89. 69
      docs/continuous_integration.md
  90. 2
      fastlane/metadata/android/en-US/changelogs/40002000.txt
  91. 1
      features/analytics/impl/build.gradle.kts
  92. 10
      features/analytics/impl/src/main/res/values-de/translations.xml
  93. 10
      features/analytics/impl/src/main/res/values-fr/translations.xml
  94. 5
      features/analytics/impl/src/main/res/values-zh-rTW/translations.xml
  95. 6
      features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt
  96. 6
      features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt
  97. 37
      features/call/build.gradle.kts
  98. 61
      features/call/src/main/AndroidManifest.xml
  99. 89
      features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt
  100. 45
      features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt
  101. Some files were not shown because too many files have changed in this diff Show More

2
.github/workflows/build.yml

@ -27,7 +27,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }} group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881

2
.github/workflows/danger.yml

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Danger main check name: Danger main check
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: | - run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger - name: Danger

2
.github/workflows/gradle-wrapper-update.yml

@ -8,7 +8,7 @@ jobs:
update-gradle-wrapper: update-gradle-wrapper:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Update Gradle Wrapper - name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1 uses: gradle-update/update-gradle-wrapper-action@v1
# Skip in forks # Skip in forks

2
.github/workflows/gradle-wrapper-validation.yml

@ -11,5 +11,5 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# No concurrency required, this is a prerequisite to other actions and should run every time. # No concurrency required, this is a prerequisite to other actions and should run every time.
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v1

2
.github/workflows/maestro.yml

@ -24,7 +24,7 @@ jobs:
group: ${{ format('maestro-{0}', github.ref) }} group: ${{ format('maestro-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881

2
.github/workflows/nightly.yml

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository == 'vector-im/element-x-android' }} if: ${{ github.repository == 'vector-im/element-x-android' }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:

2
.github/workflows/nightlyReports.yml

@ -55,7 +55,7 @@ jobs:
name: Dependency analysis name: Dependency analysis
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:

10
.github/workflows/quality.yml

@ -17,7 +17,7 @@ jobs:
name: Search for forbidden patterns name: Search for forbidden patterns
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Run code quality check suite - name: Run code quality check suite
run: ./tools/check/check_code_quality.sh run: ./tools/check/check_code_quality.sh
@ -29,7 +29,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@ -52,12 +52,6 @@ jobs:
name: linting-report name: linting-report
path: | path: |
*/build/reports/**/*.* */build/reports/**/*.*
- name: 🔊 Publish results to Sonar
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
- name: Prepare Danger - name: Prepare Danger
if: always() if: always()
run: | run: |

2
.github/workflows/release.yml

@ -18,7 +18,7 @@ jobs:
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }} group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:

14
.github/workflows/sonar.yml

@ -1,4 +1,4 @@
name: Code Quality Checks name: Sonar
on: on:
workflow_dispatch: workflow_dispatch:
@ -10,18 +10,18 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon --warn
jobs: jobs:
sonar: sonar:
name: Project Check Suite name: Sonar Quality Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('sonar-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('sonar-develop-{0}', github.sha) || format('sonar-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('sonar-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('sonar-develop-{0}', github.sha) || format('sonar-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@ -41,9 +41,3 @@ jobs:
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
- 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

2
.github/workflows/sync-localazy.yml

@ -11,7 +11,7 @@ jobs:
# Skip in forks # Skip in forks
if: github.repository == 'vector-im/element-x-android' if: github.repository == 'vector-im/element-x-android'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python 3.9 - name: Set up Python 3.9
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:

10
.github/workflows/tests.yml

@ -22,6 +22,16 @@ jobs:
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) }} 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 cancel-in-progress: true
steps: steps:
# Increase swapfile size to prevent screenshot tests getting terminated
# https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749
- name: 💽 Increase swapfile size
run: |
sudo swapoff -a
sudo fallocate -l 8G /mnt/swapfile
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
sudo swapon --show
- name: ⏬ Checkout with LFS - name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
with: with:

2
.maestro/tests/account/changeServer.yaml

@ -15,7 +15,7 @@ appId: ${APP_ID}
- tapOn: "gnuradio.org" - tapOn: "gnuradio.org"
- extendedWaitUntil: - extendedWaitUntil:
visible: "This server currently doesn’t support sliding sync." visible: "This server currently doesn’t support sliding sync."
timeout: 10_000 timeout: 10000
- tapOn: "Cancel" - tapOn: "Cancel"
- back - back
- back - back

2
.maestro/tests/assertions/assertAnalyticsDisplayed.yaml

@ -2,4 +2,4 @@ appId: ${APP_ID}
--- ---
- extendedWaitUntil: - extendedWaitUntil:
visible: "Help improve Element X dbg" visible: "Help improve Element X dbg"
timeout: 10_000 timeout: 10000

2
.maestro/tests/assertions/assertHomeDisplayed.yaml

@ -2,4 +2,4 @@ appId: ${APP_ID}
--- ---
- extendedWaitUntil: - extendedWaitUntil:
visible: "All Chats" visible: "All Chats"
timeout: 10_000 timeout: 10000

2
.maestro/tests/assertions/assertInitDisplayed.yaml

@ -2,4 +2,4 @@ appId: ${APP_ID}
--- ---
- extendedWaitUntil: - extendedWaitUntil:
visible: "Be in your element" visible: "Be in your element"
timeout: 10_000 timeout: 10000

2
.maestro/tests/assertions/assertLoginDisplayed.yaml

@ -2,4 +2,4 @@ appId: ${APP_ID}
--- ---
- extendedWaitUntil: - extendedWaitUntil:
visible: "Change account provider" visible: "Change account provider"
timeout: 10_000 timeout: 10000

2
.maestro/tests/assertions/assertRoomListSynced.yaml

@ -2,4 +2,4 @@ appId: ${APP_ID}
--- ---
- extendedWaitUntil: - extendedWaitUntil:
visible: ${ROOM_NAME} visible: ${ROOM_NAME}
timeout: 10_000 timeout: 10000

2
.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml

@ -3,4 +3,4 @@ appId: ${APP_ID}
- extendedWaitUntil: - extendedWaitUntil:
visible: visible:
id: "welcome_screen-title" id: "welcome_screen-title"
timeout: 10_000 timeout: 10000

3
.maestro/tests/roomList/timeline/messages/text.yaml

@ -1,7 +1,8 @@
appId: ${APP_ID} appId: ${APP_ID}
--- ---
- takeScreenshot: build/maestro/510-Timeline - takeScreenshot: build/maestro/510-Timeline
- tapOn: "Message" - tapOn:
id: "rich_text_editor"
- inputText: "Hello world!" - inputText: "Hello world!"
- tapOn: "Send" - tapOn: "Send"
- hideKeyboard - hideKeyboard

39
CHANGES.md

@ -1,3 +1,42 @@
Changes in Element X v0.2.0 (2023-09-18)
========================================
Features ✨
----------
- Bump Rust SDK to `v0.1.54`
- Add a "Mute" shortcut icon and a "Notifications" section in the room details screen ([#506](https://github.com/vector-im/element-x-android/issues/506))
- Add a notification permission screen to the initial flow. ([#897](https://github.com/vector-im/element-x-android/issues/897))
- Integrate Element Call into EX by embedding a call in a WebView. ([#1300](https://github.com/vector-im/element-x-android/issues/1300))
- Implement Bloom effect modifier. ([#1217](https://github.com/vector-im/element-x-android/issues/1217))
- Set color on display name and default avatar in the timeline. ([#1224](https://github.com/vector-im/element-x-android/issues/1224))
- Display a thread decorator in timeline so we know when a message is coming from a thread. ([#1236](https://github.com/vector-im/element-x-android/issues/1236))
- [Rich text editor] Integrate rich text editor library. Note that markdown is now not supported and further formatting support will be introduced through the rich text editor. ([#1172](https://github.com/vector-im/element-x-android/issues/1172))
- [Rich text editor] Add formatting menu (accessible via the '+' button) ([#1261](https://github.com/vector-im/element-x-android/issues/1261))
- [Rich text editor] Add feature flag for rich text editor. Markdown support can now be enabled by disabling the rich text editor. ([#1289](https://github.com/vector-im/element-x-android/issues/1289))
- [Rich text editor] Update design ([#1332](https://github.com/vector-im/element-x-android/issues/1332))
Bugfixes 🐛
----------
- Make links in room topic clickable ([#612](https://github.com/vector-im/element-x-android/issues/612))
- Reply action: harmonize conditions in bottom sheet and swipe to reply. ([#1173](https://github.com/vector-im/element-x-android/issues/1173))
- Fix system bar color after login on light theme. ([#1222](https://github.com/vector-im/element-x-android/issues/1222))
- Fix long click on simple formatted messages ([#1232](https://github.com/vector-im/element-x-android/issues/1232))
- Enable polls in release build. ([#1241](https://github.com/vector-im/element-x-android/issues/1241))
- Fix top padding in room list when app is opened in offline mode. ([#1297](https://github.com/vector-im/element-x-android/issues/1297))
- [Rich text editor] Fix 'text formatting' option only partially visible ([#1335](https://github.com/vector-im/element-x-android/issues/1335))
- [Rich text editor] Ensure keyboard opens for reply and text formatting modes ([#1337](https://github.com/vector-im/element-x-android/issues/1337))
- [Rich text editor] Fix placeholder spilling onto multiple lines ([#1347](https://github.com/vector-im/element-x-android/issues/1347))
Other changes
-------------
- Add a sub-screen "Notifications" in the existing application Settings ([#510](https://github.com/vector-im/element-x-android/issues/510))
- Exclude some groups related to analytics to be included. ([#1191](https://github.com/vector-im/element-x-android/issues/1191))
- Use the new SyncIndicator API. ([#1244](https://github.com/vector-im/element-x-android/issues/1244))
- Improve RoomSummary mapping by using RoomInfo. ([#1251](https://github.com/vector-im/element-x-android/issues/1251))
- Ensure Posthog data are sent to "https://posthog.element.io" ([#1269](https://github.com/vector-im/element-x-android/issues/1269))
- New app icon, with monochrome support. ([#1363](https://github.com/vector-im/element-x-android/issues/1363))
Changes in Element X v0.1.6 (2023-09-04) Changes in Element X v0.1.6 (2023-09-04)
======================================== ========================================

1
app/build.gradle.kts

@ -198,6 +198,7 @@ dependencies {
allLibrariesImpl() allLibrariesImpl()
allServicesImpl() allServicesImpl()
allFeaturesImpl(rootDir, logger) allFeaturesImpl(rootDir, logger)
implementation(projects.features.call)
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
implementation(projects.appnav) implementation(projects.appnav)
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)

10
app/src/debug/res/drawable/ic_launcher_background.xml

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#F7F07E"
android:fillType="evenOdd"
android:pathData="m0,0h108v108h-108z" />
</vector>

3
app/src/main/AndroidManifest.xml

@ -22,8 +22,9 @@
<application <application
android:name=".ElementXApplication" android:name=".ElementXApplication"
android:allowBackup="true" android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

BIN
app/src/main/ic_launcher-playstore.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 260 KiB

27
app/src/main/kotlin/io/element/android/x/icon/IconPreview.kt

@ -17,13 +17,20 @@
package io.element.android.x.icon package io.element.android.x.icon
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.x.R import io.element.android.x.R
@Preview @Preview
@ -47,3 +54,23 @@ internal fun RoundIconPreview(
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null) Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
} }
} }
@Preview
@Composable
internal fun MonochromeIconPreview(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(108.dp)
.background(Color(0xFF2F3133))
.clip(shape = RoundedCornerShape(32.dp)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.mipmap.ic_launcher_monochrome),
colorFilter = ColorFilter.tint(Color(0xFFC3E0F6)),
contentDescription = null
)
}
}

3
app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt

@ -17,6 +17,7 @@
package io.element.android.x.initializer package io.element.android.x.initializer
import android.content.Context import android.content.Context
import android.system.Os
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.startup.Initializer import androidx.startup.Initializer
import io.element.android.features.preferences.impl.developer.tracing.SharedPrefTracingConfigurationStore import io.element.android.features.preferences.impl.developer.tracing.SharedPrefTracingConfigurationStore
@ -57,6 +58,8 @@ class TracingInitializer : Initializer<Unit> {
} }
bugReporter.cleanLogDirectoryIfNeeded() bugReporter.cleanLogDirectoryIfNeeded()
tracingService.setupTracing(tracingConfiguration) tracingService.setupTracing(tracingConfiguration)
// Also set env variable for rust back trace
Os.setenv("RUST_BACKTRACE", "1", true)
} }
override fun dependencies(): List<Class<out Initializer<*>>> = mutableListOf() override fun dependencies(): List<Class<out Initializer<*>>> = mutableListOf()

20
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@ -1,6 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2023 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<!-- Waiting for design monochrome android:drawable="@mipmap/ic_launcher_monochrome" /--> <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

20
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@ -1,6 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2023 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<!-- Waiting for design monochrome android:drawable="@mipmap/ic_launcher_monochrome" /--> <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_background.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_background.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_background.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_background.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_background.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

11
app/src/main/res/xml/backup_rules.xml

@ -15,15 +15,8 @@
--> -->
<!-- <!--
Sample backup rules file; uncomment and customize as necessary. All backup is disabled since it would clash with encryption.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
--> -->
<full-backup-content> <full-backup-content>
<!-- <exclude domain="root" path="." />
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content> </full-backup-content>

16
app/src/main/res/xml/data_extraction_rules.xml

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- <?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 New Vector Ltd ~ Copyright (c) 2023 New Vector Ltd
~ ~
~ Licensed under the Apache License, Version 2.0 (the "License"); ~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License. ~ you may not use this file except in compliance with the License.
@ -15,21 +15,13 @@
--> -->
<!-- <!--
Sample data extraction rules file; uncomment and customize as necessary. All backup is disabled since it would clash with encryption.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
--> -->
<data-extraction-rules> <data-extraction-rules>
<cloud-backup> <cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up. <exclude domain="root" path="." />
<include .../>
<exclude .../>
-->
</cloud-backup> </cloud-backup>
<!--
<device-transfer> <device-transfer>
<include .../> <exclude domain="root" path="." />
<exclude .../>
</device-transfer> </device-transfer>
-->
</data-extraction-rules> </data-extraction-rules>

10
app/src/nightly/res/drawable/ic_launcher_background.xml

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#07007E"
android:fillType="evenOdd"
android:pathData="m0,0h108v108h-108z" />
</vector>

2
app/src/release/res/drawable/ic_launcher_background.xml

@ -0,0 +1,2 @@
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@mipmap/ic_launcher_background" />

2
appnav/build.gradle.kts

@ -48,8 +48,6 @@ dependencies {
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)
implementation(libs.coil) implementation(libs.coil)

5
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -60,7 +60,6 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.StartSyncReason
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
@ -125,7 +124,7 @@ class LoggedInFlowNode @AssistedInject constructor(
onStop = { onStop = {
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method. //Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
coroutineScope.launch { coroutineScope.launch {
syncService.stopSync(StartSyncReason.AppInForeground) syncService.stopSync()
} }
}, },
onDestroy = { onDestroy = {
@ -151,7 +150,7 @@ class LoggedInFlowNode @AssistedInject constructor(
.collect { (syncState, networkStatus) -> .collect { (syncState, networkStatus) ->
Timber.d("Sync state: $syncState, network status: $networkStatus") Timber.d("Sync state: $syncState, network status: $networkStatus")
if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) { if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) {
syncService.startSync(StartSyncReason.AppInForeground) syncService.startSync()
} }
} }
} }

12
appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt

@ -32,6 +32,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.onboarding.api.OnBoardingEntryPoint import io.element.android.features.onboarding.api.OnBoardingEntryPoint
import io.element.android.features.preferences.api.ConfigureTracingEntryPoint
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ -43,6 +44,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val onBoardingEntryPoint: OnBoardingEntryPoint, private val onBoardingEntryPoint: OnBoardingEntryPoint,
private val configureTracingEntryPoint: ConfigureTracingEntryPoint,
private val loginEntryPoint: LoginEntryPoint, private val loginEntryPoint: LoginEntryPoint,
private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory, private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory,
) : BackstackNode<NotLoggedInFlowNode.NavTarget>( ) : BackstackNode<NotLoggedInFlowNode.NavTarget>(
@ -70,6 +72,9 @@ class NotLoggedInFlowNode @AssistedInject constructor(
data class LoginFlow( data class LoginFlow(
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
) : NavTarget ) : NavTarget
@Parcelize
data object ConfigureTracing : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -83,6 +88,10 @@ class NotLoggedInFlowNode @AssistedInject constructor(
override fun onSignIn() { override fun onSignIn() {
backstack.push(NavTarget.LoginFlow(isAccountCreation = false)) backstack.push(NavTarget.LoginFlow(isAccountCreation = false))
} }
override fun onOpenDeveloperSettings() {
backstack.push(NavTarget.ConfigureTracing)
}
} }
onBoardingEntryPoint onBoardingEntryPoint
.nodeBuilder(this, buildContext) .nodeBuilder(this, buildContext)
@ -94,6 +103,9 @@ class NotLoggedInFlowNode @AssistedInject constructor(
.params(LoginEntryPoint.Params(isAccountCreation = navTarget.isAccountCreation)) .params(LoginEntryPoint.Params(isAccountCreation = navTarget.isAccountCreation))
.build() .build()
} }
NavTarget.ConfigureTracing -> {
configureTracingEntryPoint.createNode(this, buildContext)
}
} }
} }

38
appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt

@ -16,44 +16,26 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import android.Manifest
import android.os.Build
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import kotlinx.coroutines.delay
import javax.inject.Inject import javax.inject.Inject
private const val DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS = 1500L
class LoggedInPresenter @Inject constructor( class LoggedInPresenter @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val pushService: PushService, private val pushService: PushService,
) : Presenter<LoggedInState> { ) : Presenter<LoggedInState> {
private val postNotificationPermissionsPresenter by lazy {
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
} else {
NoopPermissionsPresenter()
}
}
@Composable @Composable
override fun present(): LoggedInState { override fun present(): LoggedInState {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -64,25 +46,15 @@ class LoggedInPresenter @Inject constructor(
pushService.registerWith(matrixClient, pushProvider, distributor) pushService.registerWith(matrixClient, pushProvider, distributor)
} }
val roomListState by matrixClient.roomListService.state.collectAsState() val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
val networkStatus by networkMonitor.connectivity.collectAsState() val networkStatus by networkMonitor.connectivity.collectAsState()
val permissionsState = postNotificationPermissionsPresenter.present() val showSyncSpinner by remember {
var showSyncSpinner by remember { derivedStateOf {
mutableStateOf(false) networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
}
LaunchedEffect(roomListState, networkStatus) {
showSyncSpinner = when {
networkStatus == NetworkStatus.Offline -> false
roomListState == RoomListService.State.Running -> false
else -> {
delay(DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS)
true
}
} }
} }
return LoggedInState( return LoggedInState(
showSyncSpinner = showSyncSpinner, showSyncSpinner = showSyncSpinner,
permissionsState = permissionsState,
) )
} }
} }

3
appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt

@ -16,9 +16,6 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import io.element.android.libraries.permissions.api.PermissionsState
data class LoggedInState( data class LoggedInState(
val showSyncSpinner: Boolean, val showSyncSpinner: Boolean,
val permissionsState: PermissionsState,
) )

2
appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt

@ -17,7 +17,6 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> { open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState> override val values: Sequence<LoggedInState>
@ -32,5 +31,4 @@ fun aLoggedInState(
showSyncSpinner: Boolean = true, showSyncSpinner: Boolean = true,
) = LoggedInState( ) = LoggedInState(
showSyncSpinner = showSyncSpinner, showSyncSpinner = showSyncSpinner,
permissionsState = createDummyPostNotificationPermissionsState(),
) )

9
appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt

@ -23,21 +23,16 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.androidutils.system.openAppSettingsPage
import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.permissions.api.PermissionsView
@Composable @Composable
fun LoggedInView( fun LoggedInView(
state: LoggedInState, state: LoggedInState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
@ -49,10 +44,6 @@ fun LoggedInView(
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
isVisible = state.showSyncSpinner, isVisible = state.showSyncSpinner,
) )
PermissionsView(
state = state.permissionsState,
openSystemSettings = context::openAppSettingsPage
)
} }
} }

5
appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt

@ -31,10 +31,15 @@ import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolde
import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService import io.element.android.services.apperror.impl.DefaultAppErrorStateService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test import org.junit.Test
class RootPresenterTest { class RootPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createPresenter() val presenter = createPresenter()

15
appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt

@ -26,16 +26,20 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test import org.junit.Test
class LoggedInPresenterTest { class LoggedInPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createPresenter() val presenter = createPresenter()
@ -43,7 +47,7 @@ class LoggedInPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.permissionsState.permission).isEmpty() assertThat(initialState.showSyncSpinner).isFalse()
} }
} }
@ -68,11 +72,6 @@ class LoggedInPresenterTest {
): LoggedInPresenter { ): LoggedInPresenter {
return LoggedInPresenter( return LoggedInPresenter(
matrixClient = FakeMatrixClient(roomListService = roomListService), matrixClient = FakeMatrixClient(roomListService = roomListService),
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permission: String): PermissionsPresenter {
return NoopPermissionsPresenter()
}
},
networkMonitor = FakeNetworkMonitor(networkStatus), networkMonitor = FakeNetworkMonitor(networkStatus),
pushService = object : PushService { pushService = object : PushService {
override fun notificationStyleChanged() { override fun notificationStyleChanged() {

22
build.gradle.kts

@ -6,7 +6,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript { buildscript {
dependencies { dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
classpath("com.google.gms:google-services:4.3.15") classpath("com.google.gms:google-services:4.4.0")
} }
} }
@ -62,7 +62,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml")) config.from(files("$rootDir/tools/detekt/detekt.yml"))
} }
dependencies { dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.2.1") detektPlugins("io.nlopez.compose.rules:detekt:0.2.2")
} }
// KtLint // KtLint
@ -143,22 +143,6 @@ sonar {
} }
} }
allprojects {
val projectDir = projectDir.toString()
sonar {
properties {
// Note: folders `kotlin` are not supported (yet), I asked on their side: https://community.sonarsource.com/t/82824
// As a workaround provide the path in `sonar.sources` property.
if (File("$projectDir/src/main/kotlin").exists()) {
property("sonar.sources", "src/main/kotlin")
}
if (File("$projectDir/src/test/kotlin").exists()) {
property("sonar.tests", "src/test/kotlin")
}
}
}
}
allprojects { allprojects {
tasks.withType<Test> { tasks.withType<Test> {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
@ -261,6 +245,8 @@ koverMerged {
includes += "*Presenter" includes += "*Presenter"
excludes += "*Fake*Presenter" excludes += "*Fake*Presenter"
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
// Some options can't be tested at the moment
excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
} }
bound { bound {
minValue = 85 minValue = 85

69
docs/continuous_integration.md

@ -0,0 +1,69 @@
# Continuous integration strategy
<!--- TOC -->
* [Introduction](#introduction)
* [CI tools](#ci-tools)
* [Rules](#rules)
* [What is the CI checking](#what-is-the-ci-checking)
* [What is the CI reporting](#what-is-the-ci-reporting)
* [Current choices](#current-choices)
* [R8 task](#r8-task)
* [Android test (connected test)](#android-test-connected-test)
<!--- END -->
## Introduction
This document gives some information about how we take advantage of the continuous integration (CI).
## CI tools
We use GitHub Actions to configure and perform the CI.
## Rules
We want:
1. The CI to detect as soon as possible any issue in the code
2. The CI to be fast - it's run on all the Pull Requests, and developers do not like to wait too long
3. The CI to be reliable - it should not fail randomly
4. The CI to generate artifacts which can be used by the team and the community
5. The CI to generate useful logs and reports, not too verbose, not too short
6. The developer to be able to run the CI locally - to help with this we have [a script](../tools/check/check_code_quality.sh) the can be run locally and which does more checks that just building and deploying the app.
7. The CI to be used as a common environment for the team: generate the screenshots image for the screenshot test, build the release build (unsigned)
8. The CI to run repeated tasks, like building the nightly builds, integrating data from external tools (translations, etc.)
9. The CI to upgrade our dependencies (Renovate)
10. The CI to do some issue triaging
## What is the CI checking
The CI checks that:
1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app
2. The tests are passing
3. The code quality is good (detekt, ktlint, lint)
4. The code is running and smoke tests are passing (maestro)
5. The PullRequest itself is good (with danger)
6. Files that must be added with git-lfs are added with git-lfs
## What is the CI reporting
The CI reports:
1. Code coverage reports
2. Sonar reports
## Current choices
### R8 task
The CI does not run R8 because it's too slow, and it breaks rule 2.
The drawback is that the nightly build can fail, as well as the release build.
Since the nightly build is failing, the team can detect the failure quite fast and react to it.
### Android test (connected test)
We limit the number of connected tests (tests under folder `androidTest`), because it often break rule 2 and 3.

2
fastlane/metadata/android/en-US/changelogs/40002000.txt

@ -0,0 +1,2 @@
Main changes in this version: Element Call, design update, bugfixes
Full changelog: https://github.com/vector-im/element-x-android/releases

1
features/analytics/impl/build.gradle.kts

@ -51,4 +51,5 @@ dependencies {
testImplementation(libs.test.mockk) testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.tests.testutils)
} }

10
features/analytics/impl/src/main/res/values-de/translations.xml

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Wir werden keine personenbezogenen Daten aufzeichnen oder auswerten"</string> <string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string> <string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string> <string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Bedingungen lesen %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string> <string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string> <string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string> <string name="screen_analytics_prompt_third_party_sharing">"Wir geben Ihre Daten nicht an Dritte weiter"</string>
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string> <string name="screen_analytics_prompt_title">"Hilf uns %1$s zu verbessern"</string>
</resources> </resources>

10
features/analytics/impl/src/main/res/values-fr/translations.xml

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Nous n\'enregistrerons ni ne traiterons aucune donnée personnelle"</string> <string name="screen_analytics_prompt_data_usage">"Nous n’enregistrerons ni ne profilerons aucune donnée personnelle"</string>
<string name="screen_analytics_prompt_help_us_improve">"Partagez des données d\'utilisation anonymes pour nous aider à identifier les problèmes."</string> <string name="screen_analytics_prompt_help_us_improve">"Partagez des données dutilisation anonymes pour nous aider à identifier les problèmes."</string>
<string name="screen_analytics_prompt_read_terms">"Consultez nos conditions d\'utilisation %1$s."</string> <string name="screen_analytics_prompt_read_terms">"Vous pouvez lire toutes nos conditions %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"ici"</string> <string name="screen_analytics_prompt_read_terms_content_link">"ici"</string>
<string name="screen_analytics_prompt_settings">"Vous pouvez désactiver cette fonction à tout moment"</string> <string name="screen_analytics_prompt_settings">"Vous pouvez le désactiver à tout moment"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Nous ne partagerons pas vos données avec des tiers"</string> <string name="screen_analytics_prompt_third_party_sharing">"Nous ne partagerons pas vos données avec des tiers"</string>
<string name="screen_analytics_prompt_title">"Aidez-nous à améliorer %1$s"</string> <string name="screen_analytics_prompt_title">"Aidez à améliorer %1$s"</string>
</resources> </resources>

5
features/analytics/impl/src/main/res/values-zh-rTW/translations.xml

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string>
<string name="screen_analytics_prompt_help_us_improve">"分享匿名的使用數據以協助我們釐清問題"</string>
<string name="screen_analytics_prompt_read_terms">"您可以到 %1$s 閱讀我們的條款。"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string>
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string> <string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string> <string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>
<string name="screen_analytics_prompt_title">"讓 %1$s 變得更好"</string>
</resources> </resources>

6
features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt

@ -23,11 +23,17 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test import org.junit.Test
class AnalyticsOptInPresenterTest { class AnalyticsOptInPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test @Test
fun `present - enable`() = runTest { fun `present - enable`() = runTest {
val analyticsService = FakeAnalyticsService(isEnabled = false) val analyticsService = FakeAnalyticsService(isEnabled = false)

6
features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt

@ -23,10 +23,16 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test import org.junit.Test
class AnalyticsPreferencesPresenterTest { class AnalyticsPreferencesPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test @Test
fun `present - initial state available`() = runTest { fun `present - initial state available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = DefaultAnalyticsPreferencesPresenter(

37
features/call/build.gradle.kts

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
}
android {
namespace = "io.element.android.features.call"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.network)
implementation(libs.androidx.webkit)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
}

61
features/call/src/main/AndroidManifest.xml

@ -0,0 +1,61 @@
<!--
~ Copyright (c) 2023 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application>
<activity
android:name=".ElementCallActivity"
android:label="@string/element_call"
android:exported="true"
android:taskAffinity="io.element.android.features.call"
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
android:launchMode="singleTask">
<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="http" />
<data android:scheme="https" />
<data android:host="call.element.io" />
</intent-filter>
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="element" />
<data android:host="call" />
</intent-filter>
</activity>
<service android:name=".CallForegroundService" android:enabled="true" android:foregroundServiceType="mediaPlayback" />
</application>
</manifest>

89
features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt

@ -0,0 +1,89 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.call
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.graphics.drawable.IconCompat
import io.element.android.libraries.designsystem.utils.CommonDrawables
class CallForegroundService : Service() {
companion object {
fun start(context: Context) {
val intent = Intent(context, CallForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
val intent = Intent(context, CallForegroundService::class.java)
context.stopService(intent)
}
}
private lateinit var notificationManagerCompat: NotificationManagerCompat
override fun onCreate() {
super.onCreate()
notificationManagerCompat = NotificationManagerCompat.from(this)
val foregroundServiceChannel = NotificationChannelCompat.Builder(
"call_foreground_service_channel",
NotificationManagerCompat.IMPORTANCE_LOW,
).setName(
getString(R.string.call_foreground_service_channel_title_android).ifEmpty { "Ongoing call" }
).build()
notificationManagerCompat.createNotificationChannel(foregroundServiceChannel)
val callActivityIntent = Intent(this, ElementCallActivity::class.java)
val pendingIntent = PendingIntentCompat.getActivity(this, 0, callActivityIntent, 0, false)
val notification = NotificationCompat.Builder(this, foregroundServiceChannel.id)
.setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification_small))
.setContentTitle(getString(R.string.call_foreground_service_title_android))
.setContentText(getString(R.string.call_foreground_service_message_android))
.setContentIntent(pendingIntent)
.build()
startForeground(1, notification)
}
@Suppress("DEPRECATION")
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

45
features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt

@ -0,0 +1,45 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.call
import android.net.Uri
import java.net.URLDecoder
object CallIntentDataParser {
private val validHttpSchemes = sequenceOf("http", "https")
fun parse(data: String?): String? {
val parsedUrl = data?.let { Uri.parse(data) } ?: return null
val scheme = parsedUrl.scheme
return when {
scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> data
scheme == "element" && parsedUrl.host == "call" -> {
// We use this custom scheme to load arbitrary URLs for other instances of Element Call,
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host
parsedUrl.getQueryParameter("url")
?.let { URLDecoder.decode(it, "utf-8") }
?.takeIf {
val internalUri = Uri.parse(it)
internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank()
}
}
// This should never be possible, but we still need to take into account the possibility
else -> null
}
}
}

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

Loading…
Cancel
Save