Merge branch 'release/0.5.0' into main
7
.github/pull_request_template.md
vendored
|
|
@ -1,12 +1,5 @@
|
|||
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
||||
|
||||
## Type of change
|
||||
|
||||
- [ ] Feature
|
||||
- [ ] Bugfix
|
||||
- [ ] Technical
|
||||
- [ ] Other :
|
||||
|
||||
## Content
|
||||
|
||||
<!-- Describe shortly what has been changed -->
|
||||
|
|
|
|||
2
.github/workflows/build_enterprise.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
name: Build Enterprise APKs
|
||||
runs-on: ubuntu-latest
|
||||
# Skip in forks
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [debug, release, nightly]
|
||||
|
|
|
|||
4
.github/workflows/danger.yml
vendored
|
|
@ -6,6 +6,8 @@ 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@v4
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
|
|
@ -13,7 +15,7 @@ jobs:
|
|||
with:
|
||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||
- name: Clone submodules
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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
|
||||
|
|
|
|||
2
.github/workflows/generate_github_pages.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
generate-github-pages:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip in forks
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- name: ⏬ Checkout with LFS
|
||||
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||
|
|
|
|||
2
.github/workflows/gradle-wrapper-update.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: Update Gradle Wrapper
|
||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||
# Skip in forks
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
target-branch: develop
|
||||
|
|
|
|||
19
.github/workflows/quality.yml
vendored
|
|
@ -20,10 +20,11 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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: Run code quality check suite
|
||||
run: ./tools/check/check_code_quality.sh
|
||||
|
|
@ -77,10 +78,11 @@ jobs:
|
|||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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 17
|
||||
uses: actions/setup-java@v4
|
||||
|
|
@ -116,10 +118,11 @@ jobs:
|
|||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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 17
|
||||
uses: actions/setup-java@v4
|
||||
|
|
@ -159,10 +162,11 @@ jobs:
|
|||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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 17
|
||||
uses: actions/setup-java@v4
|
||||
|
|
@ -198,10 +202,11 @@ jobs:
|
|||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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 17
|
||||
uses: actions/setup-java@v4
|
||||
|
|
@ -237,10 +242,11 @@ jobs:
|
|||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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 17
|
||||
uses: actions/setup-java@v4
|
||||
|
|
@ -271,6 +277,7 @@ jobs:
|
|||
name: Project Check Suite
|
||||
runs-on: ubuntu-latest
|
||||
needs: [konsist, lint, ktlint, detekt]
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
|
|
@ -42,6 +42,7 @@ jobs:
|
|||
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-gplay-{0}', github.sha) }}
|
||||
cancel-in-progress: true
|
||||
|
|
@ -49,6 +50,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
|
|
|
|||
2
.github/workflows/sync-localazy.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
sync-localazy:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip in forks
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use JDK 17
|
||||
|
|
|
|||
2
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
sync-sas-strings:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip in forks
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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@v4
|
||||
|
|
|
|||
3
.github/workflows/tests.yml
vendored
|
|
@ -40,10 +40,11 @@ jobs:
|
|||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@v0.9.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
|
||||
if: github.repository == 'element-hq/element-x-android'
|
||||
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 17
|
||||
uses: actions/setup-java@v4
|
||||
|
|
|
|||
74
CHANGES.md
|
|
@ -1,3 +1,77 @@
|
|||
Changes in Element X v0.4.16 (2024-07-05)
|
||||
=========================================
|
||||
|
||||
### ✨ Features
|
||||
* Avatar cluster for DM by @bmarty in https://github.com/element-hq/element-x-android/pull/3069
|
||||
* Feature : Draft support by @ganfra in https://github.com/element-hq/element-x-android/pull/3099
|
||||
* Timeline : re-enable edition of local echo by @ganfra in https://github.com/element-hq/element-x-android/pull/3126
|
||||
* Draft : add volatile storage when moving to edit mode. by @ganfra in https://github.com/element-hq/element-x-android/pull/3132
|
||||
|
||||
### 🙌 Improvements
|
||||
* Give locale and theme to Element Call by @bmarty in https://github.com/element-hq/element-x-android/pull/3118
|
||||
* Let the SDK retrieve and parse Element well known content by @bmarty in https://github.com/element-hq/element-x-android/pull/3127
|
||||
|
||||
### 🐛 Bugfixes
|
||||
* Let role and permissions screens works for invited room members too. by @bmarty in https://github.com/element-hq/element-x-android/pull/3081
|
||||
* Fix image rendering after clear cache by @bmarty in https://github.com/element-hq/element-x-android/pull/3082
|
||||
* Replace the 'answer' PendingIntent in ringing call notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3093
|
||||
* Use IO dispatcher for cleanup in bug reporter by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3092
|
||||
* Fix `@room` mentions crashing in debug builds by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3107
|
||||
* Auth : fix restore session when there is no network. by @ganfra in https://github.com/element-hq/element-x-android/pull/3109
|
||||
* Alert for incoming call even if notifications are disabled - WAITING FOR FINAL PRODUCT DECISION by @bmarty in https://github.com/element-hq/element-x-android/pull/3053
|
||||
* Fix incorrect 'device verified' screen when app was opened with no network connection by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3110
|
||||
* Draft : also clear draft when composer is blank by @ganfra in https://github.com/element-hq/element-x-android/pull/3115
|
||||
* Timeline : fix text item not refreshed when content change by @ganfra in https://github.com/element-hq/element-x-android/pull/3123
|
||||
* FFs can now be toggled in release builds too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3101
|
||||
* Fix crash when getting the system ringtone for ringing calls by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3131
|
||||
* Bugfix : avoid potential NPE on verification service. by @ganfra in https://github.com/element-hq/element-x-android/pull/3140
|
||||
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3114
|
||||
* Sync Strings - Add Greek translations by @ElementBot in https://github.com/element-hq/element-x-android/pull/3133
|
||||
|
||||
### 🧱 Build
|
||||
* Let GitHub generates the release notes by @bmarty in https://github.com/element-hq/element-x-android/pull/3105
|
||||
* Fix F-Droid reproducible build. by @bmarty in https://github.com/element-hq/element-x-android/pull/3106
|
||||
* Element enterprise (EE) foundations by @bmarty in https://github.com/element-hq/element-x-android/pull/3025
|
||||
* Fix Element Enterprise nightly build and publication using App Distribution by @bmarty in https://github.com/element-hq/element-x-android/pull/3130
|
||||
* Improve screenshot testing with ComposablePreviewScanner by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3125
|
||||
|
||||
### Dependency upgrades
|
||||
* Update dependency com.posthog:posthog-android to v3.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3060
|
||||
* Update danger/danger-js action to v12.3.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3059
|
||||
* Update dependency com.freeletics.flowredux:compose to v1.2.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3067
|
||||
* Update dependency com.google.firebase:firebase-bom to v33.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3062
|
||||
* Update dependency androidx.test.ext:junit to v1.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3088
|
||||
* Update test.core to v1.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3090
|
||||
* Remove dependencies androidx.test.espresso:espresso-core and androidx.appcompat:appcompat by @renovate in https://github.com/element-hq/element-x-android/pull/3087
|
||||
* Update wysiwyg to v2.37.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3094
|
||||
* Update dependency androidx.test:runner to v1.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3089
|
||||
* Update test.core to v1.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3104
|
||||
* Update dependency androidx.test:runner to v1.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3103
|
||||
* Update dependency androidx.test.ext:junit to v1.2.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3102
|
||||
* Update dependency com.google.truth:truth to v1.4.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3108
|
||||
* Update dependency com.posthog:posthog-android to v3.4.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3111
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.5 by @renovate in https://github.com/element-hq/element-x-android/pull/3116
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.29 by @renovate in https://github.com/element-hq/element-x-android/pull/3119
|
||||
* Update plugin dependencycheck to v10 by @renovate in https://github.com/element-hq/element-x-android/pull/3128
|
||||
* Update plugin dependencycheck to v10.0.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3129
|
||||
* Update dependency io.sentry:sentry-android to v7.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3122
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.30 by @renovate in https://github.com/element-hq/element-x-android/pull/3138
|
||||
|
||||
### Others
|
||||
* Feature/fga/sending queue iteration by @ganfra in https://github.com/element-hq/element-x-android/pull/3054
|
||||
* Use full date format for day dividers in timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3057
|
||||
* Let Dms use other member color. by @bmarty in https://github.com/element-hq/element-x-android/pull/3058
|
||||
* Resolve display names in mentions in real time by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3051
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3077
|
||||
* Improve the way we cut the bubble layout to give space for the sender Avatar by @bmarty in https://github.com/element-hq/element-x-android/pull/3080
|
||||
* Upgrade build tools and fix `pg-map-id` for F-Droid by @bmarty in https://github.com/element-hq/element-x-android/pull/3084
|
||||
* Improve room filtering behavior. by @bmarty in https://github.com/element-hq/element-x-android/pull/3083
|
||||
* Adapt our code to the new authentication APIs in the Rust SDK by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3068
|
||||
* Add temporary icon for Element Enterprise by @bmarty in https://github.com/element-hq/element-x-android/pull/3134
|
||||
* Improve click behavior on room timeline title by @bmarty in https://github.com/element-hq/element-x-android/pull/3064
|
||||
|
||||
Changes in Element X v0.4.15 (2024-06-19)
|
||||
=========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ plugins {
|
|||
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
||||
alias(libs.plugins.knit)
|
||||
id("kotlin-parcelize")
|
||||
id("com.google.android.gms.oss-licenses-plugin")
|
||||
// To be able to update the firebase.xml files, uncomment and build the project
|
||||
// id("com.google.gms.google-services")
|
||||
}
|
||||
|
|
@ -250,6 +251,7 @@ dependencies {
|
|||
implementation(projects.anvilannotations)
|
||||
implementation(projects.appnav)
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
anvil(projects.anvilcodegen)
|
||||
|
||||
// Comment to not include firebase in the project
|
||||
|
|
@ -257,6 +259,8 @@ dependencies {
|
|||
// Comment to not include unified push in the project
|
||||
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||
|
||||
"gplayImplementation"(libs.play.services.oss.licenses)
|
||||
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.x.licenses
|
||||
|
||||
import android.app.Activity
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class FdroidOpenSourceLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||
override val hasOpenSourceLicenses: Boolean = false
|
||||
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||
error("Not supported, please ensure that hasOpenSourcesLicenses is true before calling this method")
|
||||
}
|
||||
}
|
||||
12
app/src/gplay/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||
android:theme="@style/Theme.OssLicenses" />
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||
android:theme="@style/Theme.OssLicenses" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.x.licenses
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class OssOpenSourcesLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||
override val hasOpenSourceLicenses: Boolean = true
|
||||
|
||||
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||
val title = activity.getString(CommonStrings.common_open_source_licenses)
|
||||
OssLicensesMenuActivity.setActivityTitle(title)
|
||||
activity.startActivity(Intent(activity, OssLicensesMenuActivity::class.java))
|
||||
}
|
||||
}
|
||||
32
app/src/gplay/res/values-night/colors.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Use a few colors from compoundColorsDark -->
|
||||
<!-- DarkColorTokens.colorThemeBg -->
|
||||
<color name="colorThemeBg">#FF101317</color>
|
||||
<!-- DarkColorTokens.colorGray1400 -->
|
||||
<color name="textPrimary">#FFEBEEF2</color>
|
||||
<!-- DarkColorTokens.colorGray900 -->
|
||||
<color name="textSecondary">#ff808994</color>
|
||||
<!-- DarkColorTokens.colorBlue900 -->
|
||||
<color name="textLinkExternal">#FF4187EB</color>
|
||||
|
||||
<bool name="windowLightStatusBar">false</bool>
|
||||
<bool name="windowLightNavigationBar">false</bool>
|
||||
|
||||
</resources>
|
||||
25
app/src/gplay/res/values-v27/themes.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Theme.OssLicenses.Light.v27" parent="Base.Theme.OssLicenses">
|
||||
<item name="android:windowLightNavigationBar">@bool/windowLightNavigationBar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OssLicenses" parent="Theme.OssLicenses.Light.v27"/>
|
||||
|
||||
</resources>
|
||||
32
app/src/gplay/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Use a few colors from compoundColorsLight -->
|
||||
<!-- LightColorTokens.colorThemeBg -->
|
||||
<color name="colorThemeBg">#FFFFFFFF</color>
|
||||
<!-- LightColorTokens.colorGray1400 -->
|
||||
<color name="textPrimary">#FF1B1D22</color>
|
||||
<!-- LightColorTokens.colorGray900 -->
|
||||
<color name="textSecondary">#FF656D77</color>
|
||||
<!-- LightColorTokens.colorBlue900 -->
|
||||
<color name="textLinkExternal">#FF0467DD</color>
|
||||
|
||||
<bool name="windowLightStatusBar">true</bool>
|
||||
<bool name="windowLightNavigationBar">true</bool>
|
||||
|
||||
</resources>
|
||||
23
app/src/gplay/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="NoElevationToolbar" parent="Widget.MaterialComponents.Toolbar">
|
||||
<item name="android:elevation">0dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
41
app/src/gplay/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Base.Theme.OssLicenses" parent="Theme.MaterialComponents.DayNight">
|
||||
<!-- Background of title bar -->
|
||||
<item name="colorPrimary">@color/colorThemeBg</item>
|
||||
<!-- Background of the screen -->
|
||||
<item name="android:colorBackground">@color/colorThemeBg</item>
|
||||
<!-- Text of the licenses -->
|
||||
<item name="android:textColor">@color/textSecondary</item>
|
||||
<!-- Link text color -->
|
||||
<item name="android:textColorLink">@color/textLinkExternal</item>
|
||||
<!-- Title, back button and license item text color -->
|
||||
<item name="android:textColorPrimary">@color/textPrimary</item>
|
||||
<!-- Background of status bar -->
|
||||
<item name="android:statusBarColor">@color/colorThemeBg</item>
|
||||
<item name="android:windowLightStatusBar">@bool/windowLightStatusBar</item>
|
||||
<!-- Background of navigation bar -->
|
||||
<item name="android:navigationBarColor">@color/colorThemeBg</item>
|
||||
<!-- Try to remove Toolbar elevation, but it does not work :/ -->
|
||||
<item name="toolbarStyle">@style/NoElevationToolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.OssLicenses" parent="Base.Theme.OssLicenses" />
|
||||
|
||||
</resources>
|
||||
|
|
@ -26,9 +26,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
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
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
|
|
@ -38,16 +35,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||
import com.bumble.appyx.core.integration.NodeHost
|
||||
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.theme.isDark
|
||||
import io.element.android.compound.theme.mapToTheme
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.lockscreen.api.handleSecureFlag
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||
import io.element.android.x.di.AppBindings
|
||||
import io.element.android.x.intent.SafeUriHandler
|
||||
|
|
@ -74,14 +68,8 @@ class MainActivity : NodeActivity() {
|
|||
|
||||
@Composable
|
||||
private fun MainContent(appBindings: AppBindings) {
|
||||
val theme by remember {
|
||||
appBindings.preferencesStore().getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
val migrationState = appBindings.migrationEntryPoint().present()
|
||||
ElementTheme(
|
||||
darkTheme = theme.isDark()
|
||||
) {
|
||||
ElementThemeApp(appBindings.preferencesStore()) {
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||
LocalUriHandler provides SafeUriHandler(this),
|
||||
|
|
|
|||
|
|
@ -71,8 +71,7 @@ class TracingInitializer : Initializer<Unit> {
|
|||
return WriteToFilesConfiguration.Enabled(
|
||||
directory = bugReporter.logDirectory().absolutePath,
|
||||
filenamePrefix = "logs",
|
||||
filenameSuffix = null,
|
||||
// Keep a minimum of 1 week of log files.
|
||||
// Keep a maximum of 1 week of log files.
|
||||
numberOfFiles = 7 * 24,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@
|
|||
<locale android:name="in"/>
|
||||
<locale android:name="it"/>
|
||||
<locale android:name="ka"/>
|
||||
<locale android:name="pl"/>
|
||||
<locale android:name="pt"/>
|
||||
<locale android:name="pt_BR"/>
|
||||
<locale android:name="ro"/>
|
||||
<locale android:name="ru"/>
|
||||
<locale android:name="sk"/>
|
||||
|
|
|
|||
|
|
@ -15,20 +15,13 @@
|
|||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.appconfig"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.annotationjvm)
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,44 +16,28 @@
|
|||
|
||||
package io.element.android.appconfig
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Configuration for the lock screen feature.
|
||||
* @property isPinMandatory Whether the PIN is mandatory or not.
|
||||
* @property pinBlacklist Some PINs are forbidden.
|
||||
* @property pinSize The size of the PIN.
|
||||
* @property maxPinCodeAttemptsBeforeLogout Number of attempts before the user is logged out.
|
||||
* @property gracePeriod Time period before locking the app once backgrounded.
|
||||
* @property isStrongBiometricsEnabled Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported.
|
||||
* @property isWeakBiometricsEnabled Authentication with weak methods (most face/iris unlock implementations) is supported.
|
||||
*/
|
||||
data class LockScreenConfig(
|
||||
val isPinMandatory: Boolean,
|
||||
val pinBlacklist: Set<String>,
|
||||
val pinSize: Int,
|
||||
val maxPinCodeAttemptsBeforeLogout: Int,
|
||||
val gracePeriod: Duration,
|
||||
val isStrongBiometricsEnabled: Boolean,
|
||||
val isWeakBiometricsEnabled: Boolean,
|
||||
)
|
||||
object LockScreenConfig {
|
||||
/** Whether the PIN is mandatory or not. */
|
||||
const val IS_PIN_MANDATORY: Boolean = false
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
@Module
|
||||
object LockScreenConfigModule {
|
||||
@Provides
|
||||
fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig(
|
||||
isPinMandatory = false,
|
||||
pinBlacklist = setOf("0000", "1234"),
|
||||
pinSize = 4,
|
||||
maxPinCodeAttemptsBeforeLogout = 3,
|
||||
gracePeriod = 0.seconds,
|
||||
isStrongBiometricsEnabled = true,
|
||||
isWeakBiometricsEnabled = true,
|
||||
)
|
||||
/** Set of forbidden PIN codes. */
|
||||
val FORBIDDEN_PIN_CODES: Set<String> = setOf("0000", "1234")
|
||||
|
||||
/** The size of the PIN. */
|
||||
const val PIN_SIZE: Int = 4
|
||||
|
||||
/** Number of attempts before the user is logged out. */
|
||||
const val MAX_PIN_CODE_ATTEMPTS_BEFORE_LOGOUT: Int = 3
|
||||
|
||||
/** Time period before locking the app once backgrounded. */
|
||||
val GRACE_PERIOD: Duration = 2.minutes
|
||||
|
||||
/** Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported. */
|
||||
const val IS_STRONG_BIOMETRICS_ENABLED: Boolean = true
|
||||
|
||||
/** Authentication with weak methods (most face/iris unlock implementations) is supported. */
|
||||
const val IS_WEAK_BIOMETRICS_ENABLED: Boolean = true
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.deeplink)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
|
|
|||
|
|
@ -104,11 +104,6 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
|||
inputs.room.subscribeToSync()
|
||||
}
|
||||
},
|
||||
onPause = {
|
||||
appCoroutineScope.launch {
|
||||
inputs.room.unsubscribeFromSync()
|
||||
}
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.login.api.LoginUserStory
|
|||
import io.element.android.features.preferences.api.CacheService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
@ -42,6 +43,7 @@ class RootNavStateFlowFactory @Inject constructor(
|
|||
private val matrixClientsHolder: MatrixClientsHolder,
|
||||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
private val loginUserStory: LoginUserStory,
|
||||
private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory,
|
||||
) {
|
||||
private var currentCacheIndex = 0
|
||||
|
||||
|
|
@ -73,6 +75,8 @@ class RootNavStateFlowFactory @Inject constructor(
|
|||
matrixClientsHolder.remove(sessionId)
|
||||
// Ensure image loader will be recreated with the new MatrixClient
|
||||
imageLoaderHolder.remove(sessionId)
|
||||
// Also remove cached value for SessionPreferencesStore
|
||||
sessionPreferencesStoreFactory.remove(sessionId)
|
||||
}
|
||||
.toIndexFlow(initialCacheIndex)
|
||||
.onEach { cacheIndex ->
|
||||
|
|
|
|||
|
|
@ -123,7 +123,9 @@ class JoinRoomLoadedFlowNodeTest {
|
|||
@Test
|
||||
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeMatrixRoom()
|
||||
val room = FakeMatrixRoom(
|
||||
updateMembersResult = { }
|
||||
)
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
|
|
@ -144,7 +146,9 @@ class JoinRoomLoadedFlowNodeTest {
|
|||
@Test
|
||||
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeMatrixRoom()
|
||||
val room = FakeMatrixRoom(
|
||||
updateMembersResult = { }
|
||||
)
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import org.junit.Test
|
|||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) class SendQueuesTest {
|
||||
private val matrixClient = FakeMatrixClient()
|
||||
private val room = FakeMatrixRoom()
|
||||
private val networkMonitor = FakeNetworkMonitor()
|
||||
private val sut = SendQueues(matrixClient, networkMonitor)
|
||||
|
||||
|
|
@ -43,11 +42,11 @@ import org.junit.Test
|
|||
val setAllSendQueuesEnabledLambda = lambdaRecorder { _: Boolean -> }
|
||||
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
|
||||
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
|
||||
matrixClient.givenGetRoomResult(room.roomId, room)
|
||||
|
||||
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
|
||||
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
||||
|
||||
val room = FakeMatrixRoom(
|
||||
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
||||
)
|
||||
matrixClient.givenGetRoomResult(room.roomId, room)
|
||||
sut.launchIn(backgroundScope)
|
||||
|
||||
sendQueueDisabledFlow.emit(room.roomId)
|
||||
|
|
@ -72,10 +71,11 @@ import org.junit.Test
|
|||
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
|
||||
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
|
||||
networkMonitor.connectivity.value = NetworkStatus.Offline
|
||||
matrixClient.givenGetRoomResult(room.roomId, room)
|
||||
|
||||
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
|
||||
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
||||
val room = FakeMatrixRoom(
|
||||
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
||||
)
|
||||
matrixClient.givenGetRoomResult(room.roomId, room)
|
||||
|
||||
sut.launchIn(backgroundScope)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ buildscript {
|
|||
dependencies {
|
||||
classpath(libs.kotlin.gradle.plugin)
|
||||
classpath(libs.gms.google.services)
|
||||
classpath(libs.oss.licenses.plugin)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
1
changelog.d/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
!.gitignore
|
||||
|
|
@ -1 +0,0 @@
|
|||
Store and restore drafts for each room.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Use a more natural date format for day dividers in the timeline. Also improve the time format for last messages in the room list.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Resolve display names in mentions in real time, also send mentions with user ids as the fallback text for the link representation of the mentions.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Alert for incoming call even if notifications are disabled
|
||||
|
|
@ -1 +0,0 @@
|
|||
Updated Rust SDK to `v0.2.28`. Fixed incompatibilities.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix feature flags not being able to be toggle in developer settings in release builds.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Let roles and permissions screens work for invited room members too.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix image rendering after clear cache
|
||||
|
|
@ -1 +0,0 @@
|
|||
Improve room filters behavior
|
||||
|
|
@ -1 +0,0 @@
|
|||
Make sure we replace the 'answer call' pending intent on ringing call notifications.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Make sure we don't use the main dispatcher while closing the bug report request, as it can lead to crashes in strict mode.
|
||||
2
fastlane/metadata/android/en-US/changelogs/40005000.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: mostly bug fixes and performance improvements.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
||||
<string name="screen_analytics_settings_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
|
||||
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_analytics_settings_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
|
||||
<string name="screen_analytics_settings_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
|
||||
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Compartilhar dados de utilização"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Nie będziemy udostępniać Twoich danych podmiotom trzecim"</string>
|
||||
<string name="screen_analytics_prompt_title">"Pomóż nam ulepszyć %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_analytics_prompt_data_usage">"Não registraremos nem criaremos perfil baseado em qualquer dado pessoal"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Você pode desativar isso a qualquer momento"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Não compartilharemos seus dados com terceiros"</string>
|
||||
<string name="screen_analytics_prompt_title">"Ajude a melhorar o %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -43,6 +43,7 @@ dependencies {
|
|||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.impl)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.network)
|
||||
|
|
@ -70,4 +71,6 @@ dependencies {
|
|||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@
|
|||
<application>
|
||||
<activity
|
||||
android:name=".ui.ElementCallActivity"
|
||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:exported="true"
|
||||
android:label="@string/element_call"
|
||||
android:launchMode="singleTask"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity="io.element.android.features.call">
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
|
|
@ -77,10 +78,11 @@
|
|||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.IncomingCallActivity"
|
||||
<activity
|
||||
android:name=".ui.IncomingCallActivity"
|
||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="io.element.android.features.call" />
|
||||
|
||||
|
|
@ -90,9 +92,10 @@
|
|||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall" />
|
||||
|
||||
<receiver android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||
android:exported="false"
|
||||
android:enabled="true" />
|
||||
<receiver
|
||||
android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.pip
|
||||
|
||||
sealed interface PictureInPictureEvents {
|
||||
data object EnterPictureInPicture : PictureInPictureEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.pip
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("PiP")
|
||||
|
||||
class PictureInPicturePresenter @Inject constructor(
|
||||
pipSupportProvider: PipSupportProvider,
|
||||
) : Presenter<PictureInPictureState> {
|
||||
private val isPipSupported = pipSupportProvider.isPipSupported()
|
||||
private var isInPictureInPicture = mutableStateOf(false)
|
||||
private var hostActivity: WeakReference<Activity>? = null
|
||||
|
||||
@Composable
|
||||
override fun present(): PictureInPictureState {
|
||||
fun handleEvent(event: PictureInPictureEvents) {
|
||||
when (event) {
|
||||
PictureInPictureEvents.EnterPictureInPicture -> switchToPip()
|
||||
}
|
||||
}
|
||||
|
||||
return PictureInPictureState(
|
||||
supportPip = isPipSupported,
|
||||
isInPictureInPicture = isInPictureInPicture.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
fun onCreate(activity: Activity) {
|
||||
if (isPipSupported) {
|
||||
Timber.tag(loggerTag.value).d("onCreate: Setting PiP params")
|
||||
hostActivity = WeakReference(activity)
|
||||
hostActivity?.get()?.setPictureInPictureParams(getPictureInPictureParams())
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("onCreate: PiP is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
Timber.tag(loggerTag.value).d("onDestroy")
|
||||
hostActivity?.clear()
|
||||
hostActivity = null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getPictureInPictureParams(): PictureInPictureParams {
|
||||
return PictureInPictureParams.Builder()
|
||||
// Portrait for calls seems more appropriate
|
||||
.setAspectRatio(Rational(3, 5))
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
setAutoEnterEnabled(true)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters Picture-in-Picture mode.
|
||||
*/
|
||||
private fun switchToPip() {
|
||||
if (isPipSupported) {
|
||||
Timber.tag(loggerTag.value).d("Switch to PiP mode")
|
||||
hostActivity?.get()?.enterPictureInPictureMode(getPictureInPictureParams())
|
||||
?.also { Timber.tag(loggerTag.value).d("Switch to PiP mode result: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||
isInPictureInPicture.value = isInPictureInPictureMode
|
||||
}
|
||||
|
||||
fun onUserLeaveHint() {
|
||||
Timber.tag(loggerTag.value).d("onUserLeaveHint")
|
||||
switchToPip()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.pip
|
||||
|
||||
data class PictureInPictureState(
|
||||
val supportPip: Boolean,
|
||||
val isInPictureInPicture: Boolean,
|
||||
val eventSink: (PictureInPictureEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.pip
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
open class PictureInPictureStateProvider : PreviewParameterProvider<PictureInPictureState> {
|
||||
override val values: Sequence<PictureInPictureState>
|
||||
get() = sequenceOf(
|
||||
aPictureInPictureState(supportPip = true),
|
||||
aPictureInPictureState(supportPip = true, isInPictureInPicture = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aPictureInPictureState(
|
||||
supportPip: Boolean = false,
|
||||
isInPictureInPicture: Boolean = false,
|
||||
eventSink: (PictureInPictureEvents) -> Unit = {},
|
||||
): PictureInPictureState {
|
||||
return PictureInPictureState(
|
||||
supportPip = supportPip,
|
||||
isInPictureInPicture = isInPictureInPicture,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.pip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
interface PipSupportProvider {
|
||||
@ChecksSdkIntAtLeast(Build.VERSION_CODES.O)
|
||||
fun isPipSupported(): Boolean
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPipSupportProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : PipSupportProvider {
|
||||
override fun isPipSupported(): Boolean {
|
||||
val isSupportedByTheOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
context.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).orFalse()
|
||||
return if (isSupportedByTheOs) {
|
||||
runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PictureInPicture) }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,11 +23,12 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
|
|||
override val values: Sequence<CallScreenState>
|
||||
get() = sequenceOf(
|
||||
aCallScreenState(),
|
||||
aCallScreenState(urlState = AsyncData.Loading()),
|
||||
aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aCallScreenState(
|
||||
internal fun aCallScreenState(
|
||||
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||
userAgent: String = "",
|
||||
isInWidgetMode: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.call.impl.R
|
||||
import io.element.android.features.call.impl.pip.PictureInPictureEvents
|
||||
import io.element.android.features.call.impl.pip.PictureInPictureState
|
||||
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
|
||||
import io.element.android.features.call.impl.pip.aPictureInPictureState
|
||||
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
|
|
@ -58,25 +62,36 @@ interface CallScreenNavigator {
|
|||
@Composable
|
||||
internal fun CallScreenView(
|
||||
state: CallScreenState,
|
||||
pipState: PictureInPictureState,
|
||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun handleBack() {
|
||||
if (pipState.supportPip) {
|
||||
pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture)
|
||||
} else {
|
||||
state.eventSink(CallScreenEvents.Hangup)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.element_call)) },
|
||||
navigationIcon = {
|
||||
BackButton(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
onClick = { state.eventSink(CallScreenEvents.Hangup) }
|
||||
)
|
||||
}
|
||||
)
|
||||
if (!pipState.isInPictureInPicture) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.element_call)) },
|
||||
navigationIcon = {
|
||||
BackButton(
|
||||
imageVector = if (pipState.supportPip) CompoundIcons.ArrowLeft() else CompoundIcons.Close(),
|
||||
onClick = ::handleBack,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
BackHandler {
|
||||
state.eventSink(CallScreenEvents.Hangup)
|
||||
handleBack()
|
||||
}
|
||||
CallWebView(
|
||||
modifier = Modifier
|
||||
|
|
@ -177,6 +192,19 @@ internal fun CallScreenViewPreview(
|
|||
) = ElementPreview {
|
||||
CallScreenView(
|
||||
state = state,
|
||||
pipState = aPictureInPictureState(),
|
||||
requestPermissions = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CallScreenPipViewPreview(
|
||||
@PreviewParameter(PictureInPictureStateProvider::class) state: PictureInPictureState,
|
||||
) = ElementPreview {
|
||||
CallScreenView(
|
||||
state = aCallScreenState(),
|
||||
pipState = state,
|
||||
requestPermissions = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,28 +30,26 @@ import androidx.activity.compose.setContent
|
|||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.content.IntentCompat
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.theme.isDark
|
||||
import io.element.android.compound.theme.mapToTheme
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||
import io.element.android.features.call.impl.di.CallBindings
|
||||
import io.element.android.features.call.impl.pip.PictureInPicturePresenter
|
||||
import io.element.android.features.call.impl.services.CallForegroundService
|
||||
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
|
||||
|
||||
private lateinit var presenter: CallScreenPresenter
|
||||
|
||||
|
|
@ -67,6 +65,8 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
private var isDarkMode = false
|
||||
private val webViewTarget = mutableStateOf<CallType?>(null)
|
||||
|
||||
private var eventSink: ((CallScreenEvents) -> Unit)? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -86,20 +86,19 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
updateUiMode(resources.configuration)
|
||||
}
|
||||
|
||||
pictureInPicturePresenter.onCreate(this)
|
||||
|
||||
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
requestAudioFocus()
|
||||
|
||||
setContent {
|
||||
val theme by remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
val state = presenter.present()
|
||||
ElementTheme(
|
||||
darkTheme = theme.isDark()
|
||||
) {
|
||||
val pipState = pictureInPicturePresenter.present()
|
||||
ElementThemeApp(appPreferencesStore) {
|
||||
val state = presenter.present()
|
||||
eventSink = state.eventSink
|
||||
CallScreenView(
|
||||
state = state,
|
||||
pipState = pipState,
|
||||
requestPermissions = { permissions, callback ->
|
||||
requestPermissionCallback = callback
|
||||
requestPermissionsLauncher.launch(permissions)
|
||||
|
|
@ -114,6 +113,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
updateUiMode(newConfig)
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
pictureInPicturePresenter.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
|
||||
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
Timber.d("Exiting PiP mode: Hangup the call")
|
||||
eventSink?.invoke(CallScreenEvents.Hangup)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setCallType(intent)
|
||||
|
|
@ -131,10 +140,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
pictureInPicturePresenter.onUserLeaveHint()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
releaseAudioFocus()
|
||||
CallForegroundService.stop(this)
|
||||
pictureInPicturePresenter.onDestroy()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import io.element.android.features.call.impl.notifications.CallNotificationData
|
|||
import io.element.android.features.call.impl.utils.ActiveCallManager
|
||||
import io.element.android.features.call.impl.utils.CallState
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -51,6 +53,9 @@ class IncomingCallActivity : AppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var activeCallManager: ActiveCallManager
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferencesStore: AppPreferencesStore
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -68,11 +73,13 @@ class IncomingCallActivity : AppCompatActivity() {
|
|||
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
|
||||
if (notificationData != null) {
|
||||
setContent {
|
||||
IncomingCallScreen(
|
||||
notificationData = notificationData,
|
||||
onAnswer = ::onAnswer,
|
||||
onCancel = ::onCancel,
|
||||
)
|
||||
ElementThemeApp(appPreferencesStore) {
|
||||
IncomingCallScreen(
|
||||
notificationData = notificationData,
|
||||
onAnswer = ::onAnswer,
|
||||
onCancel = ::onCancel,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No data, finish the activity
|
||||
|
|
|
|||
|
|
@ -64,67 +64,65 @@ internal fun IncomingCallScreen(
|
|||
onAnswer: (CallNotificationData) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
ElementTheme {
|
||||
OnboardingBackground()
|
||||
OnboardingBackground()
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 20.dp, end = 20.dp, top = 124.dp)
|
||||
.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 20.dp, end = 20.dp, top = 124.dp)
|
||||
.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(
|
||||
id = notificationData.senderId.value,
|
||||
name = notificationData.senderName,
|
||||
url = notificationData.avatarUrl,
|
||||
size = AvatarSize.IncomingCall,
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = notificationData.senderName ?: notificationData.senderId.value,
|
||||
style = ElementTheme.typography.fontHeadingMdBold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_incoming_call_subtitle_android),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ActionButton(
|
||||
size = 64.dp,
|
||||
onClick = { onAnswer(notificationData) },
|
||||
icon = CompoundIcons.VoiceCall(),
|
||||
title = stringResource(CommonStrings.action_accept),
|
||||
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
|
||||
borderColor = ElementTheme.colors.borderSuccessSubtle
|
||||
Avatar(
|
||||
avatarData = AvatarData(
|
||||
id = notificationData.senderId.value,
|
||||
name = notificationData.senderName,
|
||||
url = notificationData.avatarUrl,
|
||||
size = AvatarSize.IncomingCall,
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = notificationData.senderName ?: notificationData.senderId.value,
|
||||
style = ElementTheme.typography.fontHeadingMdBold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.screen_incoming_call_subtitle_android),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ActionButton(
|
||||
size = 64.dp,
|
||||
onClick = { onAnswer(notificationData) },
|
||||
icon = CompoundIcons.VoiceCall(),
|
||||
title = stringResource(CommonStrings.action_accept),
|
||||
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
|
||||
borderColor = ElementTheme.colors.borderSuccessSubtle
|
||||
)
|
||||
|
||||
ActionButton(
|
||||
size = 64.dp,
|
||||
onClick = onCancel,
|
||||
icon = CompoundIcons.EndCall(),
|
||||
title = stringResource(CommonStrings.action_reject),
|
||||
backgroundColor = ElementTheme.colors.iconCriticalPrimary,
|
||||
borderColor = ElementTheme.colors.borderCriticalSubtle
|
||||
)
|
||||
}
|
||||
ActionButton(
|
||||
size = 64.dp,
|
||||
onClick = onCancel,
|
||||
icon = CompoundIcons.EndCall(),
|
||||
title = stringResource(CommonStrings.action_reject),
|
||||
backgroundColor = ElementTheme.colors.iconCriticalPrimary,
|
||||
borderColor = ElementTheme.colors.borderCriticalSubtle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -145,7 +143,8 @@ private fun ActionButton(
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
FilledIconButton(
|
||||
modifier = Modifier.size(size + borderSize)
|
||||
modifier = Modifier
|
||||
.size(size + borderSize)
|
||||
.border(borderSize, borderColor, CircleShape),
|
||||
onClick = onClick,
|
||||
colors = IconButtonDefaults.filledIconButtonColors(
|
||||
|
|
@ -171,22 +170,20 @@ private fun ActionButton(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun IncomingCallScreenPreview() {
|
||||
ElementPreview {
|
||||
IncomingCallScreen(
|
||||
notificationData = CallNotificationData(
|
||||
sessionId = SessionId("@alice:matrix.org"),
|
||||
roomId = RoomId("!1234:matrix.org"),
|
||||
eventId = EventId("\$asdadadsad:matrix.org"),
|
||||
senderId = UserId("@bob:matrix.org"),
|
||||
roomName = "A room",
|
||||
senderName = "Bob",
|
||||
avatarUrl = null,
|
||||
notificationChannelId = "incoming_call",
|
||||
timestamp = 0L,
|
||||
),
|
||||
onAnswer = {},
|
||||
onCancel = {},
|
||||
)
|
||||
}
|
||||
internal fun IncomingCallScreenPreview() = ElementPreview {
|
||||
IncomingCallScreen(
|
||||
notificationData = CallNotificationData(
|
||||
sessionId = SessionId("@alice:matrix.org"),
|
||||
roomId = RoomId("!1234:matrix.org"),
|
||||
eventId = EventId("\$asdadadsad:matrix.org"),
|
||||
senderId = UserId("@bob:matrix.org"),
|
||||
roomName = "A room",
|
||||
senderName = "Bob",
|
||||
avatarUrl = null,
|
||||
notificationChannelId = "incoming_call",
|
||||
timestamp = 0L,
|
||||
),
|
||||
onAnswer = {},
|
||||
onCancel = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,25 @@ import io.element.android.features.call.impl.notifications.CallNotificationData
|
|||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -79,11 +90,16 @@ class DefaultActiveCallManager @Inject constructor(
|
|||
private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler,
|
||||
private val ringingCallNotificationCreator: RingingCallNotificationCreator,
|
||||
private val notificationManagerCompat: NotificationManagerCompat,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : ActiveCallManager {
|
||||
private var timedOutCallJob: Job? = null
|
||||
|
||||
override val activeCall = MutableStateFlow<ActiveCall?>(null)
|
||||
|
||||
init {
|
||||
observeRingingCall()
|
||||
}
|
||||
|
||||
override fun registerIncomingCall(notificationData: CallNotificationData) {
|
||||
if (activeCall.value != null) {
|
||||
displayMissedCallNotification(notificationData)
|
||||
|
|
@ -173,6 +189,35 @@ class DefaultActiveCallManager @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun observeRingingCall() {
|
||||
// This will observe ringing calls and ensure they're terminated if the room call is cancelled
|
||||
activeCall
|
||||
.filterNotNull()
|
||||
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
|
||||
.flatMapLatest { activeCall ->
|
||||
val callType = activeCall.callType as CallType.RoomCall
|
||||
// Get a flow of updated `hasRoomCall` values for the room
|
||||
matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()
|
||||
?.getRoom(callType.roomId)
|
||||
?.roomInfoFlow
|
||||
?.map { it.hasRoomCall }
|
||||
?: flowOf()
|
||||
}
|
||||
// We only want to check if the room active call status changes
|
||||
.distinctUntilChanged()
|
||||
// Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway)
|
||||
.drop(1)
|
||||
.onEach { roomHasActiveCall ->
|
||||
if (!roomHasActiveCall) {
|
||||
// The call was cancelled
|
||||
timedOutCallJob?.cancel()
|
||||
incomingCallTimedOut()
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
<string name="call_foreground_service_channel_title_android">"Laufender Anruf"</string>
|
||||
<string name="call_foreground_service_message_android">"Tippen, um zum Anruf zurückzukehren"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Anruf läuft"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Eingehender Element Call"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="call_foreground_service_channel_title_android">"Połączenie w trakcie"</string>
|
||||
<string name="call_foreground_service_message_android">"Stuknij, aby wrócić do rozmowy"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Rozmowa w toku"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="call_foreground_service_channel_title_android">"Chamada em andamento"</string>
|
||||
<string name="call_foreground_service_message_android">"Toque para retornar à chamada"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ Chamada em andamento"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.pip
|
||||
|
||||
class FakePipSupportProvider(
|
||||
private val isPipSupported: Boolean
|
||||
) : PipSupportProvider {
|
||||
override fun isPipSupported() = isPipSupported
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.pip
|
||||
|
||||
import android.os.Build.VERSION_CODES
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.impl.ui.ElementCallActivity
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class PictureInPicturePresenterTest {
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
|
||||
fun `when pip is not supported, the state value supportPip is false`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = false)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.supportPip).isFalse()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
|
||||
fun `when pip is supported, the state value supportPip is true`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.supportPip).isTrue()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.S])
|
||||
fun `when entering pip is supported, the state value isInPictureInPicture is true`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
|
||||
presenter.onPictureInPictureModeChanged(true)
|
||||
val pipState = awaitItem()
|
||||
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||
// User stops pip
|
||||
presenter.onPictureInPictureModeChanged(false)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.isInPictureInPicture).isFalse()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [VERSION_CODES.S])
|
||||
fun `when onUserLeaveHint is called, the state value isInPictureInPicture becomes true`() = runTest {
|
||||
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||
presenter.onUserLeaveHint()
|
||||
presenter.onPictureInPictureModeChanged(true)
|
||||
val pipState = awaitItem()
|
||||
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||
}
|
||||
presenter.onDestroy()
|
||||
}
|
||||
|
||||
private fun createPictureInPicturePresenter(
|
||||
supportPip: Boolean = true,
|
||||
): PictureInPicturePresenter {
|
||||
val activity = Robolectric.buildActivity(ElementCallActivity::class.java)
|
||||
return PictureInPicturePresenter(
|
||||
pipSupportProvider = FakePipSupportProvider(supportPip),
|
||||
).apply {
|
||||
onCreate(activity.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.impl.ui
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.call.impl.pip.PictureInPictureEvents
|
||||
import io.element.android.features.call.impl.pip.PictureInPictureState
|
||||
import io.element.android.features.call.impl.pip.aPictureInPictureState
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CallScreenViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back when pip is not supported hangs up`() {
|
||||
val eventsRecorder = EventsRecorder<CallScreenEvents>()
|
||||
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>(expectEvents = false)
|
||||
rule.setCallScreenView(
|
||||
aCallScreenState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
aPictureInPictureState(
|
||||
supportPip = false,
|
||||
eventSink = pipEventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.pressBack()
|
||||
eventsRecorder.assertSize(2)
|
||||
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
|
||||
eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on back when pip is supported enables PiP`() {
|
||||
val eventsRecorder = EventsRecorder<CallScreenEvents>()
|
||||
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
|
||||
rule.setCallScreenView(
|
||||
aCallScreenState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
aPictureInPictureState(
|
||||
supportPip = true,
|
||||
eventSink = pipEventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.pressBack()
|
||||
eventsRecorder.assertSize(1)
|
||||
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
|
||||
pipEventsRecorder.assertSingle(PictureInPictureEvents.EnterPictureInPicture)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCallScreenView(
|
||||
state: CallScreenState,
|
||||
pipState: PictureInPictureState,
|
||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit = { _, _ -> },
|
||||
) {
|
||||
setContent {
|
||||
CallScreenView(
|
||||
state = state,
|
||||
pipState = pipState,
|
||||
requestPermissions = requestPermissions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,10 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
|||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
||||
|
|
@ -42,7 +45,11 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
|||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
|
|
@ -59,26 +66,28 @@ class DefaultActiveCallManagerTest {
|
|||
@Test
|
||||
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = callNotificationData.sessionId,
|
||||
roomId = callNotificationData.roomId,
|
||||
),
|
||||
callState = CallState.Ringing(callNotificationData)
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = callNotificationData.sessionId,
|
||||
roomId = callNotificationData.roomId,
|
||||
),
|
||||
callState = CallState.Ringing(callNotificationData)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
runCurrent()
|
||||
runCurrent()
|
||||
|
||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -86,38 +95,42 @@ class DefaultActiveCallManagerTest {
|
|||
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||
)
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||
)
|
||||
|
||||
// Register existing call
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
val activeCall = manager.activeCall.value
|
||||
// Register existing call
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
val activeCall = manager.activeCall.value
|
||||
|
||||
// Now add a new call
|
||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
||||
// Now add a new call
|
||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
||||
|
||||
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
||||
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
||||
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
||||
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
||||
|
||||
advanceTimeBy(1)
|
||||
advanceTimeBy(1)
|
||||
|
||||
addMissedCallNotificationLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
||||
addMissedCallNotificationLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||
)
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||
)
|
||||
|
||||
manager.incomingCallTimedOut()
|
||||
manager.incomingCallTimedOut()
|
||||
|
||||
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
||||
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -125,82 +138,167 @@ class DefaultActiveCallManagerTest {
|
|||
fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest {
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
||||
notificationManagerCompat = notificationManagerCompat,
|
||||
)
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
||||
notificationManagerCompat = notificationManagerCompat,
|
||||
)
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
|
||||
manager.incomingCallTimedOut()
|
||||
advanceTimeBy(1)
|
||||
manager.incomingCallTimedOut()
|
||||
advanceTimeBy(1)
|
||||
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
|
||||
val notificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(notificationData)
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
val notificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(notificationData)
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
|
||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
|
||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
|
||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val manager = createActiveCallManager(
|
||||
notificationManagerCompat = notificationManagerCompat,
|
||||
)
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
callState = CallState.InCall,
|
||||
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
callState = CallState.InCall,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
runCurrent()
|
||||
runCurrent()
|
||||
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createActiveCallManager(
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `observeRingingCalls - will cancel the active ringing call if the call is cancelled`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomInfo(aRoomInfo())
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
|
||||
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
|
||||
// Call is active (the other user join the call)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
// Call is cancelled (the other user left the call)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomInfo(aRoomInfo())
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
|
||||
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||
|
||||
// No matrix client
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
|
||||
// The call should still be active
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
|
||||
// No room
|
||||
client.givenGetRoomResult(A_ROOM_ID, null)
|
||||
matrixClientProvider.getClient = { Result.success(client) }
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
|
||||
// The call should still be active
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.inCancellableScope(block: suspend CoroutineScope.() -> Unit) {
|
||||
launch(SupervisorJob()) {
|
||||
block()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.createActiveCallManager(
|
||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
||||
coroutineScope: CoroutineScope = this,
|
||||
) = DefaultActiveCallManager(
|
||||
coroutineScope = this,
|
||||
coroutineScope = coroutineScope,
|
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
|
|
@ -209,5 +307,6 @@ class DefaultActiveCallManagerTest {
|
|||
notificationBitmapLoader = FakeNotificationBitmapLoader(),
|
||||
),
|
||||
notificationManagerCompat = notificationManagerCompat,
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ class DefaultCallWidgetProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getWidget - fails if it can't generate the URL for the widget`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenGenerateWidgetWebViewUrlResult(Result.failure(Exception("Can't generate URL for widget")))
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.failure(Exception("Can't generate URL for widget")) }
|
||||
)
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
|
|
@ -66,10 +66,10 @@ class DefaultCallWidgetProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getWidget - fails if it can't get the widget driver`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||
givenGetWidgetDriverResult(Result.failure(Exception("Can't get a widget driver")))
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||
getWidgetDriverResult = { Result.failure(Exception("Can't get a widget driver")) }
|
||||
)
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
|
|
@ -79,10 +79,10 @@ class DefaultCallWidgetProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||
)
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
|
|
@ -92,10 +92,10 @@ class DefaultCallWidgetProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||
)
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
|
|
@ -120,10 +120,10 @@ class DefaultCallWidgetProviderTest {
|
|||
val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient ->
|
||||
providesLambda(matrixClient)
|
||||
}
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
||||
}
|
||||
val room = FakeMatrixRoom(
|
||||
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||
)
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Nowy pokój"</string>
|
||||
<string name="screen_create_room_add_people_title">"Zaproś znajomych"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Wystąpił błąd podczas tworzenia pokoju"</string>
|
||||
<string name="screen_create_room_private_option_description">"Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."</string>
|
||||
<string name="screen_create_room_private_option_title">"Pokój prywatny (tylko zaproszenie)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."</string>
|
||||
<string name="screen_create_room_public_option_title">"Pokój publiczny (każdy)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
|
||||
<string name="screen_create_room_title">"Utwórz pokój"</string>
|
||||
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Wystąpił błąd podczas próby rozpoczęcia czatu"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Nova sala"</string>
|
||||
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
|
||||
<string name="screen_create_room_private_option_description">"As mensagens nesta sala serão criptografadas. A criptografia não pode ser desativada posteriormente."</string>
|
||||
<string name="screen_create_room_private_option_title">"Sala privativa (somente por convite)"</string>
|
||||
<string name="screen_create_room_public_option_description">"As mensagens não serão criptografadas e qualquer pessoa pode lê-las. Você pode ativar a criptografia posteriormente."</string>
|
||||
<string name="screen_create_room_public_option_title">"Sala pública (qualquer pessoa)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
||||
<string name="screen_create_room_title">"Criar uma sala"</string>
|
||||
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar um chat"</string>
|
||||
</resources>
|
||||
|
|
@ -45,7 +45,7 @@ import io.element.android.libraries.di.SessionScope
|
|||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -93,11 +93,9 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
})
|
||||
|
||||
analyticsService.didAskUserConsent()
|
||||
.drop(1) // We only care about consent passing from not asked to asked state
|
||||
.onEach { didAskUserConsent ->
|
||||
if (didAskUserConsent) {
|
||||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.ftue.api.state.FtueService
|
|||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
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.PermissionStateProvider
|
||||
|
|
@ -34,6 +35,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -47,7 +49,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultFtueService @Inject constructor(
|
||||
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
||||
coroutineScope: CoroutineScope,
|
||||
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val permissionStateProvider: PermissionStateProvider,
|
||||
private val lockScreenService: LockScreenService,
|
||||
|
|
@ -66,11 +68,12 @@ class DefaultFtueService @Inject constructor(
|
|||
init {
|
||||
sessionVerificationService.sessionVerifiedStatus
|
||||
.onEach { updateState() }
|
||||
.launchIn(coroutineScope)
|
||||
.launchIn(sessionCoroutineScope)
|
||||
|
||||
analyticsService.didAskUserConsent()
|
||||
.distinctUntilChanged()
|
||||
.onEach { updateState() }
|
||||
.launchIn(coroutineScope)
|
||||
.launchIn(sessionCoroutineScope)
|
||||
}
|
||||
|
||||
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||
|
|
@ -119,10 +122,7 @@ class DefaultFtueService @Inject constructor(
|
|||
emit(SessionVerifiedStatus.NotVerified)
|
||||
}
|
||||
.first()
|
||||
// For some obscure reason we need to call this *before* we check the `readyVerifiedSessionStatus`, otherwise there's a deadlock
|
||||
// It seems like a DataStore bug
|
||||
val skipVerification = canSkipVerification()
|
||||
return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !skipVerification
|
||||
return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !canSkipVerification()
|
||||
}
|
||||
|
||||
private suspend fun canSkipVerification(): Boolean {
|
||||
|
|
@ -130,7 +130,6 @@ class DefaultFtueService @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun needsAnalyticsOptIn(): Boolean {
|
||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||
return analyticsService.didAskUserConsent().first().not()
|
||||
}
|
||||
|
||||
|
|
|
|||
11
features/ftue/impl/src/main/res/values-pl/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_notification_optin_subtitle">"Możesz zmienić ustawienia później."</string>
|
||||
<string name="screen_notification_optin_title">"Zezwól na powiadomienia i nie przegap żadnej wiadomości"</string>
|
||||
<string name="screen_welcome_bullet_1">"Połączenia, ankiety, wyszukiwanie i inne zostaną dodane później w tym roku."</string>
|
||||
<string name="screen_welcome_bullet_2">"Historia wiadomości dla pokoi szyfrowanych nie jest jeszcze dostępna."</string>
|
||||
<string name="screen_welcome_bullet_3">"Chętnie poznamy Twoją opinię. Daj nam znać, co myślisz na stronie ustawień."</string>
|
||||
<string name="screen_welcome_button">"Naprzód!"</string>
|
||||
<string name="screen_welcome_subtitle">"Oto, co musisz wiedzieć:"</string>
|
||||
<string name="screen_welcome_title">"Witamy w %1$s!"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_notification_optin_subtitle">"Você pode alterar suas configurações mais tarde."</string>
|
||||
<string name="screen_notification_optin_title">"Permita notificações e nunca perca uma mensagem"</string>
|
||||
<string name="screen_welcome_bullet_1">"Chamadas, enquetes, pesquisa e muito mais serão adicionadas ainda este ano."</string>
|
||||
<string name="screen_welcome_bullet_2">"O histórico de mensagens para salas criptografadas ainda não está disponível."</string>
|
||||
<string name="screen_welcome_bullet_3">"Adoraríamos ouvir sua opinião. Deixe-nos saber o que você pensa através da página de configurações."</string>
|
||||
<string name="screen_welcome_button">"Vamos lá!"</string>
|
||||
<string name="screen_welcome_subtitle">"Aqui está o que você precisa saber:"</string>
|
||||
<string name="screen_welcome_title">"Bem-vindo ao %1$s!"</string>
|
||||
</resources>
|
||||
|
|
@ -251,7 +251,7 @@ class DefaultFtueServiceTest {
|
|||
// First version where notification permission is required
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||
) = DefaultFtueService(
|
||||
coroutineScope = coroutineScope,
|
||||
sessionCoroutineScope = coroutineScope,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||
analyticsService = analyticsService,
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
|
|||
anAcceptDeclineInviteState(),
|
||||
anAcceptDeclineInviteState(
|
||||
invite = Optional.of(
|
||||
InviteData(RoomId("!room:matrix.org"), isDirect = true, roomName = "Alice"),
|
||||
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
|
||||
),
|
||||
declineAction = AsyncAction.Confirming,
|
||||
),
|
||||
anAcceptDeclineInviteState(
|
||||
invite = Optional.of(
|
||||
InviteData(RoomId("!room:matrix.org"), isDirect = false, roomName = "Some room"),
|
||||
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room"),
|
||||
),
|
||||
declineAction = AsyncAction.Confirming,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
data class InviteData(
|
||||
val roomId: RoomId,
|
||||
val roomName: String,
|
||||
val isDirect: Boolean,
|
||||
val isDm: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import io.element.android.libraries.architecture.runUpdatingState
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Optional
|
||||
|
|
@ -44,7 +44,7 @@ import kotlin.jvm.optionals.getOrNull
|
|||
class AcceptDeclineInvitePresenter @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val joinRoom: JoinRoom,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val notificationCleaner: NotificationCleaner,
|
||||
) : Presenter<AcceptDeclineInviteState> {
|
||||
@Composable
|
||||
override fun present(): AcceptDeclineInviteState {
|
||||
|
|
@ -112,7 +112,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
|||
trigger = JoinedRoom.Trigger.Invite,
|
||||
)
|
||||
.onSuccess {
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
}
|
||||
.map { roomId }
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
|||
suspend {
|
||||
client.getRoom(roomId)?.use {
|
||||
it.leave().getOrThrow()
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
}
|
||||
roomId
|
||||
}.runCatchingUpdatingState(declinedAction)
|
||||
|
|
|
|||
|
|
@ -79,13 +79,13 @@ private fun DeclineConfirmationDialog(
|
|||
onDismissClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contentResource = if (invite.isDirect) {
|
||||
val contentResource = if (invite.isDm) {
|
||||
R.string.screen_invites_decline_direct_chat_message
|
||||
} else {
|
||||
R.string.screen_invites_decline_chat_message
|
||||
}
|
||||
|
||||
val titleResource = if (invite.isDirect) {
|
||||
val titleResource = if (invite.isDm) {
|
||||
R.string.screen_invites_decline_direct_chat_title
|
||||
} else {
|
||||
R.string.screen_invites_decline_chat_title
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie do dołączenia do %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>
|
||||
<string name="screen_invites_empty_list">"Brak zaproszeń"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) zaprosił Cię"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_chat_message">"Tem certeza de que deseja recusar o convite para ingressar em %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Recusar convite"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Tem certeza de que deseja recusar esse chat privado com %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Recusar chat"</string>
|
||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</string>
|
||||
</resources>
|
||||
|
|
@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -92,9 +92,9 @@ class AcceptDeclineInvitePresenterTest {
|
|||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
result = FakeMatrixRoom(
|
||||
leaveRoomLambda = declineInviteFailure
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
||||
|
|
@ -133,7 +133,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
val fakeNotificationCleaner = FakeNotificationCleaner(
|
||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
||||
)
|
||||
val declineInviteSuccess = lambdaRecorder { ->
|
||||
|
|
@ -142,14 +142,14 @@ class AcceptDeclineInvitePresenterTest {
|
|||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
result = FakeMatrixRoom(
|
||||
leaveRoomLambda = declineInviteSuccess
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
val presenter = createAcceptDeclineInvitePresenter(
|
||||
client = client,
|
||||
notificationDrawerManager = notificationDrawerManager,
|
||||
notificationCleaner = fakeNotificationCleaner,
|
||||
)
|
||||
presenter.test {
|
||||
val inviteData = anInviteData()
|
||||
|
|
@ -219,7 +219,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
val fakeNotificationCleaner = FakeNotificationCleaner(
|
||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
||||
)
|
||||
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
|
||||
|
|
@ -227,7 +227,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
}
|
||||
val presenter = createAcceptDeclineInvitePresenter(
|
||||
joinRoomLambda = joinRoomSuccess,
|
||||
notificationDrawerManager = notificationDrawerManager,
|
||||
notificationCleaner = fakeNotificationCleaner,
|
||||
)
|
||||
presenter.test {
|
||||
val inviteData = anInviteData()
|
||||
|
|
@ -260,12 +260,12 @@ class AcceptDeclineInvitePresenterTest {
|
|||
private fun anInviteData(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
isDirect: Boolean = false
|
||||
isDm: Boolean = false
|
||||
): InviteData {
|
||||
return InviteData(
|
||||
roomId = roomId,
|
||||
roomName = name,
|
||||
isDirect = isDirect
|
||||
isDm = isDm
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -274,12 +274,12 @@ class AcceptDeclineInvitePresenterTest {
|
|||
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
},
|
||||
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(),
|
||||
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
||||
): AcceptDeclineInvitePresenter {
|
||||
return AcceptDeclineInvitePresenter(
|
||||
client = client,
|
||||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
||||
notificationDrawerManager = notificationDrawerManager,
|
||||
notificationCleaner = notificationCleaner,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
|||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
|
|
@ -173,7 +174,7 @@ private fun RoomPreview.toContentState(): ContentState {
|
|||
topic = topic,
|
||||
alias = canonicalAlias,
|
||||
numberOfMembers = numberOfJoinedMembers,
|
||||
isDirect = false,
|
||||
isDm = false,
|
||||
roomType = roomType,
|
||||
roomAvatarUrl = avatarUrl,
|
||||
joinAuthorisationStatus = when {
|
||||
|
|
@ -194,7 +195,7 @@ internal fun RoomDescription.toContentState(): ContentState {
|
|||
topic = topic,
|
||||
alias = alias,
|
||||
numberOfMembers = numberOfMembers,
|
||||
isDirect = false,
|
||||
isDm = false,
|
||||
roomType = RoomType.Room,
|
||||
roomAvatarUrl = avatarUrl,
|
||||
joinAuthorisationStatus = when (joinRule) {
|
||||
|
|
@ -213,7 +214,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
|||
topic = topic,
|
||||
alias = canonicalAlias,
|
||||
numberOfMembers = activeMembersCount,
|
||||
isDirect = isDirect,
|
||||
isDm = isDm,
|
||||
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||
roomAvatarUrl = avatarUrl,
|
||||
joinAuthorisationStatus = when {
|
||||
|
|
@ -233,7 +234,7 @@ internal fun ContentState.toInviteData(): InviteData? {
|
|||
roomId = roomId,
|
||||
// Note: name should not be null at this point, but use Id just in case...
|
||||
roomName = name ?: roomId.value,
|
||||
isDirect = isDirect
|
||||
isDm = isDm
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ sealed interface ContentState {
|
|||
val topic: String?,
|
||||
val alias: RoomAlias?,
|
||||
val numberOfMembers: Long?,
|
||||
val isDirect: Boolean,
|
||||
val isDm: Boolean,
|
||||
val roomType: RoomType,
|
||||
val roomAvatarUrl: String?,
|
||||
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
|||
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.room.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||
|
||||
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||
|
|
@ -84,6 +85,12 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
|||
roomType = RoomType.Space,
|
||||
)
|
||||
),
|
||||
aJoinRoomState(
|
||||
contentState = aLoadedContentState(
|
||||
name = "A DM",
|
||||
isDm = true,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +113,7 @@ fun aLoadedContentState(
|
|||
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
|
||||
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
||||
numberOfMembers: Long? = null,
|
||||
isDirect: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
roomType: RoomType = RoomType.Room,
|
||||
roomAvatarUrl: String? = null,
|
||||
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
||||
|
|
@ -116,7 +123,7 @@ fun aLoadedContentState(
|
|||
alias = alias,
|
||||
topic = topic,
|
||||
numberOfMembers = numberOfMembers,
|
||||
isDirect = isDirect,
|
||||
isDm = isDm,
|
||||
roomType = roomType,
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
joinAuthorisationStatus = joinAuthorisationStatus
|
||||
|
|
|
|||
|
|
@ -155,13 +155,13 @@ private fun JoinRoomFooter(
|
|||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.Large,
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptInvite,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.Large,
|
||||
size = ButtonSize.LargeLowPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class JoinRoomPresenterTest {
|
|||
assertThat(contentState.topic).isEqualTo(roomInfo.topic)
|
||||
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
|
||||
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount)
|
||||
assertThat(contentState.isDirect).isEqualTo(roomInfo.isDirect)
|
||||
assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect)
|
||||
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
|
||||
}
|
||||
}
|
||||
|
|
@ -283,7 +283,7 @@ class JoinRoomPresenterTest {
|
|||
assertThat(contentState.topic).isEqualTo(roomDescription.topic)
|
||||
assertThat(contentState.alias).isEqualTo(roomDescription.alias)
|
||||
assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers)
|
||||
assertThat(contentState.isDirect).isFalse()
|
||||
assertThat(contentState.isDm).isFalse()
|
||||
assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl)
|
||||
}
|
||||
}
|
||||
|
|
@ -398,7 +398,7 @@ class JoinRoomPresenterTest {
|
|||
topic = "Room topic",
|
||||
alias = RoomAlias("#alias:matrix.org"),
|
||||
numberOfMembers = 2,
|
||||
isDirect = false,
|
||||
isDm = false,
|
||||
roomType = RoomType.Room,
|
||||
roomAvatarUrl = "avatarUrl",
|
||||
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="leave_conversation_alert_subtitle">"Czy na pewno chcesz opuścić tę konwersację? Konwersacja nie jest publiczna i nie będziesz mógł dołączyć ponownie bez zaproszenia."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Jesteś pewien, że chcesz opuścić ten pokój? Jesteś tu jedyną osobą. Jeśli wyjdziesz, nikt nie będzie mógł dołączyć, w tym Ty."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Czy na pewno chcesz opuścić ten pokój? Ten pokój nie jest publiczny i nie będziesz mógł do niego wrócić bez zaproszenia."</string>
|
||||
<string name="leave_room_alert_subtitle">"Jesteś pewien, że chcesz wyjść z tego pokoju?"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="leave_room_alert_empty_subtitle">"Tem certeza de que deseja sair desta sala? Você é a única pessoa aqui. Se você sair, ninguém poderá ingressar no futuro, inclusive você."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Tem certeza de que deseja sair desta sala? Esta sala não é pública e você não poderá entrar novamente sem um convite."</string>
|
||||
<string name="leave_room_alert_subtitle">"Tem certeza de que deseja sair da sala?"</string>
|
||||
</resources>
|
||||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.di.SessionScope
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class DefaultLeaveRoomPresenterTest {
|
|||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true, isOneToOne = true),
|
||||
result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -140,7 +140,9 @@ class DefaultLeaveRoomPresenterTest {
|
|||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom(),
|
||||
result = FakeMatrixRoom(
|
||||
leaveRoomLambda = { Result.success(Unit) }
|
||||
),
|
||||
)
|
||||
},
|
||||
roomMembershipObserver = roomMembershipObserver
|
||||
|
|
@ -162,9 +164,9 @@ class DefaultLeaveRoomPresenterTest {
|
|||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||
},
|
||||
result = FakeMatrixRoom(
|
||||
leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -186,7 +188,9 @@ class DefaultLeaveRoomPresenterTest {
|
|||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom(),
|
||||
result = FakeMatrixRoom(
|
||||
leaveRoomLambda = { Result.success(Unit) }
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -208,9 +212,9 @@ class DefaultLeaveRoomPresenterTest {
|
|||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||
},
|
||||
result = FakeMatrixRoom(
|
||||
leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,13 +29,15 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
|
|||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.SendLocationInvocation
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -46,16 +48,18 @@ class SendLocationPresenterTest {
|
|||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val fakePermissionsPresenter = FakePermissionsPresenter()
|
||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||
private val fakeLocationActions = FakeLocationActions()
|
||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
|
||||
|
||||
private fun createSendLocationPresenter(
|
||||
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
||||
): SendLocationPresenter = SendLocationPresenter(
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
||||
},
|
||||
room = fakeMatrixRoom,
|
||||
room = matrixRoom,
|
||||
analyticsService = fakeAnalyticsService,
|
||||
messageComposerContext = fakeMessageComposerContext,
|
||||
locationActions = fakeLocationActions,
|
||||
|
|
@ -64,6 +68,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions granted`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.AllGranted,
|
||||
|
|
@ -90,6 +95,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions partially granted`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.SomeGranted,
|
||||
|
|
@ -116,6 +122,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions denied`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -142,6 +149,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions denied once`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -168,6 +176,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `rationale dialog dismiss`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -199,6 +208,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `rationale dialog continue`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -227,6 +237,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `permission denied dialog dismiss`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -258,6 +269,13 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `share sender location`() = runTest {
|
||||
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val matrixRoom = FakeMatrixRoom(
|
||||
sendLocationResult = sendLocationResult,
|
||||
)
|
||||
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.AllGranted,
|
||||
|
|
@ -289,16 +307,14 @@ class SendLocationPresenterTest {
|
|||
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
|
||||
assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
||||
assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
||||
SendLocationInvocation(
|
||||
body = "Location was shared at geo:3.0,4.0;u=5.0",
|
||||
geoUri = "geo:3.0,4.0;u=5.0",
|
||||
description = null,
|
||||
zoomLevel = 15,
|
||||
assetType = AssetType.SENDER
|
||||
sendLocationResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value("Location was shared at geo:3.0,4.0;u=5.0"),
|
||||
value("geo:3.0,4.0;u=5.0"),
|
||||
value(null),
|
||||
value(15),
|
||||
value(AssetType.SENDER),
|
||||
)
|
||||
)
|
||||
|
||||
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
|
||||
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
|
||||
|
|
@ -314,6 +330,13 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `share pin location`() = runTest {
|
||||
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val matrixRoom = FakeMatrixRoom(
|
||||
sendLocationResult = sendLocationResult,
|
||||
)
|
||||
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -345,16 +368,14 @@ class SendLocationPresenterTest {
|
|||
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
|
||||
assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
||||
assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
||||
SendLocationInvocation(
|
||||
body = "Location was shared at geo:0.0,1.0",
|
||||
geoUri = "geo:0.0,1.0",
|
||||
description = null,
|
||||
zoomLevel = 15,
|
||||
assetType = AssetType.PIN
|
||||
sendLocationResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value("Location was shared at geo:0.0,1.0"),
|
||||
value("geo:0.0,1.0"),
|
||||
value(null),
|
||||
value(15),
|
||||
value(AssetType.PIN),
|
||||
)
|
||||
)
|
||||
|
||||
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
|
||||
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
|
||||
|
|
@ -370,6 +391,13 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `composer context passes through analytics`() = runTest {
|
||||
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val matrixRoom = FakeMatrixRoom(
|
||||
sendLocationResult = sendLocationResult,
|
||||
)
|
||||
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -418,6 +446,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `open settings activity`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
|
|
@ -452,6 +481,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `application name is in state`() = runTest {
|
||||
val sendLocationPresenter = createSendLocationPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
sendLocationPresenter.present()
|
||||
}.test {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.cryptography.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.services.appnavstate.api)
|
||||
|
|
|
|||