diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 91dc6d830b..1ba71c1f61 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,6 +20,10 @@ jobs:
fail-fast: false
matrix:
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:
- uses: actions/checkout@v2
- uses: actions/cache@v2
@@ -43,6 +47,7 @@ jobs:
name: Build unsigned GPlay APKs
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
+ # Only runs on main, no concurrency.
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index 405a2b3065..ee4a87293f 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -5,6 +5,7 @@ jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
+ # No concurrency required, this is a prerequisite to other actions and should run every time.
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index f688f5579e..192be1fe9e 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -20,6 +20,7 @@ jobs:
build-android-test-matrix-sdk:
name: Matrix SDK - Build Android Tests
runs-on: macos-latest
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
@@ -41,6 +42,7 @@ jobs:
build-android-test-app:
name: App - Build Android Tests
runs-on: macos-latest
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
@@ -58,7 +60,7 @@ jobs:
- name: Build Android Tests for vector
run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
- # Run Android Tests
+ # Run Android Tests
integration-tests:
name: Matrix SDK - Running Integration Tests
runs-on: macos-latest
@@ -66,6 +68,7 @@ jobs:
fail-fast: false
matrix:
api-level: [ 28 ]
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- 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
chmod 777 start.sh
./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 }}]
uses: reactivecircus/android-emulator-runner@v2
with:
@@ -121,7 +124,7 @@ jobs:
if: always()
id: get-comment-body-account
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 }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
@@ -137,7 +140,7 @@ jobs:
if: always()
id: get-comment-body-internal
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 }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
@@ -153,7 +156,7 @@ jobs:
if: always()
id: get-comment-body-ordering
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 }}]
if: always()
uses: reactivecircus/android-emulator-runner@v2
@@ -169,7 +172,7 @@ jobs:
if: always()
id: get-comment-body-permalink
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
if: always() && github.event_name == 'pull_request'
uses: peter-evans/find-comment@v1
@@ -201,6 +204,7 @@ jobs:
fail-fast: false
matrix:
api-level: [ 28 ]
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
with:
@@ -255,14 +259,15 @@ jobs:
notify:
runs-on: ubuntu-latest
needs:
- - integration-tests
- - ui-tests
+ - integration-tests
+ - ui-tests
if: always() && github.event_name != 'workflow_dispatch'
+ # No concurrency required, runs every time on a schedule.
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: "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 }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
+ - 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: "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 }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
\ No newline at end of file
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 69f17a3875..02827e7f17 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -18,6 +18,10 @@ jobs:
ktlint:
name: Kotlin Linter
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:
- uses: actions/checkout@v2
- name: Run ktlint
@@ -87,6 +91,10 @@ jobs:
android-lint:
name: Android Linter
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:
- uses: actions/checkout@v2
- uses: actions/cache@v2
@@ -116,6 +124,10 @@ jobs:
fail-fast: false
matrix:
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:
- uses: actions/checkout@v2
- uses: actions/cache@v2
diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml
deleted file mode 100644
index 83ad067446..0000000000
--- a/.github/workflows/sanity_test.yml
+++ /dev/null
@@ -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}} [details]{{/if}}{{/with}}{{/each}}"
diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml
index a890082575..2323af0554 100644
--- a/.github/workflows/sync-from-external-sources.yml
+++ b/.github/workflows/sync-from-external-sources.yml
@@ -9,6 +9,7 @@ jobs:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
@@ -35,6 +36,7 @@ jobs:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
@@ -60,6 +62,7 @@ jobs:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
+ # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v2
- name: Run analytics import script
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 50195638de..d6e194916b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -15,6 +15,10 @@ jobs:
unit-tests:
name: Run Unit Tests
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:
- uses: actions/checkout@v2
- uses: actions/cache@v2
diff --git a/changelog.d/5005.feature b/changelog.d/5005.feature
new file mode 100644
index 0000000000..ce3b2ad1f9
--- /dev/null
+++ b/changelog.d/5005.feature
@@ -0,0 +1 @@
+Add possibility to save media from Gallery + reorder choices in message context menu
diff --git a/changelog.d/5325.feature b/changelog.d/5325.feature
new file mode 100644
index 0000000000..23754c790d
--- /dev/null
+++ b/changelog.d/5325.feature
@@ -0,0 +1 @@
+Adds forceLoginFallback feature flag and usages to FTUE login and registration
\ No newline at end of file
diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
index 573138bf5c..21af114c26 100644
--- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
+++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt
@@ -45,6 +45,8 @@ import kotlin.math.abs
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
+ protected val rootView: View
+ get() = views.rootContainer
protected val pager2: ViewPager2
get() = views.attachmentPager
protected val imageTransitionView: ImageView
@@ -298,10 +300,11 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private fun createSwipeToDismissHandler(): SwipeToDismissHandler =
SwipeToDismissHandler(
- swipeView = views.dismissContainer,
- shouldAnimateDismiss = { shouldAnimateDismiss() },
- onDismiss = { animateClose() },
- onSwipeViewMove = ::handleSwipeViewMove)
+ swipeView = views.dismissContainer,
+ shouldAnimateDismiss = { shouldAnimateDismiss() },
+ onDismiss = { animateClose() },
+ onSwipeViewMove = ::handleSwipeViewMove
+ )
private fun createSwipeDirectionDetector() =
SwipeDirectionDetector(this) { swipeDirection = it }
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
index 5cc4bd3bde..8702c8d966 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
@@ -53,7 +53,7 @@ class DebugFeaturesStateFactory @Inject constructor(
label = "FTUE Personalize profile",
key = DebugFeatureKeys.onboardingPersonalize,
factory = VectorFeatures::isOnboardingPersonalizeEnabled
- )
+ ),
))
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
index 808c379354..b54d776901 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt
@@ -43,9 +43,13 @@ class DebugPrivateSettingsFragment : VectorBaseFragment
viewModel.handle(DebugPrivateSettingsViewActions.SetDialPadVisibility(isChecked))
}
+ views.forceLoginFallback.setOnCheckedChangeListener { _, isChecked ->
+ viewModel.handle(DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled(isChecked))
+ }
}
override fun invalidate() = withState(viewModel) {
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
+ views.forceLoginFallback.isChecked = it.forceLoginFallback
}
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
index ecbb241387..1c76cf6fb2 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt
@@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
+ data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions()
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
index 624c46556a..038b1e6cc7 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt
@@ -45,15 +45,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
private fun observeVectorDataStore() {
vectorDataStore.forceDialPadDisplayFlow.setOnEach {
- copy(
- dialPadVisible = it
- )
+ copy(dialPadVisible = it)
+ }
+
+ vectorDataStore.forceLoginFallbackFlow.setOnEach {
+ copy(forceLoginFallback = it)
}
}
override fun handle(action: DebugPrivateSettingsViewActions) {
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)
}
}
+
+ private fun handleSetForceLoginFallbackEnabled(action: DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled) {
+ viewModelScope.launch {
+ vectorDataStore.setForceLoginFallbackFlow(action.force)
+ }
+ }
}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
index 0ad4b185ec..7fca29af8c 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt
@@ -19,5 +19,6 @@ package im.vector.app.features.debug.settings
import com.airbnb.mvrx.MavericksState
data class DebugPrivateSettingsViewState(
- val dialPadVisible: Boolean = false
+ val dialPadVisible: Boolean = false,
+ val forceLoginFallback: Boolean = false,
) : MavericksState
diff --git a/vector/src/debug/res/layout/fragment_debug_private_settings.xml b/vector/src/debug/res/layout/fragment_debug_private_settings.xml
index b4186e7bba..6760c68169 100644
--- a/vector/src/debug/res/layout/fragment_debug_private_settings.xml
+++ b/vector/src/debug/res/layout/fragment_debug_private_settings.xml
@@ -25,6 +25,12 @@
android:layout_height="wrap_content"
android:text="Force DialPad tab display" />
+
+
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 2cd7136ffc..33afcf1dfb 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -58,6 +58,7 @@ import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel
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.poll.create.CreatePollViewModel
import im.vector.app.features.qrcode.QrCodeScannerViewModel
@@ -594,4 +595,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(LocationSharingViewModel::class)
fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
+ fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 745cb0c731..5575d9b7f6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -343,24 +343,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
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)) {
// TODO copy images? html? see ClipBoard
add(EventSharedAction.Copy(messageContent!!.body))
@@ -382,12 +364,30 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.ViewEditHistory(informationData))
}
+ if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
+ add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
+ }
+
if (canShare(msgType)) {
add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!))
}
- if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
- add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
+ 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
+ ))
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/media/AttachmentInteractionListener.kt b/vector/src/main/java/im/vector/app/features/media/AttachmentInteractionListener.kt
new file mode 100644
index 0000000000..b0cb913596
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/AttachmentInteractionListener.kt
@@ -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)
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt
index f79fb03898..58d10d2f2d 100644
--- a/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt
+++ b/vector/src/main/java/im/vector/app/features/media/AttachmentOverlayView.kt
@@ -30,35 +30,33 @@ class AttachmentOverlayView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
- var onShareCallback: (() -> Unit)? = null
- var onBack: (() -> Unit)? = null
- var onPlayPause: ((play: Boolean) -> Unit)? = null
- var videoSeekTo: ((progress: Int) -> Unit)? = null
-
+ var interactionListener: AttachmentInteractionListener? = null
val views: MergeImageAttachmentOverlayBinding
- var isPlaying = false
-
- var suspendSeekBarUpdate = false
+ private var isPlaying = false
+ private var suspendSeekBarUpdate = false
init {
inflate(context, R.layout.merge_image_attachment_overlay, this)
views = MergeImageAttachmentOverlayBinding.bind(this)
setBackgroundColor(Color.TRANSPARENT)
views.overlayBackButton.setOnClickListener {
- onBack?.invoke()
+ interactionListener?.onDismiss()
}
views.overlayShareButton.setOnClickListener {
- onShareCallback?.invoke()
+ interactionListener?.onShare()
+ }
+ views.overlayDownloadButton.setOnClickListener {
+ interactionListener?.onDownload()
}
views.overlayPlayPauseButton.setOnClickListener {
- onPlayPause?.invoke(!isPlaying)
+ interactionListener?.onPlayPause(!isPlaying)
}
views.overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
- videoSeekTo?.invoke(progress)
+ interactionListener?.videoSeekTo(progress)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
index ca469bfbcb..4039ea112b 100644
--- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
@@ -49,14 +49,7 @@ abstract class BaseAttachmentProvider(
private val stringProvider: StringProvider
) : AttachmentSourceProvider {
- interface InteractionListener {
- fun onDismissTapped()
- fun onShareTapped()
- fun onPlayPause(play: Boolean)
- fun videoSeekTo(percent: Int)
- }
-
- var interactionListener: InteractionListener? = null
+ var interactionListener: AttachmentInteractionListener? = null
private var overlayView: AttachmentOverlayView? = null
@@ -68,18 +61,7 @@ abstract class BaseAttachmentProvider(
if (position == -1) return null
if (overlayView == null) {
overlayView = AttachmentOverlayView(context)
- overlayView?.onBack = {
- interactionListener?.onDismissTapped()
- }
- overlayView?.onShareCallback = {
- interactionListener?.onShareTapped()
- }
- overlayView?.onPlayPause = { play ->
- interactionListener?.onPlayPause(play)
- }
- overlayView?.videoSeekTo = { percent ->
- interactionListener?.videoSeekTo(percent)
- }
+ overlayView?.interactionListener = interactionListener
}
val timelineEvent = getTimelineEventAtPosition(position)
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerAction.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerAction.kt
new file mode 100644
index 0000000000..5af3cd193a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerAction.kt
@@ -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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
index 103511bad5..d8c2b83f9b 100644
--- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
@@ -17,6 +17,7 @@ package im.vector.app.features.media
import android.content.Context
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.View
@@ -30,16 +31,25 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.transition.Transition
+import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
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.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.features.themes.ActivityOtherThemes
import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.attachmentviewer.AttachmentCommands
import im.vector.lib.attachmentviewer.AttachmentViewerActivity
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@@ -47,7 +57,7 @@ import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
-class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
+class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInteractionListener {
@Parcelize
data class Args(
@@ -58,15 +68,28 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
@Inject
lateinit var sessionHolder: ActiveSessionHolder
+
@Inject
lateinit var dataSourceFactory: AttachmentProviderFactory
+
@Inject
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 isAnimatingOut = false
-
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?) {
super.onCreate(savedInstanceState)
@@ -128,6 +151,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+
+ observeViewEvents()
}
override fun onResume() {
@@ -140,12 +165,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
Timber.i("onPause Activity ${javaClass.simpleName}")
}
- private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
-
- override fun shouldAnimateDismiss(): Boolean {
- return currentPosition != initialIndex
- }
-
override fun onBackPressed() {
if (currentPosition == initialIndex) {
// show back the transition view
@@ -156,6 +175,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
super.onBackPressed()
}
+ override fun shouldAnimateDismiss(): Boolean {
+ return currentPosition != initialIndex
+ }
+
override fun animateClose() {
if (currentPosition == initialIndex) {
// show back the transition view
@@ -166,9 +189,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
ActivityCompat.finishAfterTransition(this)
}
- // ==========================================================================================
- // PRIVATE METHODS
- // ==========================================================================================
+ private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
/**
* 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 {
- const val EXTRA_ARGS = "EXTRA_ARGS"
- const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
- const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
+ private const val EXTRA_ARGS = "EXTRA_ARGS"
+ private const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
+ private const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
fun newIntent(context: Context,
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())
- )
- }
- }
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewEvents.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewEvents.kt
new file mode 100644
index 0000000000..e46ee02155
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewEvents.kt
@@ -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()
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewModel.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewModel.kt
new file mode 100644
index 0000000000..807c69caff
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerViewModel.kt
@@ -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(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: VectorDummyViewState): VectorAttachmentViewerViewModel
+ }
+
+ companion object : MavericksViewModelFactory 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)) }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt
new file mode 100644
index 0000000000..b0401ccd30
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/media/domain/usecase/DownloadMediaUseCase.kt
@@ -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 = withContext(session.coroutineDispatchers.io) {
+ runCatching {
+ saveMedia(
+ context = appContext,
+ file = input,
+ title = input.name,
+ mediaMimeType = getMimeTypeFromUri(appContext, input.toUri()),
+ notificationUtils = notificationUtils
+ )
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index ca3c3644bd..63f1875235 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -46,6 +46,7 @@ import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
+import im.vector.app.features.settings.VectorDataStore
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
@@ -78,7 +79,8 @@ class OnboardingViewModel @AssistedInject constructor(
private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService,
private val vectorFeatures: VectorFeatures,
- private val analyticsTracker: AnalyticsTracker
+ private val analyticsTracker: AnalyticsTracker,
+ private val vectorDataStore: VectorDataStore,
) : VectorViewModel(initialState) {
@AssistedFactory
@@ -90,6 +92,7 @@ class OnboardingViewModel @AssistedInject constructor(
init {
getKnownCustomHomeServersUrls()
+ observeDataStore()
}
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
private var lastAction: OnboardingAction? = null
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
index 7bad2682a9..39c5094d30 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
@@ -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
@PersistState
val loginModeSupportedTypes: List = emptyList(),
- val knownCustomHomeServersUrls: List = emptyList()
+ val knownCustomHomeServersUrls: List = emptyList(),
+ val isForceLoginFallbackEnabled: Boolean = false,
) : MavericksState {
fun isLoading(): Boolean {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
index 1e792df427..0093cb20ea 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
@@ -75,6 +75,8 @@ class FtueAuthVariant(
private val popEnterAnim = R.anim.no_anim
private val popExitAnim = R.anim.exit_fade_out
+ private var isForceLoginFallbackEnabled = false
+
private val topFragment: Fragment?
get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id)
@@ -109,10 +111,6 @@ class FtueAuthVariant(
}
}
- override fun setIsLoading(isLoading: Boolean) {
- // do nothing
- }
-
private fun addFirstFragment() {
val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) {
true -> FtueAuthSplashCarouselFragment::class.java
@@ -121,11 +119,25 @@ class FtueAuthVariant(
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) {
when (viewEvents) {
is OnboardingViewEvents.RegistrationFlowResult -> {
- // Check that all flows are supported by the application
- if (viewEvents.flowResult.missingStages.any { !it.isSupported() }) {
+ if (registrationShouldFallback(viewEvents)) {
// Display a popup to propose use web fallback
onRegistrationStageNotSupported()
} else {
@@ -136,11 +148,7 @@ class FtueAuthVariant(
// First ask for login and password
// 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
- activity.addFragmentToBackstack(views.loginFragmentContainer,
- FtueAuthLoginFragment::class.java,
- tag = FRAGMENT_REGISTRATION_STAGE_TAG,
- option = commonOption
- )
+ openAuthLoginFragmentWithTag(FRAGMENT_REGISTRATION_STAGE_TAG)
}
}
}
@@ -228,13 +236,23 @@ class FtueAuthVariant(
}.exhaustive
}
- private fun updateWithState(viewState: OnboardingViewState) {
- views.loginLoading.isVisible = 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()
- }
+ private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) =
+ isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow()
+
+ private fun OnboardingViewEvents.RegistrationFlowResult.containsUnsupportedRegistrationFlow() =
+ flowResult.missingStages.any { !it.isSupported() }
+
+ 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) {
@@ -264,29 +282,58 @@ class FtueAuthVariant(
// state.signMode could not be ready yet. So use value from the ViewEvent
when (OnboardingViewEvents.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method")
- SignMode.SignUp -> {
- // This is managed by the OnboardingViewEvents
- }
- 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)
+ SignMode.SignUp -> Unit // This case is processed in handleOnboardingViewEvents
+ SignMode.SignIn -> handleSignInSelected(state)
+ SignMode.SignInWithMatrixId -> handleSignInWithMatrixId(state)
}.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) {
+ 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
*/
@@ -296,32 +343,6 @@ class FtueAuthVariant(
?.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) {
- 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) {
// Complete all mandatory stages first
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt
index 6a5ef0ac99..a7981a8b2a 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt
@@ -59,4 +59,16 @@ class VectorDataStore @Inject constructor(
settings[forceDialPadDisplay] = force
}
}
+
+ private val forceLoginFallback = booleanPreferencesKey("force_login_fallback")
+
+ val forceLoginFallbackFlow: Flow = context.dataStore.data.map { preferences ->
+ preferences[forceLoginFallback].orFalse()
+ }
+
+ suspend fun setForceLoginFallbackFlow(force: Boolean) {
+ context.dataStore.edit { settings ->
+ settings[forceLoginFallback] = force
+ }
+ }
}
diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
index d8e2142f87..1a5c6d8bf4 100644
--- a/vector/src/main/res/layout/merge_image_attachment_overlay.xml
+++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
@@ -67,6 +67,23 @@
app:layout_constraintTop_toBottomOf="@id/overlayCounterText"
tools:text="Bill 29 Jun at 19:42" />
+
+
()
+ 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()
+ 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)
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
new file mode 100644
index 0000000000..652d3f93fd
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFile.kt
@@ -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()
+
+ 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
+ }
+}