mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-22 22:24:21 +03:00
Merge branch 'develop' into michaelk/force_java_version
This commit is contained in:
commit
bb57b6f9c8
33 changed files with 706 additions and 267 deletions
.github/workflows
build.ymlgradle-wrapper-validation.ymlnightly.ymlquality.ymlsanity_test.ymlsync-from-external-sources.ymltests.yml
changelog.d
library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer
vector/src
debug
java/im/vector/app/features/debug
features
settings
res/layout
main
java/im/vector/app
core/di
features
home/room/detail/timeline/action
media
AttachmentInteractionListener.ktAttachmentOverlayView.ktBaseAttachmentProvider.ktVectorAttachmentViewerAction.ktVectorAttachmentViewerActivity.ktVectorAttachmentViewerViewEvents.ktVectorAttachmentViewerViewModel.kt
domain/usecase
onboarding
settings
res/layout
test/java/im/vector/app
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
|
@ -20,6 +20,10 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
target: [ Gplay, Fdroid ]
|
target: [ Gplay, Fdroid ]
|
||||||
|
# Allow all jobs on develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/develop' && format('integration-tests-develop-{0}-{1}', matrix.target, github.sha) || format('build-debug-{0}-{1}', matrix.target, github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
|
@ -43,6 +47,7 @@ jobs:
|
||||||
name: Build unsigned GPlay APKs
|
name: Build unsigned GPlay APKs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
|
# Only runs on main, no concurrency.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
|
|
|
@ -5,6 +5,7 @@ jobs:
|
||||||
validation:
|
validation:
|
||||||
name: "Validation"
|
name: "Validation"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
35
.github/workflows/nightly.yml
vendored
35
.github/workflows/nightly.yml
vendored
|
@ -20,6 +20,7 @@ jobs:
|
||||||
build-android-test-matrix-sdk:
|
build-android-test-matrix-sdk:
|
||||||
name: Matrix SDK - Build Android Tests
|
name: Matrix SDK - Build Android Tests
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
|
@ -41,6 +42,7 @@ jobs:
|
||||||
build-android-test-app:
|
build-android-test-app:
|
||||||
name: App - Build Android Tests
|
name: App - Build Android Tests
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
|
@ -58,7 +60,7 @@ jobs:
|
||||||
- name: Build Android Tests for vector
|
- name: Build Android Tests for vector
|
||||||
run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
||||||
|
|
||||||
# Run Android Tests
|
# Run Android Tests
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Matrix SDK - Running Integration Tests
|
name: Matrix SDK - Running Integration Tests
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
@ -66,6 +68,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
api-level: [ 28 ]
|
api-level: [ 28 ]
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
|
@ -91,7 +94,7 @@ jobs:
|
||||||
curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
|
curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
|
||||||
chmod 777 start.sh
|
chmod 777 start.sh
|
||||||
./start.sh --no-rate-limit
|
./start.sh --no-rate-limit
|
||||||
# package: org.matrix.android.sdk.session
|
# package: org.matrix.android.sdk.session
|
||||||
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
|
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
with:
|
with:
|
||||||
|
@ -121,7 +124,7 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
id: get-comment-body-account
|
id: get-comment-body-account
|
||||||
run: python3 ./tools/ci/render_test_output.py account ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
run: python3 ./tools/ci/render_test_output.py account ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
||||||
# package: org.matrix.android.sdk.internal
|
# package: org.matrix.android.sdk.internal
|
||||||
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.internal] API[${{ matrix.api-level }}]
|
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.internal] API[${{ matrix.api-level }}]
|
||||||
if: always()
|
if: always()
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
@ -137,7 +140,7 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
id: get-comment-body-internal
|
id: get-comment-body-internal
|
||||||
run: python3 ./tools/ci/render_test_output.py internal ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
run: python3 ./tools/ci/render_test_output.py internal ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
||||||
# package: org.matrix.android.sdk.ordering
|
# package: org.matrix.android.sdk.ordering
|
||||||
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.ordering] API[${{ matrix.api-level }}]
|
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.ordering] API[${{ matrix.api-level }}]
|
||||||
if: always()
|
if: always()
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
@ -153,7 +156,7 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
id: get-comment-body-ordering
|
id: get-comment-body-ordering
|
||||||
run: python3 ./tools/ci/render_test_output.py ordering ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
run: python3 ./tools/ci/render_test_output.py ordering ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
||||||
# package: class PermalinkParserTest
|
# package: class PermalinkParserTest
|
||||||
- name: Run integration tests for Matrix SDK class [org.matrix.android.sdk.PermalinkParserTest] API[${{ matrix.api-level }}]
|
- name: Run integration tests for Matrix SDK class [org.matrix.android.sdk.PermalinkParserTest] API[${{ matrix.api-level }}]
|
||||||
if: always()
|
if: always()
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
@ -169,7 +172,7 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
id: get-comment-body-permalink
|
id: get-comment-body-permalink
|
||||||
run: python3 ./tools/ci/render_test_output.py permalink ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
run: python3 ./tools/ci/render_test_output.py permalink ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml
|
||||||
# package: class PermalinkParserTest
|
# package: class PermalinkParserTest
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
if: always() && github.event_name == 'pull_request'
|
if: always() && github.event_name == 'pull_request'
|
||||||
uses: peter-evans/find-comment@v1
|
uses: peter-evans/find-comment@v1
|
||||||
|
@ -201,6 +204,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
api-level: [ 28 ]
|
api-level: [ 28 ]
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
@ -255,14 +259,15 @@ jobs:
|
||||||
notify:
|
notify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- integration-tests
|
- integration-tests
|
||||||
- ui-tests
|
- ui-tests
|
||||||
if: always() && github.event_name != 'workflow_dispatch'
|
if: always() && github.event_name != 'workflow_dispatch'
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: michaelkaye/matrix-hookshot-action@v0.2.0
|
- uses: michaelkaye/matrix-hookshot-action@v0.2.0
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
|
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
|
||||||
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
|
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
|
||||||
text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
||||||
html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{name}} {{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a>{{/if}}{{/with}}{{/each}}"
|
html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{name}} {{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a>{{/if}}{{/with}}{{/each}}"
|
12
.github/workflows/quality.yml
vendored
12
.github/workflows/quality.yml
vendored
|
@ -18,6 +18,10 @@ jobs:
|
||||||
ktlint:
|
ktlint:
|
||||||
name: Kotlin Linter
|
name: Kotlin Linter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Run ktlint
|
- name: Run ktlint
|
||||||
|
@ -87,6 +91,10 @@ jobs:
|
||||||
android-lint:
|
android-lint:
|
||||||
name: Android Linter
|
name: Android Linter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
|
@ -116,6 +124,10 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
target: [ Gplay, Fdroid ]
|
target: [ Gplay, Fdroid ]
|
||||||
|
# Allow all jobs on develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
|
|
84
.github/workflows/sanity_test.yml
vendored
84
.github/workflows/sanity_test.yml
vendored
|
@ -1,84 +0,0 @@
|
||||||
name: Sanity Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# At 20:00 every day UTC
|
|
||||||
- cron: '0 20 * * *'
|
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
|
||||||
env:
|
|
||||||
CI_GRADLE_ARG_PROPERTIES: >
|
|
||||||
-Porg.gradle.jvmargs=-Xmx4g
|
|
||||||
-Porg.gradle.parallel=false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
integration-tests:
|
|
||||||
name: Sanity Tests (Synapse)
|
|
||||||
runs-on: macos-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
api-level: [ 28 ]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: develop
|
|
||||||
- name: Set up Python 3.8
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
- name: Start synapse server
|
|
||||||
run: |
|
|
||||||
pip install matrix-synapse
|
|
||||||
curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
|
|
||||||
| sed s/127.0.0.1/0.0.0.0/g | sed 's/http:\/\/localhost/http:\/\/10.0.2.2/g' | bash -s -- --no-rate-limit
|
|
||||||
- uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
distribution: 'adopt'
|
|
||||||
java-version: '11'
|
|
||||||
- name: Run sanity tests on API ${{ matrix.api-level }}
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
with:
|
|
||||||
api-level: ${{ matrix.api-level }}
|
|
||||||
arch: x86
|
|
||||||
profile: Nexus 5X
|
|
||||||
force-avd-creation: false
|
|
||||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
|
||||||
script: |
|
|
||||||
adb root
|
|
||||||
adb logcat -c
|
|
||||||
touch emulator.log
|
|
||||||
chmod 777 emulator.log
|
|
||||||
adb logcat >> emulator.log &
|
|
||||||
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 )
|
|
||||||
- name: Upload Test Report Log
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: sanity-error-results
|
|
||||||
path: |
|
|
||||||
emulator.log
|
|
||||||
failure_screenshots/
|
|
||||||
|
|
||||||
|
|
||||||
notify:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: integration-tests
|
|
||||||
if: always()
|
|
||||||
steps:
|
|
||||||
- uses: michaelkaye/matrix-hookshot-action@v0.2.0
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
|
|
||||||
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
|
|
||||||
text_template: "Sanity test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}} {{html_url}}{{/if}}{{/with}}{{/each}}"
|
|
||||||
html_template: "CI Sanity test run results: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a>{{/if}}{{/with}}{{/each}}"
|
|
|
@ -9,6 +9,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'vector-im/element-android'
|
if: github.repository == 'vector-im/element-android'
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
|
@ -35,6 +36,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'vector-im/element-android'
|
if: github.repository == 'vector-im/element-android'
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
|
@ -60,6 +62,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'vector-im/element-android'
|
if: github.repository == 'vector-im/element-android'
|
||||||
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Run analytics import script
|
- name: Run analytics import script
|
||||||
|
|
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
@ -15,6 +15,10 @@ jobs:
|
||||||
unit-tests:
|
unit-tests:
|
||||||
name: Run Unit Tests
|
name: Run Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
|
|
1
changelog.d/5005.feature
Normal file
1
changelog.d/5005.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add possibility to save media from Gallery + reorder choices in message context menu
|
1
changelog.d/5325.feature
Normal file
1
changelog.d/5325.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Adds forceLoginFallback feature flag and usages to FTUE login and registration
|
|
@ -45,6 +45,8 @@ import kotlin.math.abs
|
||||||
|
|
||||||
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
|
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
|
||||||
|
|
||||||
|
protected val rootView: View
|
||||||
|
get() = views.rootContainer
|
||||||
protected val pager2: ViewPager2
|
protected val pager2: ViewPager2
|
||||||
get() = views.attachmentPager
|
get() = views.attachmentPager
|
||||||
protected val imageTransitionView: ImageView
|
protected val imageTransitionView: ImageView
|
||||||
|
@ -298,10 +300,11 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
||||||
|
|
||||||
private fun createSwipeToDismissHandler(): SwipeToDismissHandler =
|
private fun createSwipeToDismissHandler(): SwipeToDismissHandler =
|
||||||
SwipeToDismissHandler(
|
SwipeToDismissHandler(
|
||||||
swipeView = views.dismissContainer,
|
swipeView = views.dismissContainer,
|
||||||
shouldAnimateDismiss = { shouldAnimateDismiss() },
|
shouldAnimateDismiss = { shouldAnimateDismiss() },
|
||||||
onDismiss = { animateClose() },
|
onDismiss = { animateClose() },
|
||||||
onSwipeViewMove = ::handleSwipeViewMove)
|
onSwipeViewMove = ::handleSwipeViewMove
|
||||||
|
)
|
||||||
|
|
||||||
private fun createSwipeDirectionDetector() =
|
private fun createSwipeDirectionDetector() =
|
||||||
SwipeDirectionDetector(this) { swipeDirection = it }
|
SwipeDirectionDetector(this) { swipeDirection = it }
|
||||||
|
|
|
@ -53,7 +53,7 @@ class DebugFeaturesStateFactory @Inject constructor(
|
||||||
label = "FTUE Personalize profile",
|
label = "FTUE Personalize profile",
|
||||||
key = DebugFeatureKeys.onboardingPersonalize,
|
key = DebugFeatureKeys.onboardingPersonalize,
|
||||||
factory = VectorFeatures::isOnboardingPersonalizeEnabled
|
factory = VectorFeatures::isOnboardingPersonalizeEnabled
|
||||||
)
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,13 @@ class DebugPrivateSettingsFragment : VectorBaseFragment<FragmentDebugPrivateSett
|
||||||
views.forceDialPadTabDisplay.setOnCheckedChangeListener { _, isChecked ->
|
views.forceDialPadTabDisplay.setOnCheckedChangeListener { _, isChecked ->
|
||||||
viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked))
|
viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked))
|
||||||
}
|
}
|
||||||
|
views.forceLoginFallback.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
viewModel.handle(DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled(isChecked))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) {
|
override fun invalidate() = withState(viewModel) {
|
||||||
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
|
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
|
||||||
|
views.forceLoginFallback.isChecked = it.forceLoginFallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
|
sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
|
||||||
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
|
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
|
||||||
|
data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions()
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,15 +45,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
private fun observeVectorDataStore() {
|
private fun observeVectorDataStore() {
|
||||||
vectorDataStore.forceDialPadDisplayFlow.setOnEach {
|
vectorDataStore.forceDialPadDisplayFlow.setOnEach {
|
||||||
copy(
|
copy(dialPadVisible = it)
|
||||||
dialPadVisible = it
|
}
|
||||||
)
|
|
||||||
|
vectorDataStore.forceLoginFallbackFlow.setOnEach {
|
||||||
|
copy(forceLoginFallback = it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: DebugPrivateSettingsViewActions) {
|
override fun handle(action: DebugPrivateSettingsViewActions) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
|
is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
|
||||||
|
is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,4 +65,10 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
|
||||||
vectorDataStore.setForceDialPadDisplay(action.force)
|
vectorDataStore.setForceDialPadDisplay(action.force)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSetForceLoginFallbackEnabled(action: DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
vectorDataStore.setForceLoginFallbackFlow(action.force)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,5 +19,6 @@ package im.vector.app.features.debug.settings
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
|
||||||
data class DebugPrivateSettingsViewState(
|
data class DebugPrivateSettingsViewState(
|
||||||
val dialPadVisible: Boolean = false
|
val dialPadVisible: Boolean = false,
|
||||||
|
val forceLoginFallback: Boolean = false,
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
|
|
@ -25,6 +25,12 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Force DialPad tab display" />
|
android:text="Force DialPad tab display" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/forceLoginFallback"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Force login and registration fallback" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -58,6 +58,7 @@ import im.vector.app.features.login.LoginViewModel
|
||||||
import im.vector.app.features.login2.LoginViewModel2
|
import im.vector.app.features.login2.LoginViewModel2
|
||||||
import im.vector.app.features.login2.created.AccountCreatedViewModel
|
import im.vector.app.features.login2.created.AccountCreatedViewModel
|
||||||
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
|
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
|
||||||
|
import im.vector.app.features.media.VectorAttachmentViewerViewModel
|
||||||
import im.vector.app.features.onboarding.OnboardingViewModel
|
import im.vector.app.features.onboarding.OnboardingViewModel
|
||||||
import im.vector.app.features.poll.create.CreatePollViewModel
|
import im.vector.app.features.poll.create.CreatePollViewModel
|
||||||
import im.vector.app.features.qrcode.QrCodeScannerViewModel
|
import im.vector.app.features.qrcode.QrCodeScannerViewModel
|
||||||
|
@ -594,4 +595,9 @@ interface MavericksViewModelModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@MavericksViewModelKey(LocationSharingViewModel::class)
|
@MavericksViewModelKey(LocationSharingViewModel::class)
|
||||||
fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
|
||||||
|
fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
}
|
}
|
||||||
|
|
|
@ -343,24 +343,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||||
add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType()))
|
add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canRedact(timelineEvent, actionPermissions)) {
|
|
||||||
if (timelineEvent.root.getClearType() == EventType.POLL_START) {
|
|
||||||
add(EventSharedAction.Redact(
|
|
||||||
eventId,
|
|
||||||
askForReason = informationData.senderId != session.myUserId,
|
|
||||||
dialogTitleRes = R.string.delete_poll_dialog_title,
|
|
||||||
dialogDescriptionRes = R.string.delete_poll_dialog_content
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
add(EventSharedAction.Redact(
|
|
||||||
eventId,
|
|
||||||
askForReason = informationData.senderId != session.myUserId,
|
|
||||||
dialogTitleRes = R.string.delete_event_dialog_title,
|
|
||||||
dialogDescriptionRes = R.string.delete_event_dialog_content
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canCopy(msgType)) {
|
if (canCopy(msgType)) {
|
||||||
// TODO copy images? html? see ClipBoard
|
// TODO copy images? html? see ClipBoard
|
||||||
add(EventSharedAction.Copy(messageContent!!.body))
|
add(EventSharedAction.Copy(messageContent!!.body))
|
||||||
|
@ -382,12 +364,30 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||||
add(EventSharedAction.ViewEditHistory(informationData))
|
add(EventSharedAction.ViewEditHistory(informationData))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
|
||||||
|
add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
|
||||||
|
}
|
||||||
|
|
||||||
if (canShare(msgType)) {
|
if (canShare(msgType)) {
|
||||||
add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!))
|
add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
|
if (canRedact(timelineEvent, actionPermissions)) {
|
||||||
add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
|
if (timelineEvent.root.getClearType() == EventType.POLL_START) {
|
||||||
|
add(EventSharedAction.Redact(
|
||||||
|
eventId,
|
||||||
|
askForReason = informationData.senderId != session.myUserId,
|
||||||
|
dialogTitleRes = R.string.delete_poll_dialog_title,
|
||||||
|
dialogDescriptionRes = R.string.delete_poll_dialog_content
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
add(EventSharedAction.Redact(
|
||||||
|
eventId,
|
||||||
|
askForReason = informationData.senderId != session.myUserId,
|
||||||
|
dialogTitleRes = R.string.delete_event_dialog_title,
|
||||||
|
dialogDescriptionRes = R.string.delete_event_dialog_content
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.media
|
||||||
|
|
||||||
|
interface AttachmentInteractionListener {
|
||||||
|
fun onDismiss()
|
||||||
|
fun onShare()
|
||||||
|
fun onDownload()
|
||||||
|
fun onPlayPause(play: Boolean)
|
||||||
|
fun videoSeekTo(percent: Int)
|
||||||
|
}
|
|
@ -30,35 +30,33 @@ class AttachmentOverlayView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||||
) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
|
) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
|
||||||
|
|
||||||
var onShareCallback: (() -> Unit)? = null
|
var interactionListener: AttachmentInteractionListener? = null
|
||||||
var onBack: (() -> Unit)? = null
|
|
||||||
var onPlayPause: ((play: Boolean) -> Unit)? = null
|
|
||||||
var videoSeekTo: ((progress: Int) -> Unit)? = null
|
|
||||||
|
|
||||||
val views: MergeImageAttachmentOverlayBinding
|
val views: MergeImageAttachmentOverlayBinding
|
||||||
|
|
||||||
var isPlaying = false
|
private var isPlaying = false
|
||||||
|
private var suspendSeekBarUpdate = false
|
||||||
var suspendSeekBarUpdate = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
inflate(context, R.layout.merge_image_attachment_overlay, this)
|
inflate(context, R.layout.merge_image_attachment_overlay, this)
|
||||||
views = MergeImageAttachmentOverlayBinding.bind(this)
|
views = MergeImageAttachmentOverlayBinding.bind(this)
|
||||||
setBackgroundColor(Color.TRANSPARENT)
|
setBackgroundColor(Color.TRANSPARENT)
|
||||||
views.overlayBackButton.setOnClickListener {
|
views.overlayBackButton.setOnClickListener {
|
||||||
onBack?.invoke()
|
interactionListener?.onDismiss()
|
||||||
}
|
}
|
||||||
views.overlayShareButton.setOnClickListener {
|
views.overlayShareButton.setOnClickListener {
|
||||||
onShareCallback?.invoke()
|
interactionListener?.onShare()
|
||||||
|
}
|
||||||
|
views.overlayDownloadButton.setOnClickListener {
|
||||||
|
interactionListener?.onDownload()
|
||||||
}
|
}
|
||||||
views.overlayPlayPauseButton.setOnClickListener {
|
views.overlayPlayPauseButton.setOnClickListener {
|
||||||
onPlayPause?.invoke(!isPlaying)
|
interactionListener?.onPlayPause(!isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
views.overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
videoSeekTo?.invoke(progress)
|
interactionListener?.videoSeekTo(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,14 +49,7 @@ abstract class BaseAttachmentProvider<Type>(
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider
|
||||||
) : AttachmentSourceProvider {
|
) : AttachmentSourceProvider {
|
||||||
|
|
||||||
interface InteractionListener {
|
var interactionListener: AttachmentInteractionListener? = null
|
||||||
fun onDismissTapped()
|
|
||||||
fun onShareTapped()
|
|
||||||
fun onPlayPause(play: Boolean)
|
|
||||||
fun videoSeekTo(percent: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
var interactionListener: InteractionListener? = null
|
|
||||||
|
|
||||||
private var overlayView: AttachmentOverlayView? = null
|
private var overlayView: AttachmentOverlayView? = null
|
||||||
|
|
||||||
|
@ -68,18 +61,7 @@ abstract class BaseAttachmentProvider<Type>(
|
||||||
if (position == -1) return null
|
if (position == -1) return null
|
||||||
if (overlayView == null) {
|
if (overlayView == null) {
|
||||||
overlayView = AttachmentOverlayView(context)
|
overlayView = AttachmentOverlayView(context)
|
||||||
overlayView?.onBack = {
|
overlayView?.interactionListener = interactionListener
|
||||||
interactionListener?.onDismissTapped()
|
|
||||||
}
|
|
||||||
overlayView?.onShareCallback = {
|
|
||||||
interactionListener?.onShareTapped()
|
|
||||||
}
|
|
||||||
overlayView?.onPlayPause = { play ->
|
|
||||||
interactionListener?.onPlayPause(play)
|
|
||||||
}
|
|
||||||
overlayView?.videoSeekTo = { percent ->
|
|
||||||
interactionListener?.videoSeekTo(percent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val timelineEvent = getTimelineEventAtPosition(position)
|
val timelineEvent = getTimelineEventAtPosition(position)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.media
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
sealed class VectorAttachmentViewerAction : VectorViewModelAction {
|
||||||
|
data class DownloadMedia(val file: File) : VectorAttachmentViewerAction()
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ package im.vector.app.features.media
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -30,16 +31,25 @@ import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.transition.Transition
|
import androidx.transition.Transition
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.core.extensions.singletonEntryPoint
|
||||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||||
|
import im.vector.app.core.platform.showOptimizedSnackbar
|
||||||
|
import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||||
|
import im.vector.app.core.utils.checkPermissions
|
||||||
|
import im.vector.app.core.utils.onPermissionDeniedDialog
|
||||||
|
import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
import im.vector.app.core.utils.shareMedia
|
import im.vector.app.core.utils.shareMedia
|
||||||
import im.vector.app.features.themes.ActivityOtherThemes
|
import im.vector.app.features.themes.ActivityOtherThemes
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import im.vector.lib.attachmentviewer.AttachmentCommands
|
import im.vector.lib.attachmentviewer.AttachmentCommands
|
||||||
import im.vector.lib.attachmentviewer.AttachmentViewerActivity
|
import im.vector.lib.attachmentviewer.AttachmentViewerActivity
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
@ -47,7 +57,7 @@ import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
|
class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInteractionListener {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Args(
|
data class Args(
|
||||||
|
@ -58,15 +68,28 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var sessionHolder: ActiveSessionHolder
|
lateinit var sessionHolder: ActiveSessionHolder
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dataSourceFactory: AttachmentProviderFactory
|
lateinit var dataSourceFactory: AttachmentProviderFactory
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var imageContentRenderer: ImageContentRenderer
|
lateinit var imageContentRenderer: ImageContentRenderer
|
||||||
|
|
||||||
|
private val viewModel: VectorAttachmentViewerViewModel by viewModel()
|
||||||
|
private val errorFormatter by lazy(LazyThreadSafetyMode.NONE) { singletonEntryPoint().errorFormatter() }
|
||||||
private var initialIndex = 0
|
private var initialIndex = 0
|
||||||
private var isAnimatingOut = false
|
private var isAnimatingOut = false
|
||||||
|
|
||||||
private var currentSourceProvider: BaseAttachmentProvider<*>? = null
|
private var currentSourceProvider: BaseAttachmentProvider<*>? = null
|
||||||
|
private val downloadActionResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
|
||||||
|
if (allGranted) {
|
||||||
|
viewModel.pendingAction?.let {
|
||||||
|
viewModel.handle(it)
|
||||||
|
}
|
||||||
|
} else if (deniedPermanently) {
|
||||||
|
onPermissionDeniedDialog(R.string.denied_permission_generic)
|
||||||
|
}
|
||||||
|
viewModel.pendingAction = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -128,6 +151,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
|
||||||
|
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
|
window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
|
window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
|
||||||
|
|
||||||
|
observeViewEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -140,12 +165,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
|
||||||
Timber.i("onPause Activity ${javaClass.simpleName}")
|
Timber.i("onPause Activity ${javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
|
|
||||||
|
|
||||||
override fun shouldAnimateDismiss(): Boolean {
|
|
||||||
return currentPosition != initialIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (currentPosition == initialIndex) {
|
if (currentPosition == initialIndex) {
|
||||||
// show back the transition view
|
// show back the transition view
|
||||||
|
@ -156,6 +175,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun shouldAnimateDismiss(): Boolean {
|
||||||
|
return currentPosition != initialIndex
|
||||||
|
}
|
||||||
|
|
||||||
override fun animateClose() {
|
override fun animateClose() {
|
||||||
if (currentPosition == initialIndex) {
|
if (currentPosition == initialIndex) {
|
||||||
// show back the transition view
|
// show back the transition view
|
||||||
|
@ -166,9 +189,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
|
||||||
ActivityCompat.finishAfterTransition(this)
|
ActivityCompat.finishAfterTransition(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================================
|
private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
|
||||||
// PRIVATE METHODS
|
|
||||||
// ==========================================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try and add a [Transition.TransitionListener] to the entering shared element
|
* Try and add a [Transition.TransitionListener] to the entering shared element
|
||||||
|
@ -218,10 +239,72 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeViewEvents() {
|
||||||
|
viewModel.viewEvents
|
||||||
|
.stream()
|
||||||
|
.onEach(::handleViewEvents)
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleViewEvents(event: VectorAttachmentViewerViewEvents) {
|
||||||
|
when (event) {
|
||||||
|
is VectorAttachmentViewerViewEvents.ErrorDownloadingMedia -> showSnackBarError(event.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnackBarError(error: Throwable) {
|
||||||
|
rootView.showOptimizedSnackbar(errorFormatter.toHumanReadable(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasWritePermission() =
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
|
||||||
|
checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, downloadActionResultLauncher)
|
||||||
|
|
||||||
|
override fun onDismiss() {
|
||||||
|
animateClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayPause(play: Boolean) {
|
||||||
|
handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoSeekTo(percent: Int) {
|
||||||
|
handle(AttachmentCommands.SeekTo(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShare() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
shareMedia(
|
||||||
|
this@VectorAttachmentViewerActivity,
|
||||||
|
file,
|
||||||
|
getMimeTypeFromUri(this@VectorAttachmentViewerActivity, file.toUri())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDownload() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val hasWritePermission = withContext(Dispatchers.Main) {
|
||||||
|
hasWritePermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch
|
||||||
|
if (hasWritePermission) {
|
||||||
|
viewModel.handle(VectorAttachmentViewerAction.DownloadMedia(file))
|
||||||
|
} else {
|
||||||
|
viewModel.pendingAction = VectorAttachmentViewerAction.DownloadMedia(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_ARGS = "EXTRA_ARGS"
|
private const val EXTRA_ARGS = "EXTRA_ARGS"
|
||||||
const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
|
private const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
|
||||||
const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
|
private const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
|
||||||
|
|
||||||
fun newIntent(context: Context,
|
fun newIntent(context: Context,
|
||||||
mediaData: AttachmentData,
|
mediaData: AttachmentData,
|
||||||
|
@ -236,30 +319,4 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDismissTapped() {
|
|
||||||
animateClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlayPause(play: Boolean) {
|
|
||||||
handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoSeekTo(percent: Int) {
|
|
||||||
handle(AttachmentCommands.SeekTo(percent))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShareTapped() {
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val file = currentSourceProvider?.getFileForSharing(currentPosition) ?: return@launch
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
shareMedia(
|
|
||||||
this@VectorAttachmentViewerActivity,
|
|
||||||
file,
|
|
||||||
getMimeTypeFromUri(this@VectorAttachmentViewerActivity, file.toUri())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.media
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class VectorAttachmentViewerViewEvents : VectorViewEvents {
|
||||||
|
data class ErrorDownloadingMedia(val error: Throwable) : VectorAttachmentViewerViewEvents()
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.media
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
|
import im.vector.app.core.platform.VectorDummyViewState
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.media.domain.usecase.DownloadMediaUseCase
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
|
||||||
|
class VectorAttachmentViewerViewModel @AssistedInject constructor(
|
||||||
|
@Assisted initialState: VectorDummyViewState,
|
||||||
|
private val session: Session,
|
||||||
|
private val downloadMediaUseCase: DownloadMediaUseCase
|
||||||
|
) : VectorViewModel<VectorDummyViewState, VectorAttachmentViewerAction, VectorAttachmentViewerViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : MavericksAssistedViewModelFactory<VectorAttachmentViewerViewModel, VectorDummyViewState> {
|
||||||
|
override fun create(initialState: VectorDummyViewState): VectorAttachmentViewerViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<VectorAttachmentViewerViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
|
var pendingAction: VectorAttachmentViewerAction? = null
|
||||||
|
|
||||||
|
override fun handle(action: VectorAttachmentViewerAction) {
|
||||||
|
when (action) {
|
||||||
|
is VectorAttachmentViewerAction.DownloadMedia -> handleDownloadAction(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDownloadAction(action: VectorAttachmentViewerAction.DownloadMedia) {
|
||||||
|
// launch in the coroutine scope session to avoid binding the coroutine to the lifecycle of the VM
|
||||||
|
session.coroutineScope.launch {
|
||||||
|
// Success event is handled via a notification inside the use case
|
||||||
|
downloadMediaUseCase.execute(action.file)
|
||||||
|
.onFailure { _viewEvents.post(VectorAttachmentViewerViewEvents.ErrorDownloadingMedia(it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.media.domain.usecase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||||
|
import im.vector.app.core.utils.saveMedia
|
||||||
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DownloadMediaUseCase @Inject constructor(
|
||||||
|
@ApplicationContext private val appContext: Context,
|
||||||
|
private val session: Session,
|
||||||
|
private val notificationUtils: NotificationUtils
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute(input: File): Result<Unit> = withContext(session.coroutineDispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
saveMedia(
|
||||||
|
context = appContext,
|
||||||
|
file = input,
|
||||||
|
title = input.name,
|
||||||
|
mediaMimeType = getMimeTypeFromUri(appContext, input.toUri()),
|
||||||
|
notificationUtils = notificationUtils
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import im.vector.app.features.login.LoginMode
|
||||||
import im.vector.app.features.login.ReAuthHelper
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import im.vector.app.features.login.ServerType
|
import im.vector.app.features.login.ServerType
|
||||||
import im.vector.app.features.login.SignMode
|
import im.vector.app.features.login.SignMode
|
||||||
|
import im.vector.app.features.settings.VectorDataStore
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||||
|
@ -78,7 +79,8 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val homeServerHistoryService: HomeServerHistoryService,
|
private val homeServerHistoryService: HomeServerHistoryService,
|
||||||
private val vectorFeatures: VectorFeatures,
|
private val vectorFeatures: VectorFeatures,
|
||||||
private val analyticsTracker: AnalyticsTracker
|
private val analyticsTracker: AnalyticsTracker,
|
||||||
|
private val vectorDataStore: VectorDataStore,
|
||||||
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
|
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
@ -90,6 +92,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getKnownCustomHomeServersUrls()
|
getKnownCustomHomeServersUrls()
|
||||||
|
observeDataStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getKnownCustomHomeServersUrls() {
|
private fun getKnownCustomHomeServersUrls() {
|
||||||
|
@ -98,6 +101,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeDataStore() = viewModelScope.launch {
|
||||||
|
vectorDataStore.forceLoginFallbackFlow.setOnEach { isForceLoginFallbackEnabled ->
|
||||||
|
copy(isForceLoginFallbackEnabled = isForceLoginFallbackEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store the last action, to redo it after user has trusted the untrusted certificate
|
// Store the last action, to redo it after user has trusted the untrusted certificate
|
||||||
private var lastAction: OnboardingAction? = null
|
private var lastAction: OnboardingAction? = null
|
||||||
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
|
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
|
||||||
|
|
|
@ -62,7 +62,8 @@ data class OnboardingViewState(
|
||||||
// Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
|
// Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable
|
||||||
@PersistState
|
@PersistState
|
||||||
val loginModeSupportedTypes: List<String> = emptyList(),
|
val loginModeSupportedTypes: List<String> = emptyList(),
|
||||||
val knownCustomHomeServersUrls: List<String> = emptyList()
|
val knownCustomHomeServersUrls: List<String> = emptyList(),
|
||||||
|
val isForceLoginFallbackEnabled: Boolean = false,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
fun isLoading(): Boolean {
|
fun isLoading(): Boolean {
|
||||||
|
|
|
@ -75,6 +75,8 @@ class FtueAuthVariant(
|
||||||
private val popEnterAnim = R.anim.no_anim
|
private val popEnterAnim = R.anim.no_anim
|
||||||
private val popExitAnim = R.anim.exit_fade_out
|
private val popExitAnim = R.anim.exit_fade_out
|
||||||
|
|
||||||
|
private var isForceLoginFallbackEnabled = false
|
||||||
|
|
||||||
private val topFragment: Fragment?
|
private val topFragment: Fragment?
|
||||||
get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id)
|
get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id)
|
||||||
|
|
||||||
|
@ -109,10 +111,6 @@ class FtueAuthVariant(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setIsLoading(isLoading: Boolean) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addFirstFragment() {
|
private fun addFirstFragment() {
|
||||||
val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) {
|
val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) {
|
||||||
true -> FtueAuthSplashCarouselFragment::class.java
|
true -> FtueAuthSplashCarouselFragment::class.java
|
||||||
|
@ -121,11 +119,25 @@ class FtueAuthVariant(
|
||||||
activity.addFragment(views.loginFragmentContainer, splashFragment)
|
activity.addFragment(views.loginFragmentContainer, splashFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateWithState(viewState: OnboardingViewState) {
|
||||||
|
isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled
|
||||||
|
views.loginLoading.isVisible = shouldShowLoading(viewState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldShowLoading(viewState: OnboardingViewState) =
|
||||||
|
if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
|
||||||
|
viewState.isLoading()
|
||||||
|
} else {
|
||||||
|
// Keep loading when during success because of the delay when switching to the next Activity
|
||||||
|
viewState.isLoading() || viewState.isAuthTaskCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setIsLoading(isLoading: Boolean) = Unit
|
||||||
|
|
||||||
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
|
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
|
||||||
when (viewEvents) {
|
when (viewEvents) {
|
||||||
is OnboardingViewEvents.RegistrationFlowResult -> {
|
is OnboardingViewEvents.RegistrationFlowResult -> {
|
||||||
// Check that all flows are supported by the application
|
if (registrationShouldFallback(viewEvents)) {
|
||||||
if (viewEvents.flowResult.missingStages.any { !it.isSupported() }) {
|
|
||||||
// Display a popup to propose use web fallback
|
// Display a popup to propose use web fallback
|
||||||
onRegistrationStageNotSupported()
|
onRegistrationStageNotSupported()
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,11 +148,7 @@ class FtueAuthVariant(
|
||||||
// First ask for login and password
|
// First ask for login and password
|
||||||
// I add a tag to indicate that this fragment is a registration stage.
|
// I add a tag to indicate that this fragment is a registration stage.
|
||||||
// This way it will be automatically popped in when starting the next registration stage
|
// This way it will be automatically popped in when starting the next registration stage
|
||||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
|
||||||
FtueAuthLoginFragment::class.java,
|
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
|
||||||
option = commonOption
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,13 +236,23 @@ class FtueAuthVariant(
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWithState(viewState: OnboardingViewState) {
|
private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) =
|
||||||
views.loginLoading.isVisible = if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
|
isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow()
|
||||||
viewState.isLoading()
|
|
||||||
} else {
|
private fun OnboardingViewEvents.RegistrationFlowResult.containsUnsupportedRegistrationFlow() =
|
||||||
// Keep loading when during success because of the delay when switching to the next Activity
|
flowResult.missingStages.any { !it.isSupported() }
|
||||||
viewState.isLoading() || viewState.isAuthTaskCompleted()
|
|
||||||
}
|
private fun onRegistrationStageNotSupported() {
|
||||||
|
MaterialAlertDialogBuilder(activity)
|
||||||
|
.setTitle(R.string.app_name)
|
||||||
|
.setMessage(activity.getString(R.string.login_registration_not_supported))
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
|
FtueAuthWebFragment::class.java,
|
||||||
|
option = commonOption)
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.no, null)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onWebLoginError(onWebLoginError: OnboardingViewEvents.OnWebLoginError) {
|
private fun onWebLoginError(onWebLoginError: OnboardingViewEvents.OnWebLoginError) {
|
||||||
|
@ -264,29 +282,58 @@ class FtueAuthVariant(
|
||||||
// state.signMode could not be ready yet. So use value from the ViewEvent
|
// state.signMode could not be ready yet. So use value from the ViewEvent
|
||||||
when (OnboardingViewEvents.signMode) {
|
when (OnboardingViewEvents.signMode) {
|
||||||
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
|
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
|
||||||
SignMode.SignUp -> {
|
SignMode.SignUp -> Unit // This case is processed in handleOnboardingViewEvents
|
||||||
// This is managed by the OnboardingViewEvents
|
SignMode.SignIn -> handleSignInSelected(state)
|
||||||
}
|
SignMode.SignInWithMatrixId -> handleSignInWithMatrixId(state)
|
||||||
SignMode.SignIn -> {
|
|
||||||
// It depends on the LoginMode
|
|
||||||
when (state.loginMode) {
|
|
||||||
LoginMode.Unknown,
|
|
||||||
is LoginMode.Sso -> error("Developer error")
|
|
||||||
is LoginMode.SsoAndPassword,
|
|
||||||
LoginMode.Password -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
|
||||||
FtueAuthLoginFragment::class.java,
|
|
||||||
tag = FRAGMENT_LOGIN_TAG,
|
|
||||||
option = commonOption)
|
|
||||||
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
|
|
||||||
}.exhaustive
|
|
||||||
}
|
|
||||||
SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
|
||||||
FtueAuthLoginFragment::class.java,
|
|
||||||
tag = FRAGMENT_LOGIN_TAG,
|
|
||||||
option = commonOption)
|
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSignInSelected(state: OnboardingViewState) {
|
||||||
|
if (isForceLoginFallbackEnabled) {
|
||||||
|
onLoginModeNotSupported(state.loginModeSupportedTypes)
|
||||||
|
} else {
|
||||||
|
disambiguateLoginMode(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disambiguateLoginMode(state: OnboardingViewState) = when (state.loginMode) {
|
||||||
|
LoginMode.Unknown,
|
||||||
|
is LoginMode.Sso -> error("Developer error")
|
||||||
|
is LoginMode.SsoAndPassword,
|
||||||
|
LoginMode.Password -> openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG)
|
||||||
|
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAuthLoginFragmentWithTag(tag: String) {
|
||||||
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
|
FtueAuthLoginFragment::class.java,
|
||||||
|
tag = tag,
|
||||||
|
option = commonOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
|
||||||
|
MaterialAlertDialogBuilder(activity)
|
||||||
|
.setTitle(R.string.app_name)
|
||||||
|
.setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ -> openAuthWebFragment() }
|
||||||
|
.setNegativeButton(R.string.no, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSignInWithMatrixId(state: OnboardingViewState) {
|
||||||
|
if (isForceLoginFallbackEnabled) {
|
||||||
|
onLoginModeNotSupported(state.loginModeSupportedTypes)
|
||||||
|
} else {
|
||||||
|
openAuthLoginFragmentWithTag(FRAGMENT_LOGIN_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAuthWebFragment() {
|
||||||
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
|
FtueAuthWebFragment::class.java,
|
||||||
|
option = commonOption)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the SSO redirection here
|
* Handle the SSO redirection here
|
||||||
*/
|
*/
|
||||||
|
@ -296,32 +343,6 @@ class FtueAuthVariant(
|
||||||
?.let { onboardingViewModel.handle(OnboardingAction.LoginWithToken(it)) }
|
?.let { onboardingViewModel.handle(OnboardingAction.LoginWithToken(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRegistrationStageNotSupported() {
|
|
||||||
MaterialAlertDialogBuilder(activity)
|
|
||||||
.setTitle(R.string.app_name)
|
|
||||||
.setMessage(activity.getString(R.string.login_registration_not_supported))
|
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
|
||||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
|
||||||
FtueAuthWebFragment::class.java,
|
|
||||||
option = commonOption)
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.no, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
|
|
||||||
MaterialAlertDialogBuilder(activity)
|
|
||||||
.setTitle(R.string.app_name)
|
|
||||||
.setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
|
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
|
||||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
|
||||||
FtueAuthWebFragment::class.java,
|
|
||||||
option = commonOption)
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.no, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleRegistrationNavigation(flowResult: FlowResult) {
|
private fun handleRegistrationNavigation(flowResult: FlowResult) {
|
||||||
// Complete all mandatory stages first
|
// Complete all mandatory stages first
|
||||||
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
|
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
|
||||||
|
|
|
@ -59,4 +59,16 @@ class VectorDataStore @Inject constructor(
|
||||||
settings[forceDialPadDisplay] = force
|
settings[forceDialPadDisplay] = force
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val forceLoginFallback = booleanPreferencesKey("force_login_fallback")
|
||||||
|
|
||||||
|
val forceLoginFallbackFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
|
||||||
|
preferences[forceLoginFallback].orFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setForceLoginFallbackFlow(force: Boolean) {
|
||||||
|
context.dataStore.edit { settings ->
|
||||||
|
settings[forceLoginFallback] = force
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,23 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/overlayCounterText"
|
app:layout_constraintTop_toBottomOf="@id/overlayCounterText"
|
||||||
tools:text="Bill 29 Jun at 19:42" />
|
tools:text="Bill 29 Jun at 19:42" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/overlayDownloadButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:contentDescription="@string/action_download"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="6dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/overlayShareButton"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/overlayTopBackground"
|
||||||
|
app:srcCompat="@drawable/ic_material_save"
|
||||||
|
app:tint="?colorOnPrimary"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/overlayShareButton"
|
android:id="@+id/overlayShareButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.media.domain.usecase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.airbnb.mvrx.test.MvRxTestRule
|
||||||
|
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||||
|
import im.vector.app.core.utils.saveMedia
|
||||||
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
|
import im.vector.app.test.fakes.FakeFile
|
||||||
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.impl.annotations.OverrideMockKs
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import io.mockk.verifyAll
|
||||||
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DownloadMediaUseCaseTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val mvRxTestRule = MvRxTestRule()
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
lateinit var appContext: Context
|
||||||
|
|
||||||
|
private val session = FakeSession()
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
lateinit var notificationUtils: NotificationUtils
|
||||||
|
|
||||||
|
private val file = FakeFile()
|
||||||
|
|
||||||
|
@OverrideMockKs
|
||||||
|
lateinit var downloadMediaUseCase: DownloadMediaUseCase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
mockkStatic("im.vector.app.core.utils.ExternalApplicationsUtilKt")
|
||||||
|
mockkStatic("im.vector.app.core.intent.VectorMimeTypeKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic("im.vector.app.core.utils.ExternalApplicationsUtilKt")
|
||||||
|
unmockkStatic("im.vector.app.core.intent.VectorMimeTypeKt")
|
||||||
|
file.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a file when calling execute then save the file in local with success`() = runBlockingTest {
|
||||||
|
// Given
|
||||||
|
val uri = mockk<Uri>()
|
||||||
|
val mimeType = "mimeType"
|
||||||
|
val name = "filename"
|
||||||
|
every { getMimeTypeFromUri(appContext, uri) } returns mimeType
|
||||||
|
file.givenName(name)
|
||||||
|
file.givenUri(uri)
|
||||||
|
coEvery { saveMedia(any(), any(), any(), any(), any()) } just runs
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = downloadMediaUseCase.execute(file.instance)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assert(result.isSuccess)
|
||||||
|
verifyAll {
|
||||||
|
file.instance.name
|
||||||
|
file.instance.toUri()
|
||||||
|
}
|
||||||
|
verify {
|
||||||
|
getMimeTypeFromUri(appContext, uri)
|
||||||
|
}
|
||||||
|
coVerify {
|
||||||
|
saveMedia(appContext, file.instance, name, mimeType, notificationUtils)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a file when calling execute then save the file in local with error`() = runBlockingTest {
|
||||||
|
// Given
|
||||||
|
val uri = mockk<Uri>()
|
||||||
|
val mimeType = "mimeType"
|
||||||
|
val name = "filename"
|
||||||
|
val error = Throwable()
|
||||||
|
file.givenName(name)
|
||||||
|
file.givenUri(uri)
|
||||||
|
every { getMimeTypeFromUri(appContext, uri) } returns mimeType
|
||||||
|
coEvery { saveMedia(any(), any(), any(), any(), any()) } throws error
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = downloadMediaUseCase.execute(file.instance)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assert(result.isFailure && result.exceptionOrNull() == error)
|
||||||
|
verifyAll {
|
||||||
|
file.instance.name
|
||||||
|
file.instance.toUri()
|
||||||
|
}
|
||||||
|
verify {
|
||||||
|
getMimeTypeFromUri(appContext, uri)
|
||||||
|
}
|
||||||
|
coVerify {
|
||||||
|
saveMedia(appContext, file.instance, name, mimeType, notificationUtils)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
Normal file
49
vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FakeFile {
|
||||||
|
|
||||||
|
val instance = mockk<File>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
mockkStatic(Uri::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be called after tests.
|
||||||
|
*/
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic(Uri::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenName(name: String) {
|
||||||
|
every { instance.name } returns name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenUri(uri: Uri) {
|
||||||
|
every { instance.toUri() } returns uri
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue