diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 77dffb43d2..34d7b40a88 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ### Pull Request Checklist - + - [ ] Changes has been tested on an Android device or Android emulator with API 21 - [ ] UI change has been tested on both light and dark themes diff --git a/AUTHORS.md b/AUTHORS.md index 3d9dffbef5..a85beb2d6f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,4 +1,4 @@ -A full developer contributors list can be found [here](https://github.com/vector-im/riotX-android/graphs/contributors). +A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors). # Core team: diff --git a/CHANGES.md b/CHANGES.md index 07b50ef632..4a82693feb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ Features ✨: - Improvements 🙌: + - You can now join room through permalink and within room directory search - Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774) Bugfix 🐛: @@ -15,6 +16,8 @@ Bugfix 🐛: - Fix bad color for settings icon on Android < 24 (#1786) - Change user or room avatar: when selecting Gallery, I'm not proposed to crop the selected image (#1590) - Loudspeaker is always used (#1685) + - Fix uploads still don't work with room v6 (#1879) + - Can't handle ongoing call events in background (#1992) Translations 🗣: - diff --git a/README.md b/README.md index 1ec425793f..457b18d775 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop) [![Weblate](https://translate.riot.im/widgets/element-android/-/svg-badge.svg)](https://translate.riot.im/engage/element-android/?utm_source=widget) [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=vector.android.riotx) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=bugs)](https://sonarcloud.io/dashboard?id=vector.android.riotx) +[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=alert_status)](https://sonarcloud.io/dashboard?id=im.vector.app.android) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=im.vector.app.android) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=bugs)](https://sonarcloud.io/dashboard?id=im.vector.app.android) # Element Android @@ -27,6 +27,6 @@ The team will work to add them on a regular basis. ## Contributing -Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! +Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org). diff --git a/build.gradle b/build.gradle index fdaef6a2f7..061dd73a40 100644 --- a/build.gradle +++ b/build.gradle @@ -81,15 +81,15 @@ apply plugin: 'org.sonarqube' sonarqube { properties { - property "sonar.projectName", "RiotX-Android" - property "sonar.projectKey", "vector.android.riotx" + property "sonar.projectName", "Element-Android" + property "sonar.projectKey", "im.vector.app.android" property "sonar.host.url", "https://sonarcloud.io" property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName property "sonar.sourceEncoding", "UTF-8" - property "sonar.links.homepage", "https://github.com/vector-im/riotX-android/" + property "sonar.links.homepage", "https://github.com/vector-im/element-android/" property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/riotx-android" - property "sonar.links.scm", "https://github.com/vector-im/riotX-android/" - property "sonar.links.issue", "https://github.com/vector-im/riotX-android/issues" + property "sonar.links.scm", "https://github.com/vector-im/element-android/" + property "sonar.links.issue", "https://github.com/vector-im/element-android/issues" property "sonar.organization", "new_vector_ltd_organization" property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid" } @@ -100,11 +100,18 @@ project(":vector") { properties { property "sonar.sources", project(":vector").android.sourceSets.main.java.srcDirs // exclude source code from analyses separated by a colon (:) - // property "sonar.exclusions", "**/*.*" + // Exclude Java source + property "sonar.exclusions", "**/BugReporterMultipartBody.java" } } } +project(":diff-match-patch") { + sonarqube { + skipProject = true + } +} + //project(":matrix-sdk-android") { // sonarqube { // properties { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index d70b0bbe84..90bdf02243 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -131,8 +131,6 @@ dependencies { // Network implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" - implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version" - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.8.1")) implementation 'com.squareup.okhttp3:okhttp' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt index 2962f9fac3..382ab54248 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -34,4 +34,6 @@ interface CallSignalingService { fun removeCallListener(listener: CallsListener) fun getCallWithId(callId: String) : MxCall? + + fun isThereAnyActiveCall(): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt index 85632d6e83..1da65b3002 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt @@ -25,7 +25,12 @@ import android.net.Uri */ sealed class PermalinkData { - data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData() + data class RoomLink( + val roomIdOrAlias: String, + val isRoomAlias: Boolean, + val eventId: String?, + val viaParameters: List + ) : PermalinkData() data class UserLink(val userId: String) : PermalinkData() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt index dd6847f1e3..59e289ffd7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.permalinks import android.net.Uri +import android.net.UrlQuerySanitizer import org.matrix.android.sdk.api.MatrixPatterns /** @@ -40,14 +41,13 @@ object PermalinkParser { if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) { return PermalinkData.FallbackLink(uri) } - val fragment = uri.fragment if (fragment.isNullOrEmpty()) { return PermalinkData.FallbackLink(uri) } - val indexOfQuery = fragment.indexOf("?") val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment + val viaQueryParameters = fragment.getViaParameters() // we are limiting to 2 params val params = safeFragment @@ -65,17 +65,29 @@ object PermalinkParser { PermalinkData.RoomLink( roomIdOrAlias = identifier, isRoomAlias = false, - eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, + viaParameters = viaQueryParameters ) } MatrixPatterns.isRoomAlias(identifier) -> { PermalinkData.RoomLink( roomIdOrAlias = identifier, isRoomAlias = true, - eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, + viaParameters = viaQueryParameters ) } else -> PermalinkData.FallbackLink(uri) } } + + private fun String.getViaParameters(): List { + return UrlQuerySanitizer(this) + .parameterList + .filter { + it.mParameter == "via" + }.map { + it.mValue + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt index 368611dd7d..89a0ce597a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt @@ -19,13 +19,12 @@ package org.matrix.android.sdk.internal.network import com.squareup.moshi.Moshi import dagger.Lazy -import org.matrix.android.sdk.internal.util.ensureTrailingSlash import okhttp3.Call import okhttp3.OkHttpClient import okhttp3.Request +import org.matrix.android.sdk.internal.util.ensureTrailingSlash import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory -import retrofit2.converter.scalars.ScalarsConverterFactory import javax.inject.Inject internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { @@ -50,7 +49,6 @@ internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { return okHttpClient.get().newCall(request) } }) - .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(UnitConverterFactory) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt new file mode 100644 index 0000000000..d4ceca2006 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.internal.network.parsing + +import androidx.annotation.Nullable +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter + +import com.squareup.moshi.Moshi +import java.io.IOException +import java.lang.reflect.Type +import java.math.BigDecimal + +/** + * This is used to check if NUMBER in json is integer or double, so we can preserve typing when serializing/deserializing in a row. + */ +interface CheckNumberType { + + companion object { + val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory { + @Nullable + override fun create(type: Type, annotations: Set?, moshi: Moshi): JsonAdapter<*>? { + if (type !== Any::class.java) { + return null + } + val delegate: JsonAdapter = moshi.nextAdapter(this, Any::class.java, emptySet()) + return object : JsonAdapter() { + @Nullable + @Throws(IOException::class) + override fun fromJson(reader: JsonReader): Any? { + return if (reader.peek() !== JsonReader.Token.NUMBER) { + delegate.fromJson(reader) + } else { + val numberAsString = reader.nextString() + val decimal = BigDecimal(numberAsString) + if (decimal.scale() <= 0) { + decimal.intValueExact() + } else { + decimal.toDouble() + } + } + } + + override fun toJson(writer: JsonWriter, value: Any?) { + delegate.toJson(writer, value) + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/ActiveCallHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/ActiveCallHandler.kt new file mode 100644 index 0000000000..40f5a56c33 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/ActiveCallHandler.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.internal.session.call + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class ActiveCallHandler @Inject constructor() { + + private val activeCallListLiveData: MutableLiveData> by lazy { + MutableLiveData>(mutableListOf()) + } + + fun addCall(call: MxCall) { + activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { add(call) }) + } + + fun removeCall(callId: String) { + activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { removeAll { it.callId == callId } }) + } + + fun getCallWithId(callId: String): MxCall? { + return activeCallListLiveData.value?.find { it.callId == callId } + } + + fun getActiveCallsLiveData(): LiveData> = activeCallListLiveData +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt index 0b097cf64f..d9bc66eddf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -49,6 +49,7 @@ import javax.inject.Inject internal class DefaultCallSignalingService @Inject constructor( @UserId private val userId: String, + private val activeCallHandler: ActiveCallHandler, private val localEchoEventFactory: LocalEchoEventFactory, private val roomEventSender: RoomEventSender, private val taskExecutor: TaskExecutor, @@ -57,8 +58,6 @@ internal class DefaultCallSignalingService @Inject constructor( private val callListeners = mutableSetOf() - private val activeCalls = mutableListOf() - private val cachedTurnServerResponse = object { // Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it. private val MIN_TTL = 60 @@ -97,7 +96,7 @@ internal class DefaultCallSignalingService @Inject constructor( } override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { - return MxCallImpl( + val call = MxCallImpl( callId = UUID.randomUUID().toString(), isOutgoing = true, roomId = roomId, @@ -106,8 +105,9 @@ internal class DefaultCallSignalingService @Inject constructor( isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, roomEventSender = roomEventSender - ).also { - activeCalls.add(it) + ) + activeCallHandler.addCall(call).also { + return call } } @@ -120,8 +120,12 @@ internal class DefaultCallSignalingService @Inject constructor( } override fun getCallWithId(callId: String): MxCall? { - Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}") - return activeCalls.find { it.callId == callId } + Timber.v("## VOIP getCallWithId $callId all calls ${activeCallHandler.getActiveCallsLiveData().value?.map { it.callId }}") + return activeCallHandler.getCallWithId(callId) + } + + override fun isThereAnyActiveCall(): Boolean { + return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true } internal fun onCallEvent(event: Event) { @@ -152,6 +156,7 @@ internal class DefaultCallSignalingService @Inject constructor( // Always ignore local echos of invite return } + event.getClearContent().toModel()?.let { content -> val incomingCall = MxCallImpl( callId = content.callId ?: return@let, @@ -163,7 +168,7 @@ internal class DefaultCallSignalingService @Inject constructor( localEchoEventFactory = localEchoEventFactory, roomEventSender = roomEventSender ) - activeCalls.add(incomingCall) + activeCallHandler.addCall(incomingCall) onCallInvite(incomingCall, content) } } @@ -185,8 +190,8 @@ internal class DefaultCallSignalingService @Inject constructor( return } + activeCallHandler.removeCall(content.callId) onCallHangup(content) - activeCalls.removeAll { it.callId == content.callId } } } EventType.CALL_CANDIDATES -> { @@ -195,7 +200,7 @@ internal class DefaultCallSignalingService @Inject constructor( return } event.getClearContent().toModel()?.let { content -> - activeCalls.firstOrNull { it.callId == content.callId }?.let { + activeCallHandler.getCallWithId(content.callId)?.let { onCallIceCandidate(it, content) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 25dcc69fa8..d8df86be8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -130,21 +130,6 @@ internal interface RoomAPI { @Body content: Content? ): Call - /** - * Send an event to a room. - * - * @param txId the transaction Id - * @param roomId the room id - * @param eventType the event type - * @param content the event content as string - */ - @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") - fun send(@Path("txId") txId: String, - @Path("roomId") roomId: String, - @Path("eventType") eventType: String, - @Body content: String? - ): Call - /** * Get the context surrounding an event. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 2199193de0..111551d66d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -208,7 +208,7 @@ internal class DefaultRelationService @AssistedInject constructor( } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt index d23835e838..f878df52b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt @@ -120,7 +120,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) } - val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) + val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, event = encryptedEvent) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } else { val sendState = when (error) { @@ -129,8 +129,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) } localEchoRepository.updateSendState(localEvent.eventId, sendState) // always return success, or the chain will be stuck for ever! - val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage - ?: "Error") + val nextWorkerParams = SendEventWorker.Params( + sessionId = params.sessionId, + event = localEvent, + lastFailureMessage = error?.localizedMessage ?: "Error" + ) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index 73791e8412..ead2dc9377 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -105,7 +105,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo } private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt index 65c692f42e..e46adeb9c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt @@ -65,7 +65,7 @@ internal class RoomEventSender @Inject constructor( } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 2868ce29c1..5da14f0a41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -21,16 +21,16 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject @@ -47,24 +47,11 @@ internal class SendEventWorker(context: Context, @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - // TODO remove after some time, it's used for compat + override val lastFailureMessage: String? = null, val event: Event? = null, - val eventId: String? = null, - val roomId: String? = null, - val type: String? = null, - val contentStr: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams { - - constructor(sessionId: String, event: Event, lastFailureMessage: String? = null) : this( - sessionId = sessionId, - eventId = event.eventId, - roomId = event.roomId, - type = event.type, - contentStr = ContentMapper.map(event.content), - lastFailureMessage = lastFailureMessage - ) - } + // Keep for compat at the moment, will be removed later + val eventId: String? = null + ) : SessionWorkerParams @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var roomAPI: RoomAPI @@ -77,27 +64,31 @@ internal class SendEventWorker(context: Context, val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) - if (params.eventId == null || params.roomId == null || params.type == null) { - // compat with old params, make it fail if any - if (params.event?.eventId != null) { - localEchoRepository.updateSendState(params.event.eventId, SendState.UNDELIVERED) + + val event = params.event + if (event?.eventId == null || event.roomId == null) { + // Old way of sending + if (params.eventId != null) { + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) } return Result.success() + .also { Timber.e("Work cancelled due to bad input data") } } + if (params.lastFailureMessage != null) { - localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) // Transmit the error return Result.success(inputData) .also { Timber.e("Work cancelled due to input error from parent") } } return try { - sendEvent(params.eventId, params.roomId, params.type, params.contentStr) + sendEvent(event.eventId, event.roomId, event.type, event.content) Result.success() } catch (exception: Throwable) { // It does start from 0, we want it to stop if it fails the third time val currentAttemptCount = runAttemptCount + 1 if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) { - localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) return Result.success() } else { Result.retry() @@ -105,10 +96,10 @@ internal class SendEventWorker(context: Context, } } - private suspend fun sendEvent(eventId: String, roomId: String, type: String, contentStr: String?) { + private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) { localEchoRepository.updateSendState(eventId, SendState.SENDING) executeRequest(eventBus) { - apiCall = roomAPI.send(eventId, roomId, type, contentStr) + apiCall = roomAPI.send(eventId, roomId, type, content) } localEchoRepository.updateSendState(eventId, SendState.SENT) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index 1a2d6b1fd3..9fd9c313db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.job import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import com.squareup.moshi.JsonEncodingException import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError @@ -30,11 +31,14 @@ import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.createUIHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.internal.session.call.ActiveCallHandler import timber.log.Timber import java.net.SocketTimeoutException import java.util.Timer @@ -48,8 +52,9 @@ private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private val typingUsersTracker: DefaultTypingUsersTracker, private val networkConnectivityChecker: NetworkConnectivityChecker, - private val backgroundDetectionObserver: BackgroundDetectionObserver) - : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { + private val backgroundDetectionObserver: BackgroundDetectionObserver, + private val activeCallHandler: ActiveCallHandler +) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle private var liveState = MutableLiveData(state) @@ -62,6 +67,12 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private var isTokenValid = true private var retryNoNetworkTask: TimerTask? = null + private val activeCallListObserver = Observer> { activeCalls -> + if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) { + pause() + } + } + init { updateStateTo(SyncState.Idle) } @@ -115,9 +126,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, override fun run() { Timber.v("Start syncing...") + isStarted = true networkConnectivityChecker.register(this) backgroundDetectionObserver.register(this) + registerActiveCallsObserver() while (state != SyncState.Killing) { Timber.v("Entering loop, state: $state") if (!isStarted) { @@ -163,6 +176,19 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, updateStateTo(SyncState.Killed) backgroundDetectionObserver.unregister(this) networkConnectivityChecker.unregister(this) + unregisterActiveCallsObserver() + } + + private fun registerActiveCallsObserver() { + syncScope.launch(Dispatchers.Main) { + activeCallHandler.getActiveCallsLiveData().observeForever(activeCallListObserver) + } + } + + private fun unregisterActiveCallsObserver() { + syncScope.launch(Dispatchers.Main) { + activeCallHandler.getActiveCallsLiveData().removeObserver(activeCallListObserver) + } } private suspend fun doSync(params: SyncTask.Params) { @@ -215,6 +241,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } override fun onMoveToBackground() { - pause() + if (activeCallHandler.getActiveCallsLiveData().value.isNullOrEmpty()) { + pause() + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt index 2b7cba0b0c..b162566403 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt @@ -19,13 +19,23 @@ package org.matrix.android.sdk.internal.worker import androidx.work.Data import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.network.parsing.CheckNumberType -object WorkerParamsFactory { +internal object WorkerParamsFactory { + + val moshi by lazy { + // We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row + // and we lost typing information doing so. + // We don't want this check to be done on all adapters, so we just add it here. + MoshiProvider.providesMoshi() + .newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + } const val KEY = "WORKER_PARAMS_JSON" inline fun toData(params: T): Data { - val moshi = MoshiProvider.providesMoshi() val adapter = moshi.adapter(T::class.java) val json = adapter.toJson(params) return Data.Builder().putString(KEY, json).build() @@ -36,7 +46,6 @@ object WorkerParamsFactory { return if (json == null) { null } else { - val moshi = MoshiProvider.providesMoshi() val adapter = moshi.adapter(T::class.java) adapter.fromJson(json) } diff --git a/vector/build.gradle b/vector/build.gradle index ef2d6c3d9e..e55ad31ef3 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -122,6 +122,9 @@ android { // Other branches (master, features, etc.) will have version code based on application version. versionCode project.getVersionCode() + // Required for sonar analysis + versionName "${versionMajor}.${versionMinor}.${versionPatch}-sonar" + buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\"" resValue "string", "git_revision", "\"${gitRevision()}\"" diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index 855333d96b..5590e19c10 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -68,7 +68,7 @@ class DebugMenuActivity : VectorBaseActivity() { } private fun renderQrCode(text: String) { - debug_qr_code.setData(text, true) + debug_qr_code.setData(text) } @OnClick(R.id.debug_test_text_view_link) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/QrCodeImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/QrCodeImageView.kt index 11e319ab78..f4b371bc52 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/QrCodeImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/QrCodeImageView.kt @@ -18,28 +18,23 @@ package im.vector.app.core.ui.views import android.content.Context import android.graphics.Color -import android.graphics.drawable.AnimationDrawable -import android.graphics.drawable.BitmapDrawable import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import im.vector.app.core.qrcode.toBitMatrix import im.vector.app.core.qrcode.toBitmap -import kotlin.random.Random class QrCodeImageView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatImageView(context, attrs, defStyleAttr) { private var data: String? = null - private var animate = false init { setBackgroundColor(Color.WHITE) } - fun setData(data: String, animate: Boolean) { + fun setData(data: String) { this.data = data - this.animate = animate render() } @@ -53,47 +48,8 @@ class QrCodeImageView @JvmOverloads constructor( data ?.takeIf { height > 0 } ?.let { - if (animate) { - // NOT SUPPORTED YET val anim = createAnimation(it) - // NOT SUPPORTED YET setImageDrawable(anim) - // NOT SUPPORTED YET anim.start() - // NOT SUPPORTED YET setImageDrawable(BitmapDrawable(resources, it.toBitMatrix(height).toBitmap())) - val bitmap = it.toBitMatrix(height).toBitmap() - post { setImageBitmap(bitmap) } - } else { - val bitmap = it.toBitMatrix(height).toBitmap() - post { setImageBitmap(bitmap) } - } + val bitmap = it.toBitMatrix(height).toBitmap() + post { setImageBitmap(bitmap) } } } - - private fun createAnimation(data: String): AnimationDrawable { - val finalQr = data.toBitMatrix(height) - - val list = mutableListOf(finalQr) - - val random = Random(System.currentTimeMillis()) - val repeatTime = 8 - repeat(repeatTime) { index -> - val alteredQr = finalQr.clone() - for (x in 0 until alteredQr.width) { - for (y in 0 until alteredQr.height) { - if (random.nextInt(repeatTime - index) == 0) { - // Pb is that it does not toggle a whole black square, but only a pixel - alteredQr.unset(x, y) - } - } - } - list.add(alteredQr) - } - - val animDrawable = AnimationDrawable() - - list.asReversed() - .forEach { - animDrawable.addFrame(BitmapDrawable(resources, it.toBitmap()), 150) - } - - return animDrawable - } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt index 953b20cb0c..0406a63bc6 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -53,7 +53,6 @@ class VerificationChooseMethodController @Inject constructor( bottomSheetVerificationQrCodeItem { id("qr") data(state.qrCodeText) - animate(false) } dividerItem { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt index 8d78c1c9d6..41c92fa76f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt @@ -32,12 +32,9 @@ abstract class BottomSheetVerificationQrCodeItem : VectorEpoxyModel + handlePermalink(permalinkData, context, navigationInterceptor, buildTask) + } + .onErrorReturnItem(false) + } + + private fun handlePermalink( + permalinkData: PermalinkData, + context: Context, + navigationInterceptor: NavigationInterceptor?, + buildTask: Boolean + ): Single { + return when (permalinkData) { is PermalinkData.RoomLink -> { permalinkData.getRoomId() .observeOn(AndroidSchedulers.mainThread()) .map { val roomId = it.getOrNull() if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) { - openRoom(context, roomId, permalinkData.eventId, buildTask) + openRoom( + context = context, + roomId = roomId, + permalinkData = permalinkData, + buildTask = buildTask + ) } true } @@ -82,22 +110,55 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti private fun PermalinkData.RoomLink.getRoomId(): Single> { val session = activeSessionHolder.getSafeActiveSession() return if (isRoomAlias && session != null) { - // At the moment we are not fetching on the server as we don't handle not join room - session.rx().getRoomIdByAlias(roomIdOrAlias, false).subscribeOn(Schedulers.io()) + session.rx().getRoomIdByAlias(roomIdOrAlias, true).subscribeOn(Schedulers.io()) } else { Single.just(Optional.from(roomIdOrAlias)) } } + private fun PermalinkData.RoomLink.getRoomAliasOrNull(): String? { + return if (isRoomAlias) { + roomIdOrAlias + } else { + null + } + } + /** * Open room either joined, or not */ - private fun openRoom(context: Context, roomId: String?, eventId: String?, buildTask: Boolean) { + private fun openRoom( + context: Context, + roomId: String?, + permalinkData: PermalinkData.RoomLink, + buildTask: Boolean + ) { val session = activeSessionHolder.getSafeActiveSession() ?: return - return if (roomId != null && session.getRoom(roomId) != null) { - navigator.openRoom(context, roomId, eventId, buildTask) - } else { - navigator.openNotJoinedRoom(context, roomId, eventId, buildTask) + if (roomId == null) { + context.toast(R.string.room_error_not_found) + return + } + val roomSummary = session.getRoomSummary(roomId) + val membership = roomSummary?.membership + val eventId = permalinkData.eventId + val roomAlias = permalinkData.getRoomAliasOrNull() + return when { + membership == Membership.BAN -> context.toast(R.string.error_opening_banned_room) + membership?.isActive().orFalse() -> { + navigator.openRoom(context, roomId, eventId, buildTask) + } + else -> { + val roomPreviewData = RoomPreviewData( + roomId = roomId, + eventId = eventId, + roomAlias = roomAlias ?: roomSummary?.canonicalAlias, + roomName = roomSummary?.displayName, + avatarUrl = roomSummary?.avatarUrl, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt index c60336c1af..b71b494948 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt @@ -28,8 +28,10 @@ import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom +import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -42,8 +44,10 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri override fun buildModels(viewState: PublicRoomsViewState) { val publicRooms = viewState.publicRooms - if (publicRooms.isEmpty() - && viewState.asyncPublicRoomsRequest is Success) { + val unknownRoomItem = viewState.buildUnknownRoomIfNeeded() + + val noResult = publicRooms.isEmpty() && viewState.asyncPublicRoomsRequest is Success && unknownRoomItem == null + if (noResult) { // No result noResultItem { id("noResult") @@ -54,6 +58,8 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri buildPublicRoom(it, viewState) } + unknownRoomItem?.addTo(this) + if ((viewState.hasMore && viewState.asyncPublicRoomsRequest is Success) || viewState.asyncPublicRoomsRequest is Incomplete) { loadingItem { @@ -109,7 +115,29 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri } } + private fun PublicRoomsViewState.buildUnknownRoomIfNeeded(): UnknownRoomItem? { + val roomIdOrAlias = currentFilter.trim() + val isAlias = MatrixPatterns.isRoomAlias(roomIdOrAlias) && !publicRooms.any { it.canonicalAlias == roomIdOrAlias } + val isRoomId = !isAlias && MatrixPatterns.isRoomId(roomIdOrAlias) && !publicRooms.any { it.roomId == roomIdOrAlias } + val roomItem = when { + isAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, roomIdOrAlias) + isRoomId -> MatrixItem.RoomItem(roomIdOrAlias) + else -> null + } + return roomItem?.let { + UnknownRoomItem_().apply { + id(roomIdOrAlias) + matrixItem(it) + avatarRenderer(this@PublicRoomsController.avatarRenderer) + globalListener { + callback?.onUnknownRoomClicked(roomIdOrAlias) + } + } + } + } + interface Callback { + fun onUnknownRoomClicked(roomIdOrAlias: String) fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) fun onPublicRoomJoin(publicRoom: PublicRoom) fun loadMore() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index 57006af6f1..b180018480 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -29,9 +29,13 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.platform.VectorBaseFragment -import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom +import im.vector.app.core.utils.toast +import im.vector.app.features.permalink.NavigationInterceptor +import im.vector.app.features.permalink.PermalinkHandler import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.fragment_public_rooms.* +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -41,7 +45,9 @@ import javax.inject.Inject * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect */ class PublicRoomsFragment @Inject constructor( - private val publicRoomsController: PublicRoomsController + private val publicRoomsController: PublicRoomsController, + private val permalinkHandler: PermalinkHandler, + private val session: Session ) : VectorBaseFragment(), PublicRoomsController.Callback { private val viewModel: RoomDirectoryViewModel by activityViewModel() @@ -112,6 +118,23 @@ class PublicRoomsFragment @Inject constructor( publicRoomsController.callback = this } + override fun onUnknownRoomClicked(roomIdOrAlias: String) { + val permalink = session.permalinkService().createPermalink(roomIdOrAlias) + permalinkHandler + .launch(requireContext(), permalink, object : NavigationInterceptor { + override fun navToRoom(roomId: String?, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + .subscribe { isSuccessful -> + if (!isSuccessful) { + requireContext().toast(R.string.room_error_not_found) + } + } + .disposeOnDestroyView() + } + override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) { Timber.v("PublicRoomClicked: $publicRoom") withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/UnknownRoomItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/UnknownRoomItem.kt new file mode 100644 index 0000000000..084f518405 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/UnknownRoomItem.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 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.roomdirectory + +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_unknown_room) +abstract class UnknownRoomItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + lateinit var matrixItem: MatrixItem + + @EpoxyAttribute + var globalListener: (() -> Unit)? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.rootView.setOnClickListener { globalListener?.invoke() } + avatarRenderer.render(matrixItem, holder.avatarView) + holder.nameView.text = matrixItem.displayName + } + + class Holder : VectorEpoxyHolder() { + val rootView by bind(R.id.itemUnknownRoomLayout) + val avatarView by bind(R.id.itemUnknownRoomAvatar) + val nameView by bind(R.id.itemUnknownRoomName) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 0a25d93de2..4876bd7f98 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -24,20 +24,23 @@ import im.vector.app.R import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity +import kotlinx.android.parcel.Parcelize import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import org.matrix.android.sdk.api.util.MatrixItem -import kotlinx.android.parcel.Parcelize +import timber.log.Timber @Parcelize data class RoomPreviewData( val roomId: String, - val roomName: String?, - val roomAlias: String?, - val topic: String?, - val worldReadable: Boolean, - val avatarUrl: String?, - val homeServer: String? + val eventId: String? = null, + val roomName: String? = null, + val roomAlias: String? = null, + val topic: String? = null, + val worldReadable: Boolean = false, + val avatarUrl: String? = null, + val homeServers: List = emptyList(), + val buildTask: Boolean = false ) : Parcelable { val matrixItem: MatrixItem get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) @@ -48,19 +51,24 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { private const val ARG = "ARG" - fun getIntent(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData): Intent { + fun newIntent(context: Context, roomPreviewData: RoomPreviewData): Intent { return Intent(context, RoomPreviewActivity::class.java).apply { - putExtra(ARG, RoomPreviewData( - roomId = publicRoom.roomId, - roomName = publicRoom.name, - roomAlias = publicRoom.getPrimaryAlias(), - topic = publicRoom.topic, - worldReadable = publicRoom.worldReadable, - avatarUrl = publicRoom.avatarUrl, - homeServer = roomDirectoryData.homeServer - )) + putExtra(ARG, roomPreviewData) } } + + fun newIntent(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData): Intent { + val roomPreviewData = RoomPreviewData( + roomId = publicRoom.roomId, + roomName = publicRoom.name, + roomAlias = publicRoom.getPrimaryAlias(), + topic = publicRoom.topic, + worldReadable = publicRoom.worldReadable, + avatarUrl = publicRoom.avatarUrl, + homeServers = listOfNotNull(roomDirectoryData.homeServer) + ) + return newIntent(context, roomPreviewData) + } } override fun getLayoutRes() = R.layout.activity_simple @@ -76,6 +84,7 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { if (args?.worldReadable == true) { // TODO Room preview: Note: M does not recommend to use /events anymore, so for now we just display the room preview // TODO the same way if it was not world readable + Timber.d("just display the room preview the same way if it was not world readable") addFragment(R.id.simpleFragmentContainer, RoomPreviewNoPreviewFragment::class.java, args) } else { addFragment(R.id.simpleFragmentContainer, RoomPreviewNoPreviewFragment::class.java, args) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 5d13d3d9fe..108c3bacf1 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -48,13 +48,15 @@ class RoomPreviewNoPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(roomPreviewNoPreviewToolbar) + val titleText = roomPreviewData.roomName ?: roomPreviewData.roomAlias ?: roomPreviewData.roomId + // Toolbar avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar) - roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName ?: roomPreviewData.roomAlias + roomPreviewNoPreviewToolbarTitle.text = titleText // Screen avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar) - roomPreviewNoPreviewName.text = roomPreviewData.roomName ?: roomPreviewData.roomAlias + roomPreviewNoPreviewName.text = titleText roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic) if (roomPreviewData.worldReadable) { @@ -98,7 +100,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor( // Quit this screen requireActivity().finish() // Open room - navigator.openRoom(requireActivity(), roomPreviewData.roomId) + navigator.openRoom(requireActivity(), roomPreviewData.roomId, roomPreviewData.eventId, roomPreviewData.buildTask) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 04b7c33fb2..900ba537b5 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -106,10 +106,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini Timber.w("Try to join an already joining room. Should not happen") return@withState } - val viaServers = state.homeServer?.let { - listOf(it) - } ?: emptyList() - session.joinRoom(state.roomId, viaServers = viaServers, callback = object : MatrixCallback { + session.joinRoom(state.roomId, viaServers = state.homeServers, callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt index f461225e2d..6816e54481 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt @@ -24,10 +24,9 @@ data class RoomPreviewViewState( val roomId: String = "", val roomAlias: String? = null, /** - * The server name (might be null) - * Set null when the server is the current user's home server. + * Can be empty when the server is the current user's home server. */ - val homeServer: String? = null, + val homeServers: List = emptyList(), // Current state of the room in preview val roomJoinState: JoinState = JoinState.NOT_JOINED, // Last error of join room request @@ -37,6 +36,6 @@ data class RoomPreviewViewState( constructor(args: RoomPreviewData) : this( roomId = args.roomId, roomAlias = args.roomAlias, - homeServer = args.homeServer + homeServers = args.homeServers ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index c8cf44778c..6a4ea83d68 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -17,7 +17,6 @@ package im.vector.app.features.settings import im.vector.app.R -import im.vector.app.core.preference.VectorSwitchPreference import javax.inject.Inject class VectorSettingsLabsFragment @Inject constructor( @@ -28,103 +27,6 @@ class VectorSettingsLabsFragment @Inject constructor( override val preferenceXmlRes = R.xml.vector_settings_labs override fun bindPref() { - // Lab - - findPreference(VectorPreferences.SETTINGS_LABS_ALLOW_EXTENDED_LOGS)?.let { - it.isChecked = vectorPreferences.labAllowedExtendedLogging() - } - - findPreference(VectorPreferences.SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB)?.let { - it.isChecked = vectorPreferences.labAddNotificationTab() - } - -// val useCryptoPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference -// val cryptoIsEnabledPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY) - - if (session.cryptoService().isCryptoEnabled()) { -// mLabsCategory.removePreference(useCryptoPref) -// -// cryptoIsEnabledPref.isEnabled = false - } else { -// mLabsCategory.removePreference(cryptoIsEnabledPref) -// -// useCryptoPref.isChecked = false -// -// useCryptoPref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValueAsVoid -> -// if (TextUtils.isEmpty(mSession.sessionParams.deviceId)) { -// activity?.let { activity -> -// AlertDialog.Builder(activity) -// .setMessage(R.string.room_settings_labs_end_to_end_warnings) -// .setPositiveButton(R.string.logout) { _, _ -> -// notImplemented() -// // TODO CommonActivityUtils.logout(activity) -// } -// .setNegativeButton(R.string.cancel) { _, _ -> -// useCryptoPref.isChecked = false -// } -// .setOnCancelListener { -// useCryptoPref.isChecked = false -// } -// .show() -// } -// } else { -// val newValue = newValueAsVoid as Boolean -// -// if (mSession.isCryptoEnabled() != newValue) { -// notImplemented() -// /* TODO -// displayLoadingView() -// -// session.enableCrypto(newValue, object : MatrixCallback { -// private fun refresh() { -// activity?.runOnUiThread { -// hideLoadingView() -// useCryptoPref.isChecked = session.isCryptoEnabled -// -// if (session.isCryptoEnabled) { -// mLabsCategory.removePreference(useCryptoPref) -// mLabsCategory.addPreference(cryptoIsEnabledPref) -// } -// } -// } -// -// override fun onSuccess(info: Void?) { -// useCryptoPref.isEnabled = false -// refresh() -// } -// -// override fun onNetworkError(e: Exception) { -// useCryptoPref.isChecked = false -// } -// -// override fun onMatrixError(e: MatrixError) { -// useCryptoPref.isChecked = false -// } -// -// override fun onUnexpectedError(e: Exception) { -// useCryptoPref.isChecked = false -// } -// }) -// */ -// } -// } -// -// true -// } - } - - // SaveMode Management -// findPreference(VectorPreferences.SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY) -// .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> -// notImplemented() -// /* TODO -// val sessions = Matrix.getMXSessions(activity) -// for (session in sessions) { -// session.setUseDataSaveMode(newValue as Boolean) -// } -// */ -// -// true -// } + // Nothing to do } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index b33a0cb5d8..bc72f812ff 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -48,7 +48,8 @@ class VectorSettingsDevicesFragment @Inject constructor( ) : VectorBaseFragment(), DevicesController.Callback { // used to avoid requesting to enter the password for each deletion - private var mAccountPassword: String = "" + // Note: Sonar does not like to use password for member name. + private var mAccountPass: String = "" override fun getLayoutResId() = R.layout.fragment_generic_recycler @@ -91,7 +92,7 @@ class VectorSettingsDevicesFragment @Inject constructor( super.showFailure(throwable) // Password is maybe not good, for safety measure, reset it here - mAccountPassword = "" + mAccountPass = "" } override fun onDestroyView() { @@ -153,12 +154,12 @@ class VectorSettingsDevicesFragment @Inject constructor( * Show a dialog to ask for user password, or use a previously entered password. */ private fun maybeShowDeleteDeviceWithPasswordDialog() { - if (mAccountPassword.isNotEmpty()) { - viewModel.handle(DevicesAction.Password(mAccountPassword)) + if (mAccountPass.isNotEmpty()) { + viewModel.handle(DevicesAction.Password(mAccountPass)) } else { PromptPasswordDialog().show(requireActivity()) { password -> - mAccountPassword = password - viewModel.handle(DevicesAction.Password(mAccountPassword)) + mAccountPass = password + viewModel.handle(DevicesAction.Password(mAccountPass)) } } } diff --git a/vector/src/main/res/layout/item_unknown_room.xml b/vector/src/main/res/layout/item_unknown_room.xml new file mode 100644 index 0000000000..a097767e63 --- /dev/null +++ b/vector/src/main/res/layout/item_unknown_room.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 0b9cbab57a..7506a4d502 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2539,4 +2539,6 @@ Enable PIN If you want to reset your PIN, tap Forgot PIN to logout and reset. Confirm PIN to disable PIN + Can\'t open a room where you are banned from. + Can\'t find this room. Make sure it exists.