Merge branch 'develop' into feature/attachments

This commit is contained in:
ganfra 2019-10-22 17:27:15 +02:00
commit 2974f8b200
138 changed files with 2236 additions and 2113 deletions
CHANGES.mdCONTRIBUTING.mdgradle.properties
matrix-sdk-android
build.gradle
src
main/java/im/vector/matrix/android
api
internal
test/java/im/vector/matrix/android/api/pushrules
vector

View file

@ -10,6 +10,8 @@ Improvements:
- Handle read markers (#84) - Handle read markers (#84)
- Attachments: start using system pickers (#52) - Attachments: start using system pickers (#52)
- Attachments: start handling incoming share (#58) - Attachments: start handling incoming share (#58)
- Mark all messages as read (#396)
- Add ability to report content (#515)
Other changes: Other changes:
- Accessibility improvements to read receipts in the room timeline and reactions emoji chooser - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser
@ -21,6 +23,7 @@ Bugfix:
- after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) - after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267)
- Picture uploads are unreliable, pictures are shown in wrong aspect ratio on desktop client (#517) - Picture uploads are unreliable, pictures are shown in wrong aspect ratio on desktop client (#517)
- Invitation notifications are not dismissed automatically if room is joined from another client (#347) - Invitation notifications are not dismissed automatically if room is joined from another client (#347)
- Opening links from RiotX reuses browser tab (#599)
Translations: Translations:
- -

View file

@ -40,28 +40,45 @@ Please add a line to the top of the file `CHANGES.md` describing your change.
Make sure the following commands execute without any error: Make sure the following commands execute without any error:
> ./tools/check/check_code_quality.sh #### Internal tool
> curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint <pre>
> ./ktlint --android -v ./tools/check/check_code_quality.sh
</pre>
#### ktlint
<pre>
curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint
./ktlint --android --experimental -v
</pre>
Note that you can run Note that you can run
> ./ktlint --android -v -F <pre>
./ktlint --android --experimental -v -F
</pre>
For ktlint to fix some detected errors for you For ktlint to fix some detected errors for you (you still have to check and commit the fix of course)
> ./gradlew lintGplayRelease #### lint
<pre>
./gradlew lintGplayRelease
./gradlew lintFdroidRelease
</pre>
### Unit tests ### Unit tests
Make sure the following commands execute without any error: Make sure the following commands execute without any error:
> ./gradlew testGplayReleaseUnitTest <pre>
./gradlew testGplayReleaseUnitTest
</pre>
### Tests ### Tests
RiotX is currently supported on Android Jelly Bean (API 16+): please test your change on an Android device (or Android emulator) running with API 16. Many issues can happen (including crashes) on older devices. RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices.
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient. Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
### Internationalisation ### Internationalisation

View file

@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
vector.debugPrivateData=false vector.debugPrivateData=false
vector.httpLogLevel=HEADERS vector.httpLogLevel=NONE
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
#vector.debugPrivateData=true #vector.debugPrivateData=true

View file

@ -102,7 +102,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0-beta04" implementation "androidx.recyclerview:recyclerview:1.1.0-beta05"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
@ -110,8 +110,8 @@ dependencies {
// Network // Network
implementation 'com.squareup.retrofit2:retrofit:2.6.2' implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2' implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
implementation 'com.squareup.okhttp3:okhttp:4.2.0' implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation 'com.novoda:merlin:1.2.0' implementation 'com.novoda:merlin:1.2.0'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"

View file

@ -19,7 +19,6 @@ package im.vector.matrix.android.api.extensions
import im.vector.matrix.android.api.comparators.DatedObjectComparators import im.vector.matrix.android.api.comparators.DatedObjectComparators
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import java.util.Collections
/* ========================================================================================== /* ==========================================================================================
* MXDeviceInfo * MXDeviceInfo
@ -29,6 +28,6 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4) ?.chunked(4)
?.joinToString(separator = " ") ?.joinToString(separator = " ")
fun List<DeviceInfo>.sortByLastSeen() { fun MutableList<DeviceInfo>.sortByLastSeen() {
Collections.sort(this, DatedObjectComparators.descComparator) sortWith(DatedObjectComparators.descComparator)
} }

View file

@ -30,9 +30,9 @@ object MatrixLinkify {
* *
* @param spannable the text in which the matrix items has to be clickable. * @param spannable the text in which the matrix items has to be clickable.
*/ */
fun addLinks(spannable: Spannable?, callback: MatrixPermalinkSpan.Callback?): Boolean { fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean {
// sanity checks // sanity checks
if (spannable.isNullOrEmpty()) { if (spannable.isEmpty()) {
return false return false
} }
val text = spannable.toString() val text = spannable.toString()

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.api.permalinks package im.vector.matrix.android.api.permalinks
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
/** /**
@ -48,7 +47,7 @@ object PermalinkFactory {
* @return the permalink, or null in case of error * @return the permalink, or null in case of error
*/ */
fun createPermalink(id: String): String? { fun createPermalink(id: String): String? {
return if (TextUtils.isEmpty(id)) { return if (id.isEmpty()) {
null null
} else MATRIX_TO_URL_BASE + escape(id) } else MATRIX_TO_URL_BASE + escape(id)
} }
@ -71,11 +70,11 @@ object PermalinkFactory {
* @param url the universal link, Ex: "https://matrix.to/#/@benoit:matrix.org" * @param url the universal link, Ex: "https://matrix.to/#/@benoit:matrix.org"
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
*/ */
fun getLinkedId(url: String?): String? { fun getLinkedId(url: String): String? {
val isSupported = url != null && url.startsWith(MATRIX_TO_URL_BASE) val isSupported = url.startsWith(MATRIX_TO_URL_BASE)
return if (isSupported) { return if (isSupported) {
url!!.substring(MATRIX_TO_URL_BASE.length) url.substring(MATRIX_TO_URL_BASE.length)
} else null } else null
} }
@ -86,6 +85,6 @@ object PermalinkFactory {
* @return the escaped id * @return the escaped id
*/ */
private fun escape(id: String): String { private fun escape(id: String): String {
return id.replace("/".toRegex(), "%2F") return id.replace("/", "%2F")
} }
} }

View file

@ -15,13 +15,11 @@
*/ */
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import timber.log.Timber import timber.log.Timber
import java.util.regex.Pattern
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) { class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
@ -34,7 +32,7 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
} }
fun isSatisfied(event: Event, displayName: String): Boolean { fun isSatisfied(event: Event, displayName: String): Boolean {
var message = when (event.type) { val message = when (event.type) {
EventType.MESSAGE -> { EventType.MESSAGE -> {
event.content.toModel<MessageContent>() event.content.toModel<MessageContent>()
} }
@ -59,20 +57,18 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
*/ */
fun caseInsensitiveFind(subString: String, longString: String): Boolean { fun caseInsensitiveFind(subString: String, longString: String): Boolean {
// add sanity checks // add sanity checks
if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) { if (subString.isEmpty() || longString.isEmpty()) {
return false return false
} }
var res = false
try { try {
val pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE) val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE)
res = pattern.matcher(longString).find() return regex.containsMatchIn(longString)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## caseInsensitiveFind() : failed") Timber.e(e, "## caseInsensitiveFind() : failed")
} }
return res return false
} }
} }
} }

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.api.session.events.model package im.vector.matrix.android.api.session.events.model
import android.text.TextUtils
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
@ -35,11 +34,10 @@ typealias Content = JsonDict
* This methods is a facility method to map a json content to a model. * This methods is a facility method to map a json content to a model.
*/ */
inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? { inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
return this?.let {
val moshi = MoshiProvider.providesMoshi() val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java) val moshiAdapter = moshi.adapter(T::class.java)
return try { return try {
moshiAdapter.fromJsonValue(it) moshiAdapter.fromJsonValue(this)
} catch (e: Exception) { } catch (e: Exception) {
if (catchError) { if (catchError) {
Timber.e(e, "To model failed : $e") Timber.e(e, "To model failed : $e")
@ -49,18 +47,15 @@ inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
} }
} }
} }
}
/** /**
* This methods is a facility method to map a model to a json Content * This methods is a facility method to map a model to a json Content
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
inline fun <reified T> T?.toContent(): Content? { inline fun <reified T> T.toContent(): Content {
return this?.let {
val moshi = MoshiProvider.providesMoshi() val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java) val moshiAdapter = moshi.adapter(T::class.java)
return moshiAdapter.toJsonValue(it) as Content return moshiAdapter.toJsonValue(this) as Content
}
} }
/** /**
@ -106,7 +101,7 @@ data class Event(
* @return true if this event is encrypted. * @return true if this event is encrypted.
*/ */
fun isEncrypted(): Boolean { fun isEncrypted(): Boolean {
return TextUtils.equals(type, EventType.ENCRYPTED) return type == EventType.ENCRYPTED
} }
/** /**
@ -139,7 +134,7 @@ data class Event(
} }
fun toContentStringWithIndent(): String { fun toContentStringWithIndent(): String {
val contentMap = toContent()?.toMutableMap() ?: HashMap() val contentMap = toContent().toMutableMap()
return JSONObject(contentMap).toString(4) return JSONObject(contentMap).toString(4)
} }

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendService
@ -38,6 +39,7 @@ interface Room :
ReadService, ReadService,
MembershipService, MembershipService,
StateService, StateService,
ReportingService,
RelationService, RelationService,
RoomCryptoService { RoomCryptoService {

View file

@ -53,4 +53,9 @@ interface RoomService {
* @return the [LiveData] of [RoomSummary] * @return the [LiveData] of [RoomSummary]
*/ */
fun liveRoomSummaries(): LiveData<List<RoomSummary>> fun liveRoomSummaries(): LiveData<List<RoomSummary>>
/**
* Mark all rooms as read
*/
fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable
} }

View file

@ -16,11 +16,9 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import android.text.TextUtils
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import java.util.*
/** /**
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
@ -46,13 +44,7 @@ data class PowerLevels(
* @return the power level * @return the power level
*/ */
fun getUserPowerLevel(userId: String): Int { fun getUserPowerLevel(userId: String): Int {
// sanity check return users.getOrElse(userId) { usersDefault }
if (!TextUtils.isEmpty(userId)) {
val powerLevel = users[userId]
return powerLevel ?: usersDefault
}
return usersDefault
} }
/** /**
@ -61,10 +53,8 @@ data class PowerLevels(
* @param userId the user * @param userId the user
* @param powerLevel the new power level * @param powerLevel the new power level
*/ */
fun setUserPowerLevel(userId: String?, powerLevel: Int) { fun setUserPowerLevel(userId: String, powerLevel: Int) {
if (null != userId) { users[userId] = powerLevel
users[userId] = Integer.valueOf(powerLevel)
}
} }
/** /**
@ -75,7 +65,7 @@ data class PowerLevels(
* @return true if the user can send the event * @return true if the user can send the event
*/ */
fun maySendEventOfType(eventTypeString: String, userId: String): Boolean { fun maySendEventOfType(eventTypeString: String, userId: String): Boolean {
return if (!TextUtils.isEmpty(eventTypeString) && !TextUtils.isEmpty(userId)) { return if (eventTypeString.isNotEmpty() && userId.isNotEmpty()) {
getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString) getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString)
} else false } else false
} }
@ -118,18 +108,14 @@ data class PowerLevels(
* @param key the notification key * @param key the notification key
* @return the level * @return the level
*/ */
fun notificationLevel(key: String?): Int { fun notificationLevel(key: String): Int {
if (null != key && notifications.containsKey(key)) { val valAsVoid = notifications[key] ?: return 50
val valAsVoid = notifications[key]
// the first implementation was a string value // the first implementation was a string value
return if (valAsVoid is String) { return if (valAsVoid is String) {
Integer.parseInt(valAsVoid) valAsVoid.toInt()
} else { } else {
valAsVoid as Int valAsVoid as Int
} }
} }
return 50
}
} }

View file

@ -145,15 +145,7 @@ class CreateRoomParams {
*/ */
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) { fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
// Remove the existing value if any. // Remove the existing value if any.
if (initialStates != null && !initialStates!!.isEmpty()) { initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY }
val newInitialStates = ArrayList<Event>()
for (event in initialStates!!) {
if (event.getClearType() != EventType.STATE_HISTORY_VISIBILITY) {
newInitialStates.add(event)
}
}
initialStates = newInitialStates
}
if (historyVisibility != null) { if (historyVisibility != null) {
val contentMap = HashMap<String, RoomHistoryVisibility>() val contentMap = HashMap<String, RoomHistoryVisibility>()

View file

@ -50,21 +50,19 @@ interface RelationService {
/** /**
* Sends a reaction (emoji) to the targetedEvent. * Sends a reaction (emoji) to the targetedEvent.
* @param reaction the reaction (preferably emoji)
* @param targetEventId the id of the event being reacted * @param targetEventId the id of the event being reacted
* @param reaction the reaction (preferably emoji)
*/ */
fun sendReaction(reaction: String, fun sendReaction(targetEventId: String,
targetEventId: String): Cancelable reaction: String): Cancelable
/** /**
* Undo a reaction (emoji) to the targetedEvent. * Undo a reaction (emoji) to the targetedEvent.
* @param reaction the reaction (preferably emoji)
* @param targetEventId the id of the event being reacted * @param targetEventId the id of the event being reacted
* @param myUserId used to know if a reaction event was made by the user * @param reaction the reaction (preferably emoji)
*/ */
fun undoReaction(reaction: String, fun undoReaction(targetEventId: String,
targetEventId: String, reaction: String): Cancelable
myUserId: String) // : Cancelable
/** /**
* Edit a text message body. Limited to "m.text" contentType * Edit a text message body. Limited to "m.text" contentType
@ -92,7 +90,7 @@ interface RelationService {
compatibilityBodyText: String = "* $newBodyText"): Cancelable compatibilityBodyText: String = "* $newBodyText"): Cancelable
/** /**
* Get's the edit history of the given event * Get the edit history of the given event
*/ */
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)

View file

@ -0,0 +1,32 @@
/*
* 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.matrix.android.api.session.room.reporting
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* This interface defines methods to report content of an event.
*/
interface ReportingService {
/**
* Report content
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid
*/
fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable
}

View file

@ -21,7 +21,6 @@ package im.vector.matrix.android.internal.crypto
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import arrow.core.Try
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.Lazy import dagger.Lazy
@ -359,29 +358,16 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) { override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
// build a devices map // build a devices map
val devicesIdListByUserId = HashMap<String, List<String>>() val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId })
for (di in devices) { for ((userId, deviceIds) in devicesIdListByUserId) {
var deviceIdsList: MutableList<String>? = devicesIdListByUserId[di.userId]?.toMutableList()
if (null == deviceIdsList) {
deviceIdsList = ArrayList()
devicesIdListByUserId[di.userId] = deviceIdsList
}
deviceIdsList.add(di.deviceId)
}
val userIds = devicesIdListByUserId.keys
for (userId in userIds) {
val storedDeviceIDs = cryptoStore.getUserDevices(userId) val storedDeviceIDs = cryptoStore.getUserDevices(userId)
// sanity checks // sanity checks
if (null != storedDeviceIDs) { if (null != storedDeviceIDs) {
var isUpdated = false var isUpdated = false
val deviceIds = devicesIdListByUserId[userId]
deviceIds?.forEach { deviceId -> deviceIds.forEach { deviceId ->
val device = storedDeviceIDs[deviceId] val device = storedDeviceIDs[deviceId]
// assume if the device is either verified or blocked // assume if the device is either verified or blocked
@ -549,16 +535,10 @@ internal class DefaultCryptoService @Inject constructor(
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
Timber.v("## encryptEventContent() starts") Timber.v("## encryptEventContent() starts")
runCatching { runCatching {
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
}
.fold(
{
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED)) MXEncryptEventContentResult(content, EventType.ENCRYPTED)
}, }.foldToCallback(callback)
{ callback.onFailure(it) }
)
} else { } else {
val algorithm = getEncryptionAlgorithm(roomId) val algorithm = getEncryptionAlgorithm(roomId)
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
@ -776,7 +756,7 @@ internal class DefaultCryptoService @Inject constructor(
GlobalScope.launch(coroutineDispatchers.main) { GlobalScope.launch(coroutineDispatchers.main) {
runCatching { runCatching {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
}.fold(callback::onSuccess, callback::onFailure) }.foldToCallback(callback)
} }
} }
@ -785,7 +765,6 @@ internal class DefaultCryptoService @Inject constructor(
* *
* @param password the password * @param password the password
* @param anIterationCount the encryption iteration count (0 means no encryption) * @param anIterationCount the encryption iteration count (0 means no encryption)
* @param callback the exported keys
*/ */
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
return withContext(coroutineDispatchers.crypto) { return withContext(coroutineDispatchers.crypto) {
@ -813,8 +792,8 @@ internal class DefaultCryptoService @Inject constructor(
progressListener: ProgressListener?, progressListener: ProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) { callback: MatrixCallback<ImportRoomKeysResult>) {
GlobalScope.launch(coroutineDispatchers.main) { GlobalScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.crypto) {
Try {
Timber.v("## importRoomKeys starts") Timber.v("## importRoomKeys starts")
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
@ -861,19 +840,14 @@ internal class DefaultCryptoService @Inject constructor(
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) { fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
// force the refresh to ensure that the devices list is up-to-date // force the refresh to ensure that the devices list is up-to-date
GlobalScope.launch(coroutineDispatchers.crypto) { GlobalScope.launch(coroutineDispatchers.crypto) {
runCatching { deviceListManager.downloadKeys(userIds, true) } runCatching {
.fold( val keys = deviceListManager.downloadKeys(userIds, true)
{ val unknownDevices = getUnknownDevices(keys)
val unknownDevices = getUnknownDevices(it) if (unknownDevices.map.isNotEmpty()) {
if (unknownDevices.map.isEmpty()) {
callback.onSuccess(Unit)
} else {
// trigger an an unknown devices exception // trigger an an unknown devices exception
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices))) throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices))
} }
}, }.foldToCallback(callback)
{ callback.onFailure(it) }
)
} }
} }

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
@ -27,7 +26,6 @@ import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
// Legacy name: MXDeviceList // Legacy name: MXDeviceList
@ -39,13 +37,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) { private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
// HS not ready for retry // HS not ready for retry
private val notReadyToRetryHS = HashSet<String>() private val notReadyToRetryHS = mutableSetOf<String>()
init { init {
var isUpdated = false var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in deviceTrackingStatuses.keys) { for ((userId, status) in deviceTrackingStatuses) {
val status = deviceTrackingStatuses[userId]!!
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
// if a download was in progress when we got shut down, it isn't any more. // if a download was in progress when we got shut down, it isn't any more.
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
@ -66,7 +63,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private fun canRetryKeysDownload(userId: String): Boolean { private fun canRetryKeysDownload(userId: String): Boolean {
var res = false var res = false
if (!TextUtils.isEmpty(userId) && userId.contains(":")) { if (':' in userId) {
try { try {
synchronized(notReadyToRetryHS) { synchronized(notReadyToRetryHS) {
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1)) res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
@ -119,11 +116,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param changed the user ids list which have new devices * @param changed the user ids list which have new devices
* @param left the user ids list which left a room * @param left the user ids list which left a room
*/ */
fun handleDeviceListsChanges(changed: List<String>?, left: List<String>?) { fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
var isUpdated = false var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
if (changed?.isNotEmpty() == true) {
for (userId in changed) { for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) { if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId") Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
@ -131,9 +127,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
isUpdated = true isUpdated = true
} }
} }
}
if (left?.isNotEmpty() == true) {
for (userId in left) { for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) { if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId") Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
@ -141,7 +135,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
isUpdated = true isUpdated = true
} }
} }
}
if (isUpdated) { if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
@ -153,7 +146,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* + update * + update
*/ */
fun invalidateAllDeviceLists() { fun invalidateAllDeviceLists() {
handleDeviceListsChanges(ArrayList(cryptoStore.getDeviceTrackingStatuses().keys), null) handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList())
} }
/** /**
@ -163,9 +156,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
*/ */
private fun onKeysDownloadFailed(userIds: List<String>) { private fun onKeysDownloadFailed(userIds: List<String>) {
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in userIds) { userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
}
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
} }
@ -177,16 +168,11 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
*/ */
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<MXDeviceInfo> { private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<MXDeviceInfo> {
if (failures != null) { if (failures != null) {
val keys = failures.keys for ((k, value) in failures) {
for (k in keys) { val statusCode = when (val status = value["status"]) {
val value = failures[k] is Double -> status.toInt()
if (value!!.containsKey("status")) { is Int -> status.toInt()
val statusCodeAsVoid = value["status"] else -> 0
var statusCode = 0
if (statusCodeAsVoid is Double) {
statusCode = statusCodeAsVoid.toInt()
} else if (statusCodeAsVoid is Int) {
statusCode = statusCodeAsVoid.toInt()
} }
if (statusCode == 503) { if (statusCode == 503) {
synchronized(notReadyToRetryHS) { synchronized(notReadyToRetryHS) {
@ -195,7 +181,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
} }
} }
}
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
val usersDevicesInfoMap = MXUsersDevicesMap<MXDeviceInfo>() val usersDevicesInfoMap = MXUsersDevicesMap<MXDeviceInfo>()
for (userId in userIds) { for (userId in userIds) {
@ -228,11 +213,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
/** /**
* Download the device keys for a list of users and stores the keys in the MXStore. * Download the device keys for a list of users and stores the keys in the MXStore.
* It must be called in getEncryptingThreadHandler() thread. * It must be called in getEncryptingThreadHandler() thread.
* The callback is called in the UI thread.
* *
* @param userIds The users to fetch. * @param userIds The users to fetch.
* @param forceDownload Always download the keys even if cached. * @param forceDownload Always download the keys even if cached.
* @param callback the asynchronous callback
*/ */
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> { suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds") Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
@ -270,7 +253,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
Timber.v("## downloadKeys() : starts") Timber.v("## downloadKeys() : starts")
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val result = doKeyDownloadForUsers(downloadUsers) val result = doKeyDownloadForUsers(downloadUsers)
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms") Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms")
result.also { result.also {
it.addEntriesFromMap(stored) it.addEntriesFromMap(stored)
} }
@ -303,16 +286,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val devices = response.deviceKeys?.get(userId) val devices = response.deviceKeys?.get(userId)
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices") Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
if (devices != null) { if (devices != null) {
val mutableDevices = HashMap(devices) val mutableDevices = devices.toMutableMap()
val deviceIds = ArrayList(mutableDevices.keys) for ((deviceId, deviceInfo) in devices) {
for (deviceId in deviceIds) {
// Get the potential previously store device keys for this device // Get the potential previously store device keys for this device
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId) val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
val deviceInfo = mutableDevices[deviceId]
// in some race conditions (like unit tests) // in some race conditions (like unit tests)
// the self device must be seen as verified // the self device must be seen as verified
if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) { if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
} }
// Validate received keys // Validate received keys
@ -365,13 +346,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
// Check that the user_id and device_id in the received deviceKeys are correct // Check that the user_id and device_id in the received deviceKeys are correct
if (!TextUtils.equals(deviceKeys.userId, userId)) { if (deviceKeys.userId != userId) {
Timber.e("## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId) Timber.e("## validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
return false return false
} }
if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) { if (deviceKeys.deviceId != deviceId) {
Timber.e("## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId) Timber.e("## validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
return false return false
} }
@ -379,21 +360,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val signKey = deviceKeys.keys?.get(signKeyId) val signKey = deviceKeys.keys?.get(signKeyId)
if (null == signKey) { if (null == signKey) {
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key") Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
return false return false
} }
val signatureMap = deviceKeys.signatures?.get(userId) val signatureMap = deviceKeys.signatures?.get(userId)
if (null == signatureMap) { if (null == signatureMap) {
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId) Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
return false return false
} }
val signature = signatureMap[signKeyId] val signature = signatureMap[signKeyId]
if (null == signature) { if (null == signature) {
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " is not signed") Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
return false return false
} }
@ -414,7 +395,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
if (null != previouslyStoredDeviceKeys) { if (null != previouslyStoredDeviceKeys) {
if (!TextUtils.equals(previouslyStoredDeviceKeys.fingerprint(), signKey)) { if (previouslyStoredDeviceKeys.fingerprint() != signKey) {
// This should only happen if the list has been MITMed; we are // This should only happen if the list has been MITMed; we are
// best off sticking with the original keys. // best off sticking with the original keys.
// //
@ -424,7 +405,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
Timber.e("## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys) Timber.e("## validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
return false return false
} }
@ -438,27 +419,18 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* This method must be called on getEncryptingThreadHandler() thread. * This method must be called on getEncryptingThreadHandler() thread.
*/ */
suspend fun refreshOutdatedDeviceLists() { suspend fun refreshOutdatedDeviceLists() {
val users = ArrayList<String>()
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in deviceTrackingStatuses.keys) { val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]) { TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
users.add(userId)
}
} }
if (users.size == 0) { if (users.isEmpty()) {
return return
} }
// update the statuses // update the statuses
for (userId in users) { users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
val status = deviceTrackingStatuses[userId]
if (null != status && TRACKING_STATUS_PENDING_DOWNLOAD == status) {
deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS)
}
}
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
runCatching { runCatching {

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -25,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -58,7 +56,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
when (roomKeyShare?.action) { when (roomKeyShare?.action) {
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event)) RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
RoomKeyShare.ACTION_SHARE_CANCELLATION -> receivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(event)) RoomKeyShare.ACTION_SHARE_CANCELLATION -> receivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(event))
else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action " + roomKeyShare?.action) else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action ${roomKeyShare?.action}")
} }
} }
@ -68,7 +66,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
* It must be called on CryptoThread * It must be called on CryptoThread
*/ */
fun processReceivedRoomKeyRequests() { fun processReceivedRoomKeyRequests() {
val roomKeyRequestsToProcess = ArrayList(receivedRoomKeyRequests) val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList()
receivedRoomKeyRequests.clear() receivedRoomKeyRequests.clear()
for (request in roomKeyRequestsToProcess) { for (request in roomKeyRequestsToProcess) {
val userId = request.userId val userId = request.userId
@ -77,7 +75,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
val roomId = body!!.roomId val roomId = body!!.roomId
val alg = body.algorithm val alg = body.algorithm
Timber.v("m.room_key_request from " + userId + ":" + deviceId + " for " + roomId + " / " + body.sessionId + " id " + request.requestId) Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (userId == null || credentials.userId != userId) { if (userId == null || credentials.userId != userId) {
// TODO: determine if we sent this device the keys already: in // TODO: determine if we sent this device the keys already: in
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now") Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
@ -92,12 +90,12 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
continue continue
} }
if (!decryptor.hasKeysForKeyRequest(request)) { if (!decryptor.hasKeysForKeyRequest(request)) {
Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session " + body.sessionId!!) Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.deleteIncomingRoomKeyRequest(request)
continue continue
} }
if (TextUtils.equals(deviceId, credentials.deviceId) && TextUtils.equals(credentials.userId, userId)) { if (credentials.deviceId == deviceId && credentials.userId == userId) {
Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored") Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored")
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.deleteIncomingRoomKeyRequest(request)
continue continue
@ -132,7 +130,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
synchronized(this.receivedRoomKeyRequestCancellations) { synchronized(this.receivedRoomKeyRequestCancellations) {
if (!this.receivedRoomKeyRequestCancellations.isEmpty()) { if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) {
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList() receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
this.receivedRoomKeyRequestCancellations.clear() this.receivedRoomKeyRequestCancellations.clear()
} }

View file

@ -16,20 +16,19 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import android.util.Base64 import android.util.Base64
import im.vector.matrix.android.internal.extensions.toUnsignedInt import im.vector.matrix.android.internal.extensions.toUnsignedInt
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.charset.Charset import java.nio.charset.Charset
import java.security.SecureRandom import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.and import kotlin.experimental.and
import kotlin.experimental.xor import kotlin.experimental.xor
import kotlin.math.min
/** /**
* Utility class to import/export the crypto data * Utility class to import/export the crypto data
@ -51,7 +50,7 @@ object MXMegolmExportEncryption {
* @return the AES key * @return the AES key
*/ */
private fun getAesKey(keyBits: ByteArray): ByteArray { private fun getAesKey(keyBits: ByteArray): ByteArray {
return Arrays.copyOfRange(keyBits, 0, 32) return keyBits.copyOfRange(0, 32)
} }
/** /**
@ -61,7 +60,7 @@ object MXMegolmExportEncryption {
* @return the Hmac key. * @return the Hmac key.
*/ */
private fun getHmacKey(keyBits: ByteArray): ByteArray { private fun getHmacKey(keyBits: ByteArray): ByteArray {
return Arrays.copyOfRange(keyBits, 32, keyBits.size) return keyBits.copyOfRange(32, keyBits.size)
} }
/** /**
@ -77,7 +76,7 @@ object MXMegolmExportEncryption {
val body = unpackMegolmKeyFile(data) val body = unpackMegolmKeyFile(data)
// check we have a version byte // check we have a version byte
if (null == body || body.size == 0) { if (null == body || body.isEmpty()) {
Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") Timber.e("## decryptMegolmKeyFile() : Invalid file: too short")
throw Exception("Invalid file: too short") throw Exception("Invalid file: too short")
} }
@ -93,27 +92,27 @@ object MXMegolmExportEncryption {
throw Exception("Invalid file: too short") throw Exception("Invalid file: too short")
} }
if (TextUtils.isEmpty(password)) { if (password.isEmpty()) {
throw Exception("Empty password is not supported") throw Exception("Empty password is not supported")
} }
val salt = Arrays.copyOfRange(body, 1, 1 + 16) val salt = body.copyOfRange(1, 1 + 16)
val iv = Arrays.copyOfRange(body, 17, 17 + 16) val iv = body.copyOfRange(17, 17 + 16)
val iterations = val iterations =
(body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt() (body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt()
val ciphertext = Arrays.copyOfRange(body, 37, 37 + ciphertextLength) val ciphertext = body.copyOfRange(37, 37 + ciphertextLength)
val hmac = Arrays.copyOfRange(body, body.size - 32, body.size) val hmac = body.copyOfRange(body.size - 32, body.size)
val deriveKey = deriveKeys(salt, iterations, password) val deriveKey = deriveKeys(salt, iterations, password)
val toVerify = Arrays.copyOfRange(body, 0, body.size - 32) val toVerify = body.copyOfRange(0, body.size - 32)
val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256") val mac = Mac.getInstance("HmacSHA256")
mac.init(macKey) mac.init(macKey)
val digest = mac.doFinal(toVerify) val digest = mac.doFinal(toVerify)
if (!Arrays.equals(hmac, digest)) { if (!hmac.contentEquals(digest)) {
Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?") Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?")
throw Exception("Authentication check failed: incorrect password?") throw Exception("Authentication check failed: incorrect password?")
} }
@ -146,7 +145,7 @@ object MXMegolmExportEncryption {
@Throws(Exception::class) @Throws(Exception::class)
@JvmOverloads @JvmOverloads
fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray { fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray {
if (TextUtils.isEmpty(password)) { if (password.isEmpty()) {
throw Exception("Empty password is not supported") throw Exception("Empty password is not supported")
} }
@ -196,7 +195,7 @@ object MXMegolmExportEncryption {
System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size) System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size)
idx += cipherArray.size idx += cipherArray.size
val toSign = Arrays.copyOfRange(resultBuffer, 0, idx) val toSign = resultBuffer.copyOfRange(0, idx)
val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256") val mac = Mac.getInstance("HmacSHA256")
@ -234,7 +233,7 @@ object MXMegolmExportEncryption {
// start the next line after the newline // start the next line after the newline
lineStart = lineEnd + 1 lineStart = lineEnd + 1
if (TextUtils.equals(line, HEADER_LINE)) { if (line == HEADER_LINE) {
break break
} }
} }
@ -244,15 +243,13 @@ object MXMegolmExportEncryption {
// look for the end line // look for the end line
while (true) { while (true) {
val lineEnd = fileStr.indexOf('\n', lineStart) val lineEnd = fileStr.indexOf('\n', lineStart)
val line: String val line = if (lineEnd < 0) {
fileStr.substring(lineStart)
if (lineEnd < 0) {
line = fileStr.substring(lineStart).trim()
} else { } else {
line = fileStr.substring(lineStart, lineEnd).trim() fileStr.substring(lineStart, lineEnd)
} }.trim()
if (TextUtils.equals(line, TRAILER_LINE)) { if (line == TRAILER_LINE) {
break break
} }
@ -290,7 +287,7 @@ object MXMegolmExportEncryption {
for (i in 1..nLines) { for (i in 1..nLines) {
outStream.write("\n".toByteArray()) outStream.write("\n".toByteArray())
val len = Math.min(LINE_LENGTH, data.size - o) val len = min(LINE_LENGTH, data.size - o)
outStream.write(Base64.encode(data, o, len, Base64.DEFAULT)) outStream.write(Base64.encode(data, o, len, Base64.DEFAULT))
o += LINE_LENGTH o += LINE_LENGTH
} }
@ -318,7 +315,7 @@ object MXMegolmExportEncryption {
// it is simpler than the generic algorithm because the expected key length is equal to the mac key length. // it is simpler than the generic algorithm because the expected key length is equal to the mac key length.
// noticed as dklen/hlen // noticed as dklen/hlen
val prf = Mac.getInstance("HmacSHA512") val prf = Mac.getInstance("HmacSHA512")
prf.init(SecretKeySpec(password.toByteArray(charset("UTF-8")), "HmacSHA512")) prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512"))
// 512 bits key length // 512 bits key length
val key = ByteArray(64) val key = ByteArray(64)
@ -326,8 +323,7 @@ object MXMegolmExportEncryption {
// U1 = PRF(Password, Salt || INT_32_BE(i)) // U1 = PRF(Password, Salt || INT_32_BE(i))
prf.update(salt) prf.update(salt)
val int32BE = ByteArray(4) val int32BE = ByteArray(4) { 0.toByte() }
Arrays.fill(int32BE, 0.toByte())
int32BE[3] = 1.toByte() int32BE[3] = 1.toByte()
prf.update(int32BE) prf.update(int32BE)
prf.doFinal(Uc, 0) prf.doFinal(Uc, 0)
@ -346,7 +342,7 @@ object MXMegolmExportEncryption {
} }
} }
Timber.v("## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") Timber.v("## deriveKeys() : $iterations in ${System.currentTimeMillis() - t0} ms")
return key return key
} }

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
@ -33,7 +32,6 @@ import im.vector.matrix.android.internal.util.convertToUTF8
import org.matrix.olm.* import org.matrix.olm.*
import timber.log.Timber import timber.log.Timber
import java.net.URLEncoder import java.net.URLEncoder
import java.util.*
import javax.inject.Inject import javax.inject.Inject
// The libolm wrapper. // The libolm wrapper.
@ -434,7 +432,7 @@ internal class MXOlmDevice @Inject constructor(
* @return the base64-encoded secret key. * @return the base64-encoded secret key.
*/ */
fun getSessionKey(sessionId: String): String? { fun getSessionKey(sessionId: String): String? {
if (!TextUtils.isEmpty(sessionId)) { if (sessionId.isNotEmpty()) {
try { try {
return outboundGroupSessionStore[sessionId]!!.sessionKey() return outboundGroupSessionStore[sessionId]!!.sessionKey()
} catch (e: Exception) { } catch (e: Exception) {
@ -451,7 +449,7 @@ internal class MXOlmDevice @Inject constructor(
* @return the current chain index. * @return the current chain index.
*/ */
fun getMessageIndex(sessionId: String): Int { fun getMessageIndex(sessionId: String): Int {
return if (!TextUtils.isEmpty(sessionId)) { return if (sessionId.isNotEmpty()) {
outboundGroupSessionStore[sessionId]!!.messageIndex() outboundGroupSessionStore[sessionId]!!.messageIndex()
} else 0 } else 0
} }
@ -464,7 +462,7 @@ internal class MXOlmDevice @Inject constructor(
* @return ciphertext * @return ciphertext
*/ */
fun encryptGroupMessage(sessionId: String, payloadString: String): String? { fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) { if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
try { try {
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString) return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString)
} catch (e: Exception) { } catch (e: Exception) {
@ -523,7 +521,7 @@ internal class MXOlmDevice @Inject constructor(
} }
try { try {
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) { if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
session.olmInboundGroupSession!!.releaseSession() session.olmInboundGroupSession!!.releaseSession()
return false return false
@ -573,7 +571,7 @@ internal class MXOlmDevice @Inject constructor(
} }
try { try {
if (!TextUtils.equals(session.olmInboundGroupSession?.sessionIdentifier(), sessionId)) { if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession() if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
continue continue
@ -758,7 +756,7 @@ internal class MXOlmDevice @Inject constructor(
if (session != null) { if (session != null) {
// Check that the room id matches the original one for the session. This stops // Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room. // the HS pretending a message was targeting a different room.
if (!TextUtils.equals(roomId, session.roomId)) { if (roomId != session.roomId) {
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
Timber.e("## getInboundGroupSession() : $errorDescription") Timber.e("## getInboundGroupSession() : $errorDescription")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)

View file

@ -16,12 +16,10 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
@ -42,11 +40,11 @@ internal class MyDeviceInfoHolder @Inject constructor(
init { init {
val keys = HashMap<String, String>() val keys = HashMap<String, String>()
if (!TextUtils.isEmpty(olmDevice.deviceEd25519Key)) { if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!! keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
} }
if (!TextUtils.isEmpty(olmDevice.deviceCurve25519Key)) { if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!! keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
} }
@ -58,13 +56,7 @@ internal class MyDeviceInfoHolder @Inject constructor(
// Add our own deviceinfo to the store // Add our own deviceinfo to the store
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId) val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
val myDevices: MutableMap<String, MXDeviceInfo> val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
if (null != endToEndDevicesForUser) {
myDevices = HashMap(endToEndDevicesForUser)
} else {
myDevices = HashMap()
}
myDevices[myDevice.deviceId] = myDevice myDevices[myDevice.deviceId] = myDevice

View file

@ -24,8 +24,9 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.floor
import kotlin.math.min
@SessionScope @SessionScope
internal class OneTimeKeysUploader @Inject constructor( internal class OneTimeKeysUploader @Inject constructor(
@ -77,7 +78,7 @@ internal class OneTimeKeysUploader @Inject constructor(
// If we run out of slots when generating new keys then olm will // If we run out of slots when generating new keys then olm will
// discard the oldest private keys first. This will eventually clean // discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message. // out stale private keys that won't receive a message.
val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt() val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
if (oneTimeKeyCount != null) { if (oneTimeKeyCount != null) {
uploadOTK(oneTimeKeyCount!!, keyLimit) uploadOTK(oneTimeKeyCount!!, keyLimit)
} else { } else {
@ -116,7 +117,7 @@ internal class OneTimeKeysUploader @Inject constructor(
// If we don't need to generate any more keys then we are done. // If we don't need to generate any more keys then we are done.
return return
} }
val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
olmDevice.generateOneTimeKeys(keysThisLoop) olmDevice.generateOneTimeKeys(keysThisLoop)
val response = uploadOneTimeKeys() val response = uploadOneTimeKeys()
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
@ -132,14 +133,14 @@ internal class OneTimeKeysUploader @Inject constructor(
*/ */
private suspend fun uploadOneTimeKeys(): KeysUploadResponse { private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
val oneTimeKeys = olmDevice.getOneTimeKeys() val oneTimeKeys = olmDevice.getOneTimeKeys()
val oneTimeJson = HashMap<String, Any>() val oneTimeJson = mutableMapOf<String, Any>()
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
if (null != curve25519Map) { if (null != curve25519Map) {
for (key_id in curve25519Map.keys) { for ((key_id, value) in curve25519Map) {
val k = HashMap<String, Any>() val k = mutableMapOf<String, Any>()
k["key"] = curve25519Map.getValue(key_id) k["key"] = value
// the key is also signed // the key is also signed
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)

View file

@ -16,13 +16,11 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
@ -62,10 +60,8 @@ internal class RoomDecryptorProvider @Inject constructor(
} }
if (roomId != null && roomId.isNotEmpty()) { if (roomId != null && roomId.isNotEmpty()) {
synchronized(roomDecryptors) { synchronized(roomDecryptors) {
if (!roomDecryptors.containsKey(roomId)) { val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() }
roomDecryptors[roomId] = HashMap() val alg = decryptors[algorithm]
}
val alg = roomDecryptors[roomId]?.get(algorithm)
if (alg != null) { if (alg != null) {
return alg return alg
} }
@ -89,7 +85,7 @@ internal class RoomDecryptorProvider @Inject constructor(
} }
else -> olmDecryptionFactory.create() else -> olmDecryptionFactory.create()
} }
if (roomId != null && !TextUtils.isEmpty(roomId)) { if (!roomId.isNullOrEmpty()) {
synchronized(roomDecryptors) { synchronized(roomDecryptors) {
roomDecryptors[roomId]?.put(algorithm, alg) roomDecryptors[roomId]?.put(algorithm, alg)
} }

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto.actions package im.vector.matrix.android.internal.crypto.actions
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXKey
@ -24,7 +23,6 @@ import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice, internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice,
@ -35,18 +33,14 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
val results = MXUsersDevicesMap<MXOlmSessionResult>() val results = MXUsersDevicesMap<MXOlmSessionResult>()
val userIds = devicesByUser.keys for ((userId, deviceInfos) in devicesByUser) {
for (deviceInfo in deviceInfos) {
for (userId in userIds) {
val deviceInfos = devicesByUser[userId]
for (deviceInfo in deviceInfos!!) {
val deviceId = deviceInfo.deviceId val deviceId = deviceInfo.deviceId
val key = deviceInfo.identityKey() val key = deviceInfo.identityKey()
val sessionId = olmDevice.getSessionId(key!!) val sessionId = olmDevice.getSessionId(key!!)
if (TextUtils.isEmpty(sessionId)) { if (sessionId.isNullOrEmpty()) {
devicesWithoutSession.add(deviceInfo) devicesWithoutSession.add(deviceInfo)
} }
@ -79,9 +73,8 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
for (userId in userIds) { for ((userId, deviceInfos) in devicesByUser) {
val deviceInfos = devicesByUser[userId] for (deviceInfo in deviceInfos) {
for (deviceInfo in deviceInfos!!) {
var oneTimeKey: MXKey? = null var oneTimeKey: MXKey? = null
val deviceIds = oneTimeKeys.getUserDeviceIds(userId) val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
if (null != deviceIds) { if (null != deviceIds) {
@ -116,24 +109,22 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
val signKeyId = "ed25519:$deviceId" val signKeyId = "ed25519:$deviceId"
val signature = oneTimeKey.signatureForUserId(userId, signKeyId) val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
if (!TextUtils.isEmpty(signature) && !TextUtils.isEmpty(deviceInfo.fingerprint())) { if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) {
var isVerified = false var isVerified = false
var errorMessage: String? = null var errorMessage: String? = null
if (signature != null) {
try { try {
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature)
isVerified = true isVerified = true
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = e.message errorMessage = e.message
} }
}
// Check one-time key signature // Check one-time key signature
if (isVerified) { if (isVerified) {
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value)
if (!TextUtils.isEmpty(sessionId)) { if (!sessionId.isNullOrEmpty()) {
Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId
+ " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
} else { } else {

View file

@ -16,14 +16,11 @@
package im.vector.matrix.android.internal.crypto.actions package im.vector.matrix.android.internal.crypto.actions
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice, internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice,
@ -36,27 +33,14 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o
*/ */
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> { suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>() val devicesByUser = users.associateWith { userId ->
for (userId in users) {
devicesByUser[userId] = ArrayList()
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
for (device in devices) { devices.filter {
val key = device.identityKey()
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
// Don't bother setting up session to ourself // Don't bother setting up session to ourself
continue it.identityKey() != olmDevice.deviceCurve25519Key
}
if (device.isVerified) {
// Don't bother setting up sessions with blocked users // Don't bother setting up sessions with blocked users
continue && !it.isVerified
}
devicesByUser[userId]!!.add(device)
} }
} }
return ensureOlmSessionsForDevicesAction.handle(devicesByUser) return ensureOlmSessionsForDevicesAction.handle(devicesByUser)

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto.actions package im.vector.matrix.android.internal.crypto.actions
import android.text.TextUtils
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
@ -25,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertToUTF8 import im.vector.matrix.android.internal.util.convertToUTF8
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal class MessageEncrypter @Inject constructor(private val credentials: Credentials, internal class MessageEncrypter @Inject constructor(private val credentials: Credentials,
@ -40,18 +38,12 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
* @return the content for an m.room.encrypted event. * @return the content for an m.room.encrypted event.
*/ */
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage { fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage {
val deviceInfoParticipantKey = HashMap<String, MXDeviceInfo>() val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
val participantKeys = ArrayList<String>()
for (di in deviceInfos) { val payloadJson = payloadFields.toMutableMap()
participantKeys.add(di.identityKey()!!)
deviceInfoParticipantKey[di.identityKey()!!] = di
}
val payloadJson = HashMap(payloadFields)
payloadJson["sender"] = credentials.userId payloadJson["sender"] = credentials.userId
payloadJson["sender_device"] = credentials.deviceId payloadJson["sender_device"] = credentials.deviceId!!
// Include the Ed25519 key so that the recipient knows what // Include the Ed25519 key so that the recipient knows what
// device this message came from. // device this message came from.
@ -67,30 +59,24 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
val ciphertext = HashMap<String, Any>() val ciphertext = HashMap<String, Any>()
for (deviceKey in participantKeys) { for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
val sessionId = olmDevice.getSessionId(deviceKey) val sessionId = olmDevice.getSessionId(deviceKey)
if (!TextUtils.isEmpty(sessionId)) { if (!sessionId.isNullOrEmpty()) {
Timber.v("Using sessionid $sessionId for device $deviceKey") Timber.v("Using sessionid $sessionId for device $deviceKey")
val deviceInfo = deviceInfoParticipantKey[deviceKey]
payloadJson["recipient"] = deviceInfo!!.userId payloadJson["recipient"] = deviceInfo.userId
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
val recipientsKeysMap = HashMap<String, String>()
recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!!
payloadJson["recipient_keys"] = recipientsKeysMap
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId!!, payloadString)!! ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!!
} }
} }
val res = EncryptedMessage() return EncryptedMessage(
algorithm = MXCRYPTO_ALGORITHM_OLM,
res.algorithm = MXCRYPTO_ALGORITHM_OLM senderKey = olmDevice.deviceCurve25519Key,
res.senderKey = olmDevice.deviceCurve25519Key cipherText = ciphertext
res.cipherText = ciphertext )
return res
} }
} }

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto.algorithms.megolm package im.vector.matrix.android.internal.crypto.algorithms.megolm
import android.text.TextUtils
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -148,7 +147,7 @@ internal class MXMegolmDecryption(private val userId: String,
selfMap["deviceId"] = "*" selfMap["deviceId"] = "*"
recipients.add(selfMap) recipients.add(selfMap)
if (!TextUtils.equals(sender, userId)) { if (sender != userId) {
val senderMap = HashMap<String, String>() val senderMap = HashMap<String, String>()
senderMap["userId"] = sender senderMap["userId"] = sender
senderMap["deviceId"] = encryptedEventContent.deviceId!! senderMap["deviceId"] = encryptedEventContent.deviceId!!
@ -176,17 +175,12 @@ internal class MXMegolmDecryption(private val userId: String,
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
if (!pendingEvents.containsKey(pendingEventsKey)) { val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
pendingEvents[pendingEventsKey] = HashMap() val events = timeline.getOrPut(timelineId) { ArrayList() }
}
if (pendingEvents[pendingEventsKey]?.containsKey(timelineId) == false) { if (event !in events) {
pendingEvents[pendingEventsKey]?.put(timelineId, ArrayList()) Timber.v("## addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
} events.add(event)
if (pendingEvents[pendingEventsKey]?.get(timelineId)?.contains(event) == false) {
Timber.v("## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId)
pendingEvents[pendingEventsKey]?.get(timelineId)?.add(event)
} }
} }
@ -203,7 +197,7 @@ internal class MXMegolmDecryption(private val userId: String,
var keysClaimed: MutableMap<String, String> = HashMap() var keysClaimed: MutableMap<String, String> = HashMap()
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList() val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.sessionId) || TextUtils.isEmpty(roomKeyContent.sessionKey)) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
Timber.e("## onRoomKeyEvent() : Key event is missing fields") Timber.e("## onRoomKeyEvent() : Key event is missing fields")
return return
} }
@ -250,13 +244,6 @@ internal class MXMegolmDecryption(private val userId: String,
keysClaimed = event.getKeysClaimed().toMutableMap() keysClaimed = event.getKeysClaimed().toMutableMap()
} }
if (roomKeyContent.sessionId == null
|| roomKeyContent.sessionKey == null
|| roomKeyContent.roomId == null) {
Timber.e("## invalid roomKeyContent")
return
}
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
roomKeyContent.sessionKey, roomKeyContent.sessionKey,
roomKeyContent.roomId, roomKeyContent.roomId,

View file

@ -18,7 +18,6 @@
package im.vector.matrix.android.internal.crypto.algorithms.megolm package im.vector.matrix.android.internal.crypto.algorithms.megolm
import android.text.TextUtils
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
@ -38,7 +37,6 @@ import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertToUTF8 import im.vector.matrix.android.internal.util.convertToUTF8
import timber.log.Timber import timber.log.Timber
import java.util.*
internal class MXMegolmEncryption( internal class MXMegolmEncryption(
// The id of the room we will be sending to. // The id of the room we will be sending to.
@ -85,7 +83,7 @@ internal class MXMegolmEncryption(
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!! keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
ArrayList(), keysClaimedMap, false) emptyList(), keysClaimedMap, false)
keysBackup.maybeBackupKeys() keysBackup.maybeBackupKeys()
@ -115,10 +113,8 @@ internal class MXMegolmEncryption(
for (deviceId in deviceIds!!) { for (deviceId in deviceIds!!) {
val deviceInfo = devicesInRoom.getObject(userId, deviceId) val deviceInfo = devicesInRoom.getObject(userId, deviceId)
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) { if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
if (!shareMap.containsKey(userId)) { val devices = shareMap.getOrPut(userId) { ArrayList() }
shareMap[userId] = ArrayList() devices.add(deviceInfo)
}
shareMap[userId]!!.add(deviceInfo)
} }
} }
} }
@ -141,21 +137,17 @@ internal class MXMegolmEncryption(
} }
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
val subMap = HashMap<String, List<MXDeviceInfo>>() val subMap = HashMap<String, List<MXDeviceInfo>>()
val userIds = ArrayList<String>()
var devicesCount = 0 var devicesCount = 0
for (userId in devicesByUsers.keys) { for ((userId, devices) in devicesByUsers) {
devicesByUsers[userId]?.let { subMap[userId] = devices
userIds.add(userId) devicesCount += devices.size
subMap[userId] = it
devicesCount += it.size
}
if (devicesCount > 100) { if (devicesCount > 100) {
break break
} }
} }
Timber.v("## shareKey() ; userId $userIds") Timber.v("## shareKey() ; userId ${subMap.keys}")
shareUserDevicesKey(session, subMap) shareUserDevicesKey(session, subMap)
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() } val remainingDevices = devicesByUsers - subMap.keys
shareKey(session, remainingDevices) shareKey(session, remainingDevices)
} }
@ -164,7 +156,6 @@ internal class MXMegolmEncryption(
* *
* @param session the session info * @param session the session info
* @param devicesByUser the devices map * @param devicesByUser the devices map
* @param callback the asynchronous callback
*/ */
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
devicesByUser: Map<String, List<MXDeviceInfo>>) { devicesByUser: Map<String, List<MXDeviceInfo>>) {
@ -210,8 +201,7 @@ internal class MXMegolmEncryption(
continue continue
} }
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
haveTargets = true haveTargets = true
} }
} }
@ -228,9 +218,8 @@ internal class MXMegolmEncryption(
// attempted to share with) rather than the contentMap (those we did // attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key // share with), because we don't want to try to claim a one-time-key
// for dead devices on every message. // for dead devices on every message.
for (userId in devicesByUser.keys) { for ((userId, devicesToShareWith) in devicesByUser) {
val devicesToShareWith = devicesByUser[userId] for ((deviceId) in devicesToShareWith) {
for ((deviceId) in devicesToShareWith!!) {
session.sharedWithDevices.setObject(userId, deviceId, chainIndex) session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
} }
} }
@ -272,7 +261,6 @@ internal class MXMegolmEncryption(
* This method must be called in getDecryptingThreadHandler() thread. * This method must be called in getDecryptingThreadHandler() thread.
* *
* @param userIds the user ids whose devices must be checked. * @param userIds the user ids whose devices must be checked.
* @param callback the asynchronous callback
*/ */
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> { private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> {
// We are happy to use a cached version here: we assume that if we already // We are happy to use a cached version here: we assume that if we already
@ -304,7 +292,7 @@ internal class MXMegolmEncryption(
continue continue
} }
if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) { if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) {
// Don't bother sending to ourself // Don't bother sending to ourself
continue continue
} }

View file

@ -18,7 +18,6 @@
package im.vector.matrix.android.internal.crypto.algorithms.olm package im.vector.matrix.android.internal.crypto.algorithms.olm
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.DeviceListManager
@ -28,7 +27,6 @@ import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import java.util.*
internal class MXOlmEncryption( internal class MXOlmEncryption(
private var roomId: String, private var roomId: String,
@ -49,7 +47,7 @@ internal class MXOlmEncryption(
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
for (device in devices) { for (device in devices) {
val key = device.identityKey() val key = device.identityKey()
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) { if (key == olmDevice.deviceCurve25519Key) {
// Don't bother setting up session to ourself // Don't bother setting up session to ourself
continue continue
} }
@ -61,13 +59,14 @@ internal class MXOlmEncryption(
} }
} }
val messageMap = HashMap<String, Any>() val messageMap = mapOf(
messageMap["room_id"] = roomId "room_id" to roomId,
messageMap["type"] = eventType "type" to eventType,
messageMap["content"] = eventContent "content" to eventContent
)
messageEncrypter.encryptMessage(messageMap, deviceInfos) messageEncrypter.encryptMessage(messageMap, deviceInfos)
return messageMap.toContent()!! return messageMap.toContent()
} }
/** /**

View file

@ -21,7 +21,6 @@ import android.os.Looper
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
@ -50,6 +49,7 @@ import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
@ -58,6 +58,7 @@ import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.awaitCallback
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -77,6 +78,7 @@ import kotlin.random.Random
@SessionScope @SessionScope
internal class KeysBackup @Inject constructor( internal class KeysBackup @Inject constructor(
@UserId private val userId: String,
private val credentials: Credentials, private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
@ -142,8 +144,8 @@ internal class KeysBackup @Inject constructor(
progressListener: ProgressListener?, progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>) { callback: MatrixCallback<MegolmBackupCreationInfo>) {
GlobalScope.launch(coroutineDispatchers.main) { GlobalScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.crypto) {
Try {
val olmPkDecryption = OlmPkDecryption() val olmPkDecryption = OlmPkDecryption()
val megolmBackupAuthData = MegolmBackupAuthData() val megolmBackupAuthData = MegolmBackupAuthData()
@ -375,8 +377,6 @@ internal class KeysBackup @Inject constructor(
*/ */
@WorkerThread @WorkerThread
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust { private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
val myUserId = credentials.userId
val keysBackupVersionTrust = KeysBackupVersionTrust() val keysBackupVersionTrust = KeysBackupVersionTrust()
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
@ -388,13 +388,13 @@ internal class KeysBackup @Inject constructor(
return keysBackupVersionTrust return keysBackupVersionTrust
} }
val mySigs = authData.signatures?.get(myUserId) val mySigs = authData.signatures?.get(userId)
if (mySigs.isNullOrEmpty()) { if (mySigs.isNullOrEmpty()) {
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
return keysBackupVersionTrust return keysBackupVersionTrust
} }
for (keyId in mySigs.keys) { for ((keyId, mySignature) in mySigs) {
// XXX: is this how we're supposed to get the device id? // XXX: is this how we're supposed to get the device id?
var deviceId: String? = null var deviceId: String? = null
val components = keyId.split(":") val components = keyId.split(":")
@ -403,7 +403,7 @@ internal class KeysBackup @Inject constructor(
} }
if (deviceId != null) { if (deviceId != null) {
val device = cryptoStore.getUserDevice(deviceId, myUserId) val device = cryptoStore.getUserDevice(deviceId, userId)
var isSignatureValid = false var isSignatureValid = false
if (device == null) { if (device == null) {
@ -412,7 +412,7 @@ internal class KeysBackup @Inject constructor(
val fingerprint = device.fingerprint() val fingerprint = device.fingerprint()
if (fingerprint != null) { if (fingerprint != null) {
try { try {
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySigs[keyId] as String) olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
isSignatureValid = true isSignatureValid = true
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
@ -450,10 +450,8 @@ internal class KeysBackup @Inject constructor(
} else { } else {
GlobalScope.launch(coroutineDispatchers.main) { GlobalScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
val myUserId = credentials.userId
// Get current signatures, or create an empty set // Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
?: HashMap() ?: HashMap()
if (trust) { if (trust) {
@ -462,7 +460,7 @@ internal class KeysBackup @Inject constructor(
val deviceSignatures = objectSigner.signObject(canonicalJson) val deviceSignatures = objectSigner.signObject(canonicalJson)
deviceSignatures[myUserId]?.forEach { entry -> deviceSignatures[userId]?.forEach { entry ->
myUserSignatures[entry.key] = entry.value myUserSignatures[entry.key] = entry.value
} }
} else { } else {
@ -478,7 +476,7 @@ internal class KeysBackup @Inject constructor(
val newMegolmBackupAuthData = authData.copy() val newMegolmBackupAuthData = authData.copy()
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap() val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
newSignatures[myUserId] = myUserSignatures newSignatures[userId] = myUserSignatures
newMegolmBackupAuthData.signatures = newSignatures newMegolmBackupAuthData.signatures = newSignatures
@ -617,8 +615,8 @@ internal class KeysBackup @Inject constructor(
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
GlobalScope.launch(coroutineDispatchers.main) { GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) { runCatching {
Try<OlmPkDecryption> { val decryption = withContext(coroutineDispatchers.crypto) {
// Check if the recovery is valid before going any further // Check if the recovery is valid before going any further
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
@ -626,36 +624,27 @@ internal class KeysBackup @Inject constructor(
} }
// Get a PK decryption instance // Get a PK decryption instance
val decryption = pkDecryptionFromRecoveryKey(recoveryKey) pkDecryptionFromRecoveryKey(recoveryKey)
}
if (decryption == null) { if (decryption == null) {
// This should not happen anymore // This should not happen anymore
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
throw InvalidParameterException("Invalid recovery key") throw InvalidParameterException("Invalid recovery key")
} }
decryption
}
}.fold(
{
callback.onFailure(it)
},
{ decryption ->
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
// Get backed up keys from the homeserver // Get backed up keys from the homeserver
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> { val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
override fun onSuccess(data: KeysBackupData) {
GlobalScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.crypto) {
val importRoomKeysResult = withContext(coroutineDispatchers.crypto) {
val sessionsData = ArrayList<MegolmSessionData>() val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data // Restore that data
var sessionsFromHsCount = 0 var sessionsFromHsCount = 0
for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) { for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) { for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
sessionsFromHsCount++ sessionsFromHsCount++
val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!!
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
sessionData?.let { sessionData?.let {
@ -694,17 +683,7 @@ internal class KeysBackup @Inject constructor(
result result
} }
}.foldToCallback(callback)
callback.onSuccess(importRoomKeysResult)
}
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
}
)
} }
} }
@ -717,7 +696,7 @@ internal class KeysBackup @Inject constructor(
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
GlobalScope.launch(coroutineDispatchers.main) { GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) { runCatching {
val progressListener = if (stepProgressListener != null) { val progressListener = if (stepProgressListener != null) {
object : ProgressListener { object : ProgressListener {
override fun onProgress(progress: Int, total: Int) { override fun onProgress(progress: Int, total: Int) {
@ -730,22 +709,18 @@ internal class KeysBackup @Inject constructor(
null null
} }
Try { val recoveryKey = withContext(coroutineDispatchers.crypto) {
recoveryKeyFromPassword(password, keysBackupVersion, progressListener) recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
} }
}.fold(
{
callback.onFailure(it)
},
{ recoveryKey ->
if (recoveryKey == null) { if (recoveryKey == null) {
Timber.v("backupKeys: Invalid configuration") Timber.v("backupKeys: Invalid configuration")
callback.onFailure(IllegalStateException("Invalid configuration")) throw IllegalStateException("Invalid configuration")
} else { } else {
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback) awaitCallback<ImportRoomKeysResult> {
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
} }
} }
) }.foldToCallback(callback)
} }
} }
@ -753,60 +728,26 @@ internal class KeysBackup @Inject constructor(
* Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable
* parameters and always returns a KeysBackupData object through the Callback * parameters and always returns a KeysBackupData object through the Callback
*/ */
private fun getKeys(sessionId: String?, private suspend fun getKeys(sessionId: String?,
roomId: String?, roomId: String?,
version: String, version: String): KeysBackupData {
callback: MatrixCallback<KeysBackupData>) { return if (roomId != null && sessionId != null) {
if (roomId != null && sessionId != null) {
// Get key for the room and for the session // Get key for the room and for the session
getRoomSessionDataTask val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) {
this.callback = object : MatrixCallback<KeyBackupData> {
override fun onSuccess(data: KeyBackupData) {
// Convert to KeysBackupData // Convert to KeysBackupData
val keysBackupData = KeysBackupData() KeysBackupData(mutableMapOf(
keysBackupData.roomIdToRoomKeysBackupData = HashMap() roomId to RoomKeysBackupData(mutableMapOf(
val roomKeysBackupData = RoomKeysBackupData() sessionId to data
roomKeysBackupData.sessionIdToKeyBackupData = HashMap() ))
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data ))
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData
callback.onSuccess(keysBackupData)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
} else if (roomId != null) { } else if (roomId != null) {
// Get all keys for the room // Get all keys for the room
getRoomSessionsDataTask val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
.configureWith(GetRoomSessionsDataTask.Params(roomId, version)) {
this.callback = object : MatrixCallback<RoomKeysBackupData> {
override fun onSuccess(data: RoomKeysBackupData) {
// Convert to KeysBackupData // Convert to KeysBackupData
val keysBackupData = KeysBackupData() KeysBackupData(mutableMapOf(roomId to data))
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data
callback.onSuccess(keysBackupData)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
} else { } else {
// Get all keys // Get all keys
getSessionsDataTask getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
.configureWith(GetSessionsDataTask.Params(version)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
} }
@ -1411,5 +1352,5 @@ internal class KeysBackup @Inject constructor(
* DEBUG INFO * DEBUG INFO
* ========================================================================================== */ * ========================================================================================== */
override fun toString() = "KeysBackup for ${credentials.userId}" override fun toString() = "KeysBackup for $userId"
} }

View file

@ -17,13 +17,11 @@
package im.vector.matrix.android.internal.crypto.model package im.vector.matrix.android.internal.crypto.model
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.MegolmSessionData import im.vector.matrix.android.internal.crypto.MegolmSessionData
import org.matrix.olm.OlmInboundGroupSession import org.matrix.olm.OlmInboundGroupSession
import timber.log.Timber import timber.log.Timber
import java.io.Serializable import java.io.Serializable
import java.util.*
/** /**
* This class adds more context to a OlmInboundGroupSession object. * This class adds more context to a OlmInboundGroupSession object.
@ -91,7 +89,7 @@ class OlmInboundGroupSessionWrapper : Serializable {
try { try {
olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!)
if (!TextUtils.equals(olmInboundGroupSession!!.sessionIdentifier(), megolmSessionData.sessionId)) { if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) {
throw Exception("Mismatched group session Id") throw Exception("Mismatched group session Id")
} }

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto.store.db package im.vector.matrix.android.internal.crypto.store.db
import android.text.TextUtils
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
@ -101,8 +100,8 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
// Check credentials // Check credentials
// The device id may not have been provided in credentials. // The device id may not have been provided in credentials.
// Check it only if provided, else trust the stored one. // Check it only if provided, else trust the stored one.
if (!TextUtils.equals(currentMetadata.userId, credentials.userId) if (currentMetadata.userId != credentials.userId
|| (credentials.deviceId != null && !TextUtils.equals(credentials.deviceId, currentMetadata.deviceId))) { || (credentials.deviceId != null && credentials.deviceId != currentMetadata.deviceId)) {
Timber.w("## open() : Credentials do not match, close this store and delete data") Timber.w("## open() : Credentials do not match, close this store and delete data")
deleteAll = true deleteAll = true
currentMetadata = null currentMetadata = null

View file

@ -44,12 +44,9 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private
} }
val map = MXUsersDevicesMap<MXKey>() val map = MXUsersDevicesMap<MXKey>()
keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> keysClaimResponse.oneTimeKeys?.let { oneTimeKeys ->
for (userId in oneTimeKeys.keys) { for ((userId, mapByUserId) in oneTimeKeys) {
val mapByUserId = oneTimeKeys[userId] for ((deviceId, deviceKey) in mapByUserId) {
val mxKey = MXKey.from(deviceKey)
if (mapByUserId != null) {
for (deviceId in mapByUserId.keys) {
val mxKey = MXKey.from(mapByUserId[deviceId])
if (mxKey != null) { if (mxKey != null) {
map.setObject(userId, deviceId, mxKey) map.setObject(userId, deviceId, mxKey)
@ -59,7 +56,6 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private
} }
} }
} }
}
return map return map
} }
} }

View file

@ -16,13 +16,11 @@
package im.vector.matrix.android.internal.crypto.tasks package im.vector.matrix.android.internal.crypto.tasks
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody
import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> { internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> {
@ -37,19 +35,13 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(private val crypt
: DownloadKeysForUsersTask { : DownloadKeysForUsersTask {
override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse {
val downloadQuery = HashMap<String, Map<String, Any>>() val downloadQuery = params.userIds?.associateWith { emptyMap<String, Any>() }.orEmpty()
if (null != params.userIds) {
for (userId in params.userIds) {
downloadQuery[userId] = HashMap()
}
}
val body = KeysQueryBody( val body = KeysQueryBody(
deviceKeys = downloadQuery deviceKeys = downloadQuery
) )
if (!TextUtils.isEmpty(params.token)) { if (!params.token.isNullOrEmpty()) {
body.token = params.token body.token = params.token
} }

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto.tasks package im.vector.matrix.android.internal.crypto.tasks
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
@ -37,7 +36,7 @@ internal class DefaultSetDeviceNameTask @Inject constructor(private val cryptoAp
override suspend fun execute(params: SetDeviceNameTask.Params) { override suspend fun execute(params: SetDeviceNameTask.Params) {
val body = UpdateDeviceInfoBody( val body = UpdateDeviceInfoBody(
displayName = if (TextUtils.isEmpty(params.deviceName)) "" else params.deviceName displayName = params.deviceName
) )
return executeRequest { return executeRequest {
apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body)

View file

@ -124,7 +124,7 @@ internal fun ChunkEntity.add(roomId: String,
backwardsDisplayIndex = currentDisplayIndex backwardsDisplayIndex = currentDisplayIndex
} }
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.getClearType())) { if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
currentStateIndex += 1 currentStateIndex += 1
forwardsStateIndex = currentStateIndex forwardsStateIndex = currentStateIndex
} else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) { } else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {

View file

@ -37,7 +37,7 @@ internal object EventMapper {
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
eventEntity.prevContent = ContentMapper.map(resolvedPrevContent) eventEntity.prevContent = ContentMapper.map(resolvedPrevContent)
eventEntity.stateKey = event.stateKey eventEntity.stateKey = event.stateKey
eventEntity.type = event.getClearType() eventEntity.type = event.type
eventEntity.sender = event.senderId eventEntity.sender = event.senderId
eventEntity.originServerTs = event.originServerTs eventEntity.originServerTs = event.originServerTs
eventEntity.redacts = event.redacts eventEntity.redacts = event.redacts

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import android.content.Context import android.content.Context
import android.text.TextUtils
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.di.MatrixScope import im.vector.matrix.android.internal.di.MatrixScope
import timber.log.Timber import timber.log.Timber
@ -60,10 +59,10 @@ internal class UserAgentHolder @Inject constructor(private val context: Context)
Timber.e(e, "## initUserAgent() : failed") Timber.e(e, "## initUserAgent() : failed")
} }
var systemUserAgent = System.getProperty("http.agent") val systemUserAgent = System.getProperty("http.agent")
// cannot retrieve the application version // cannot retrieve the application version
if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(appVersion)) { if (appName.isEmpty() || appVersion.isEmpty()) {
if (null == systemUserAgent) { if (null == systemUserAgent) {
userAgent = "Java" + System.getProperty("java.version") userAgent = "Java" + System.getProperty("java.version")
} }

View file

@ -75,9 +75,7 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
private fun updateState(key: String, state: ContentUploadStateTracker.State) { private fun updateState(key: String, state: ContentUploadStateTracker.State) {
states[key] = state states[key] = state
mainHandler.post { mainHandler.post {
listeners[key]?.also { listeners -> listeners[key]?.forEach { it.onUpdate(state) }
listeners.forEach { it.onUpdate(state) }
}
} }
} }
} }

View file

@ -65,13 +65,11 @@ internal class DefaultGetGroupDataTask @Inject constructor(
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: "" groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: ""
val roomIds = groupRooms.rooms.map { it.roomId }
groupSummaryEntity.roomIds.clear() groupSummaryEntity.roomIds.clear()
groupSummaryEntity.roomIds.addAll(roomIds) groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId }
val userIds = groupUsers.users.map { it.userId }
groupSummaryEntity.userIds.clear() groupSummaryEntity.userIds.clear()
groupSummaryEntity.userIds.addAll(userIds) groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
groupSummaryEntity.membership = when (groupSummary.user?.membership) { groupSummaryEntity.membership = when (groupSummary.user?.membership) {
Membership.JOIN.value -> Membership.JOIN Membership.JOIN.value -> Membership.JOIN

View file

@ -50,19 +50,15 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
defaultPushRuleService.dispatchRoomJoined(it) defaultPushRuleService.dispatchRoomJoined(it)
} }
val newJoinEvents = params.syncResponse.join val newJoinEvents = params.syncResponse.join
.map { entries -> .mapNotNull { (key, value) ->
entries.value.timeline?.events?.map { it.copy(roomId = entries.key) } value.timeline?.events?.map { it.copy(roomId = key) }
} }
.fold(emptyList<Event>(), { acc, next -> .flatten()
acc + (next ?: emptyList())
})
val inviteEvents = params.syncResponse.invite val inviteEvents = params.syncResponse.invite
.map { entries -> .mapNotNull { (key, value) ->
entries.value.inviteState?.events?.map { it.copy(roomId = entries.key) } value.inviteState?.events?.map { it.copy(roomId = key) }
} }
.fold(emptyList<Event>(), { acc, next -> .flatten()
acc + (next ?: emptyList())
})
val allEvents = (newJoinEvents + inviteEvents).filter { event -> val allEvents = (newJoinEvents + inviteEvents).filter { event ->
when (event.type) { when (event.type) {
EventType.MESSAGE, EventType.MESSAGE,
@ -84,16 +80,12 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
} }
val allRedactedEvents = params.syncResponse.join val allRedactedEvents = params.syncResponse.join
.map { entries -> .asSequence()
entries.value.timeline?.events?.filter { .mapNotNull { (_, value) -> value.timeline?.events }
it.type == EventType.REDACTION .flatten()
} .filter { it.type == EventType.REDACTION }
.orEmpty()
.mapNotNull { it.redacts } .mapNotNull { it.redacts }
} .toList()
.fold(emptyList<String>(), { acc, next ->
acc + next
})
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events") Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
@ -107,18 +99,11 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? { private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? {
// TODO This should be injected // TODO This should be injected
val conditionResolver = DefaultConditionResolver(event, roomService, userId) val conditionResolver = DefaultConditionResolver(event, roomService, userId)
rules.filter { it.enabled }.forEach { rule -> return rules.firstOrNull { rule ->
val isFullfilled = rule.conditions?.map {
it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false
}?.fold(true/*A rule with no conditions always matches*/, { acc, next ->
// All conditions must hold true for an event in order to apply the action for the event. // All conditions must hold true for an event in order to apply the action for the event.
acc && next rule.enabled && rule.conditions?.all {
}) ?: false it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false
} ?: false
if (isFullfilled) {
return rule
} }
} }
return null
}
} }

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendService
@ -44,15 +45,17 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val sendService: SendService, private val sendService: SendService,
private val draftService: DraftService, private val draftService: DraftService,
private val stateService: StateService, private val stateService: StateService,
private val reportingService: ReportingService,
private val readService: ReadService, private val readService: ReadService,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val relationService: RelationService, private val relationService: RelationService,
private val roomMembersService: MembershipService private val roomMembersService: MembershipService) :
) : Room, Room,
TimelineService by timelineService, TimelineService by timelineService,
SendService by sendService, SendService by sendService,
DraftService by draftService, DraftService by draftService,
StateService by stateService, StateService by stateService,
ReportingService by reportingService,
ReadService by readService, ReadService by readService,
RelationService by relationService, RelationService by relationService,
MembershipService by roomMembersService { MembershipService by roomMembersService {

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm import io.realm.Realm
@ -41,6 +42,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val roomSummaryMapper: RoomSummaryMapper, private val roomSummaryMapper: RoomSummaryMapper,
private val createRoomTask: CreateRoomTask, private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val roomFactory: RoomFactory, private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService { private val taskExecutor: TaskExecutor) : RoomService {
@ -80,4 +82,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable {
return markAllRoomsReadTask
.configureWith(MarkAllRoomsReadTask.Params(roomIds)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
} }

View file

@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
@ -245,4 +246,16 @@ internal interface RoomAPI {
@Path("eventId") parent_id: String, @Path("eventId") parent_id: String,
@Body reason: Map<String, String> @Body reason: Map<String, String>
): Call<SendResponse> ): Call<SendResponse>
/**
* Reports an event as inappropriate to the server, which may then notify the appropriate people.
*
* @param roomId the room id
* @param eventId the event to report content
* @param body body containing score and reason
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}")
fun reportContent(@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Body body: ReportContentBody): Call<Unit>
} }

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportingService
import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.state.DefaultStateService import im.vector.matrix.android.internal.session.room.state.DefaultStateService
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
@ -40,6 +41,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val sendServiceFactory: DefaultSendService.Factory, private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory, private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory, private val stateServiceFactory: DefaultStateService.Factory,
private val reportingServiceFactory: DefaultReportingService.Factory,
private val readServiceFactory: DefaultReadService.Factory, private val readServiceFactory: DefaultReadService.Factory,
private val relationServiceFactory: DefaultRelationService.Factory, private val relationServiceFactory: DefaultRelationService.Factory,
private val membershipServiceFactory: DefaultMembershipService.Factory) : private val membershipServiceFactory: DefaultMembershipService.Factory) :
@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
sendServiceFactory.create(roomId), sendServiceFactory.create(roomId),
draftServiceFactory.create(roomId), draftServiceFactory.create(roomId),
stateServiceFactory.create(roomId), stateServiceFactory.create(roomId),
reportingServiceFactory.create(roomId),
readServiceFactory.create(roomId), readServiceFactory.create(roomId),
cryptoService, cryptoService,
relationServiceFactory.create(roomId), relationServiceFactory.create(roomId),

View file

@ -40,22 +40,16 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.Default
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.relation.DefaultFetchEditHistoryTask import im.vector.matrix.android.internal.session.room.relation.*
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.timeline.* import im.vector.matrix.android.internal.session.room.timeline.*
import im.vector.matrix.android.internal.session.room.timeline.ClearUnlinkedEventsTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import retrofit2.Retrofit import retrofit2.Retrofit
@Module @Module
@ -110,6 +104,9 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
@Binds
abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
@Binds @Binds
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
@ -119,6 +116,9 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
@Binds
abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
@Binds @Binds
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask

View file

@ -132,8 +132,7 @@ internal class RoomMembers(private val realm: Realm,
.findAll() .findAll()
.map { it.asDomain() } .map { it.asDomain() }
.associateBy { it.stateKey!! } .associateBy { it.stateKey!! }
.mapValues { it.value.content.toModel<RoomMember>()!! } .filterValues { predicate(it.content.toModel<RoomMember>()!!) }
.filterValues { predicate(it) }
.keys .keys
.toList() .toList()
} }

View file

@ -0,0 +1,35 @@
/*
* 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.matrix.android.internal.session.room.read
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface MarkAllRoomsReadTask : Task<MarkAllRoomsReadTask.Params, Unit> {
data class Params(
val roomIds: List<String>
)
}
internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask) : MarkAllRoomsReadTask {
override suspend fun execute(params: MarkAllRoomsReadTask.Params) {
params.roomIds.forEach { roomId ->
readMarkersTask.execute(SetReadMarkersTask.Params(roomId, markAllAsRead = true))
}
}
}

View file

@ -65,7 +65,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
fun create(roomId: String): RelationService fun create(roomId: String): RelationService
} }
override fun sendReaction(reaction: String, targetEventId: String): Cancelable { override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
@ -75,13 +75,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return CancelableWork(context, sendRelationWork.id) return CancelableWork(context, sendRelationWork.id)
} }
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
val params = FindReactionEventForUndoTask.Params( val params = FindReactionEventForUndoTask.Params(
roomId, roomId,
targetEventId, targetEventId,
reaction, reaction
myUserId
) )
// TODO We should avoid using MatrixCallback internally
val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> { val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
override fun onSuccess(data: FindReactionEventForUndoTask.Result) { override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
if (data.redactEventId == null) { if (data.redactEventId == null) {
@ -89,7 +89,6 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
// TODO? // TODO?
} }
data.redactEventId?.let { toRedact -> data.redactEventId?.let { toRedact ->
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also { val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
saveLocalEcho(it) saveLocalEcho(it)
} }
@ -99,7 +98,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
} }
} }
} }
findReactionEventForUndoTask return findReactionEventForUndoTask
.configureWith(params) { .configureWith(params) {
this.retryCount = Int.MAX_VALUE this.retryCount = Int.MAX_VALUE
this.callback = callback this.callback = callback

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import io.realm.Realm import io.realm.Realm
import javax.inject.Inject import javax.inject.Inject
@ -29,8 +30,7 @@ internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoT
data class Params( data class Params(
val roomId: String, val roomId: String,
val eventId: String, val eventId: String,
val reaction: String, val reaction: String
val myUserId: String
) )
data class Result( data class Result(
@ -38,33 +38,33 @@ internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoT
) )
} }
internal class DefaultFindReactionEventForUndoTask @Inject constructor(private val monarchy: Monarchy) : FindReactionEventForUndoTask { internal class DefaultFindReactionEventForUndoTask @Inject constructor(private val monarchy: Monarchy,
@UserId private val userId: String) : FindReactionEventForUndoTask {
override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result { override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result {
val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId getReactionToRedact(realm, params.reaction, params.eventId)?.eventId
} }
return FindReactionEventForUndoTask.Result(eventId) return FindReactionEventForUndoTask.Result(eventId)
} }
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? { private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? {
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null
if (summary != null) {
summary.reactionsSummary.where() val rase = summary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
.findFirst()?.let { .findFirst() ?: return null
// want to find the event orignated by me! // want to find the event orignated by me!
it.sourceEvents.forEach { return rase.sourceEvents
.asSequence()
.mapNotNull {
// find source event // find source event
EventEntity.where(realm, it).findFirst()?.let { eventEntity -> EventEntity.where(realm, it).findFirst()
}
.firstOrNull { eventEntity ->
// is it mine? // is it mine?
if (eventEntity.sender == userId) { eventEntity.sender == userId
return eventEntity
} }
} }
} }
}
}
return null
}
}

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import io.realm.Realm import io.realm.Realm
import javax.inject.Inject import javax.inject.Inject
@ -30,8 +31,7 @@ internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params
val roomId: String, val roomId: String,
val eventId: String, val eventId: String,
val reaction: String, val reaction: String,
val oppositeReaction: String, val oppositeReaction: String
val myUserId: String
) )
data class Result( data class Result(
@ -40,17 +40,18 @@ internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params
) )
} }
internal class DefaultUpdateQuickReactionTask @Inject constructor(private val monarchy: Monarchy) : UpdateQuickReactionTask { internal class DefaultUpdateQuickReactionTask @Inject constructor(private val monarchy: Monarchy,
@UserId private val userId: String) : UpdateQuickReactionTask {
override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result { override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result {
var res: Pair<String?, List<String>?>? = null var res: Pair<String?, List<String>?>? = null
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId) res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId)
} }
return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList())
} }
private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair<String?, List<String>?> { private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair<String?, List<String>?> {
// the emoji reaction has been selected, we need to check if we have reacted it or not // the emoji reaction has been selected, we need to check if we have reacted it or not
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
?: return Pair(reaction, null) ?: return Pair(reaction, null)
@ -68,7 +69,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull { val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
// find source event // find source event
val entity = EventEntity.where(realm, it).findFirst() val entity = EventEntity.where(realm, it).findFirst()
if (entity?.sender == myUserId) entity.eventId else null if (entity?.sender == userId) entity.eventId else null
} }
return Pair(reaction, toRedact) return Pair(reaction, toRedact)
} else { } else {
@ -77,7 +78,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
val toRedact = aggregationForReaction.sourceEvents.mapNotNull { val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
// find source event // find source event
val entity = EventEntity.where(realm, it).findFirst() val entity = EventEntity.where(realm, it).findFirst()
if (entity?.sender == myUserId) entity.eventId else null if (entity?.sender == userId) entity.eventId else null
} }
return Pair(null, toRedact) return Pair(null, toRedact)
} }

View file

@ -0,0 +1,46 @@
/*
* 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.matrix.android.internal.session.room.reporting
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
private val taskExecutor: TaskExecutor,
private val reportContentTask: ReportContentTask
) : ReportingService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): ReportingService
}
override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable {
val params = ReportContentTask.Params(roomId, eventId, score, reason)
return reportContentTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.matrix.android.internal.session.room.reporting
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class ReportContentBody(
/**
* Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive.
*/
@Json(name = "score") val score: Int,
/**
* Required. The reason the content is being reported. May be blank.
*/
@Json(name = "reason") val reason: String
)

View file

@ -0,0 +1,39 @@
/*
* 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.matrix.android.internal.session.room.reporting
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface ReportContentTask : Task<ReportContentTask.Params, Unit> {
data class Params(
val roomId: String,
val eventId: String,
val score: Int,
val reason: String
)
}
internal class DefaultReportContentTask @Inject constructor(private val roomAPI: RoomAPI) : ReportContentTask {
override suspend fun execute(params: ReportContentTask.Params) {
return executeRequest {
apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason))
}
}
}

View file

@ -38,24 +38,24 @@ internal class TimelineEventDecryptor(
private val newSessionListener = object : NewSessionListener { private val newSessionListener = object : NewSessionListener {
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
synchronized(unknownSessionsFailure) { synchronized(unknownSessionsFailure) {
val toDecryptAgain = ArrayList<String>() unknownSessionsFailure[sessionId]
unknownSessionsFailure[sessionId]?.let { eventIds -> .orEmpty()
toDecryptAgain.addAll(eventIds) .toList()
} .also {
if (toDecryptAgain.isNotEmpty()) {
unknownSessionsFailure[sessionId]?.clear() unknownSessionsFailure[sessionId]?.clear()
toDecryptAgain.forEach { }
}.forEach {
requestDecryption(it) requestDecryption(it)
} }
} }
} }
}
}
private var executor: ExecutorService? = null private var executor: ExecutorService? = null
private val existingRequests = HashSet<String>() // Set of eventIds which are currently decrypting
private val unknownSessionsFailure = HashMap<String, MutableList<String>>() private val existingRequests = mutableSetOf<String>()
// sessionId -> list of eventIds
private val unknownSessionsFailure = mutableMapOf<String, MutableList<String>>()
fun start() { fun start() {
executor = Executors.newSingleThreadExecutor() executor = Executors.newSingleThreadExecutor()
@ -66,26 +66,30 @@ internal class TimelineEventDecryptor(
cryptoService.removeSessionListener(newSessionListener) cryptoService.removeSessionListener(newSessionListener)
executor?.shutdownNow() executor?.shutdownNow()
executor = null executor = null
synchronized(unknownSessionsFailure) {
unknownSessionsFailure.clear() unknownSessionsFailure.clear()
}
synchronized(existingRequests) {
existingRequests.clear() existingRequests.clear()
} }
}
fun requestDecryption(eventId: String) { fun requestDecryption(eventId: String) {
synchronized(existingRequests) { synchronized(unknownSessionsFailure) {
if (existingRequests.contains(eventId)) { for (eventIds in unknownSessionsFailure.values) {
return Unit.also { if (eventId in eventIds) {
Timber.d("Skip Decryption request for event $eventId, already requested") Timber.d("Skip Decryption request for event $eventId, unknown session")
return
} }
} }
}
synchronized(existingRequests) {
if (eventId in existingRequests) {
Timber.d("Skip Decryption request for event $eventId, already requested")
return
}
existingRequests.add(eventId) existingRequests.add(eventId)
} }
synchronized(unknownSessionsFailure) {
unknownSessionsFailure.values.forEach {
if (it.contains(eventId)) return@synchronized Unit.also {
Timber.d("Skip Decryption request for event $eventId, unknown session")
}
}
}
executor?.execute { executor?.execute {
Realm.getInstance(realmConfiguration).use { realm -> Realm.getInstance(realmConfiguration).use { realm ->
processDecryptRequest(eventId, realm) processDecryptRequest(eventId, realm)
@ -107,7 +111,7 @@ internal class TimelineEventDecryptor(
eventEntity.setDecryptionResult(result) eventEntity.setDecryptionResult(result)
} }
} catch (e: MXCryptoError) { } catch (e: MXCryptoError) {
Timber.v("Failed to decrypt event $eventId $e") Timber.v(e, "Failed to decrypt event $eventId")
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// Keep track of unknown sessions to automatically try to decrypt on new session // Keep track of unknown sessions to automatically try to decrypt on new session
realm.executeTransaction { realm.executeTransaction {
@ -116,10 +120,7 @@ internal class TimelineEventDecryptor(
event.content?.toModel<EncryptedEventContent>()?.let { content -> event.content?.toModel<EncryptedEventContent>()?.let { content ->
content.sessionId?.let { sessionId -> content.sessionId?.let { sessionId ->
synchronized(unknownSessionsFailure) { synchronized(unknownSessionsFailure) {
val list = unknownSessionsFailure[sessionId] val list = unknownSessionsFailure.getOrPut(sessionId) { ArrayList() }
?: ArrayList<String>().also {
unknownSessionsFailure[sessionId] = it
}
list.add(eventId) list.add(eventId)
} }
} }

View file

@ -461,9 +461,9 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey) inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher) CipherOutputStream(outputStream, inputCipher).use {
cipherOutputStream.write(secret) it.write(secret)
cipherOutputStream.close() }
return outputStream.toByteArray() return outputStream.toByteArray()
} }

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.sync package im.vector.matrix.android.internal.session.sync
import android.text.TextUtils
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -41,9 +40,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt()) initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt())
// Decrypt event if necessary // Decrypt event if necessary
decryptEvent(event, null) decryptEvent(event, null)
if (TextUtils.equals(event.getClearType(), EventType.MESSAGE) if (event.getClearType() == EventType.MESSAGE
&& event.getClearContent()?.toModel<MessageContent>()?.type == "m.bad.encrypted") { && event.getClearContent()?.toModel<MessageContent>()?.type == "m.bad.encrypted") {
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.content) Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
} else { } else {
sasVerificationService.onToDeviceEvent(event) sasVerificationService.onToDeviceEvent(event)
cryptoService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event)

View file

@ -25,7 +25,7 @@ import io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// the receipts dictionnaries // the receipts dictionaries
// key : $EventId // key : $EventId
// value : dict key $UserId // value : dict key $UserId
// value dict key ts // value dict key ts

View file

@ -31,7 +31,8 @@ import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import timber.log.Timber import timber.log.Timber
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.util.* import java.util.Timer
import java.util.TimerTask
/** /**
* Can execute periodic sync task. * Can execute periodic sync task.

View file

@ -31,18 +31,10 @@ internal class DirectChatsHelper @Inject constructor(@SessionDatabase
*/ */
fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> { fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> {
return Realm.getInstance(realmConfiguration).use { realm -> return Realm.getInstance(realmConfiguration).use { realm ->
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm) RoomSummaryEntity.getDirectRooms(realm)
val directChatsMap = mutableMapOf<String, MutableList<String>>() .asSequence()
for (directRoom in currentDirectRooms) { .filter { it.roomId != filterRoomId && it.directUserId != null }
if (directRoom.roomId == filterRoomId) continue .groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId })
val directUserId = directRoom.directUserId ?: continue
directChatsMap
.getOrPut(directUserId, { arrayListOf() })
.apply {
add(directRoom.roomId)
}
}
directChatsMap
} }
} }
} }

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.task
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import java.util.* import java.util.UUID
internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS, internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS,
init: (ConfigurableTask.Builder<PARAMS, RESULT>.() -> Unit) = {} init: (ConfigurableTask.Builder<PARAMS, RESULT>.() -> Unit) = {}

View file

@ -59,19 +59,11 @@ object CompatUtil {
private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated" private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated"
private var sSecretKeyAndVersion: SecretKeyAndVersion? = null private var sSecretKeyAndVersion: SecretKeyAndVersion? = null
private var sPrng: SecureRandom? = null
/** /**
* Returns the unique SecureRandom instance shared for all local storage encryption operations. * Returns the unique SecureRandom instance shared for all local storage encryption operations.
*/ */
private val prng: SecureRandom private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() }
get() {
if (sPrng == null) {
sPrng = SecureRandom()
}
return sPrng!!
}
/** /**
* Create a GZIPOutputStream instance * Create a GZIPOutputStream instance

View file

@ -24,12 +24,9 @@ import java.security.MessageDigest
fun String.md5() = try { fun String.md5() = try {
val digest = MessageDigest.getInstance("md5") val digest = MessageDigest.getInstance("md5")
digest.update(toByteArray()) digest.update(toByteArray())
val bytes = digest.digest() digest.digest()
val sb = StringBuilder() .joinToString("") { String.format("%02X", it) }
for (i in bytes.indices) { .toLowerCase()
sb.append(String.format("%02X", bytes[i]))
}
sb.toString().toLowerCase()
} catch (exc: Exception) { } catch (exc: Exception) {
// Should not happen, but just in case // Should not happen, but just in case
hashCode().toString() hashCode().toString()

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
@ -172,7 +173,6 @@ class PushrulesConditionTest {
} }
class MockRoomService() : RoomService { class MockRoomService() : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable { override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
@ -192,9 +192,21 @@ class PushrulesConditionTest {
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return MutableLiveData() return MutableLiveData()
} }
override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
} }
class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room { class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room {
override fun getReadMarkerLive(): LiveData<Optional<String>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun getMyReadReceiptLive(): LiveData<Optional<String>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
@ -242,7 +254,7 @@ class PushrulesConditionTest {
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) { override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
} }
override fun liveTimeLineEvent(eventId: String): LiveData<TimelineEvent> { override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
@ -250,7 +262,7 @@ class PushrulesConditionTest {
return _numberOfJoinedMembers return _numberOfJoinedMembers
} }
override fun liveRoomSummary(): LiveData<RoomSummary> { override fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
@ -330,11 +342,11 @@ class PushrulesConditionTest {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
override fun sendReaction(reaction: String, targetEventId: String): Cancelable { override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String) { override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
@ -347,7 +359,7 @@ class PushrulesConditionTest {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }
override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> { override fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates. TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
} }

View file

@ -281,7 +281,7 @@ dependencies {
// UI // UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha10' implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'me.gujun.android:span:1.7' implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version"

View file

@ -59,7 +59,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.vector_home, menu) menuInflater.inflate(R.menu.home, menu)
return true return true
} }
} }

View file

@ -21,7 +21,6 @@ package im.vector.riotx.gplay.push.fcm
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.TextUtils
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
@ -214,10 +213,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
} }
} else { } else {
if (notifiableEvent is NotifiableMessageEvent) { if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) { if (notifiableEvent.senderName.isNullOrEmpty()) {
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: "" notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
} }
if (TextUtils.isEmpty(notifiableEvent.roomName)) { if (notifiableEvent.roomName.isNullOrEmpty()) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: "" notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
} }
} }

View file

@ -42,6 +42,8 @@ import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
@ -104,12 +106,10 @@ interface ScreenComponent {
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet) fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet) fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet) fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
fun inject(messageMenuFragment: MessageMenuFragment)
fun inject(vectorSettingsActivity: VectorSettingsActivity) fun inject(vectorSettingsActivity: VectorSettingsActivity)
fun inject(createRoomFragment: CreateRoomFragment) fun inject(createRoomFragment: CreateRoomFragment)
@ -136,8 +136,6 @@ interface ScreenComponent {
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
fun inject(quickReactionFragment: QuickReactionFragment)
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity) fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
fun inject(loginActivity: LoginActivity) fun inject(loginActivity: LoginActivity)

View file

@ -18,7 +18,6 @@ package im.vector.riotx.core.dialogs
import android.app.Activity import android.app.Activity
import android.text.Editable import android.text.Editable
import android.text.TextUtils
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -45,11 +44,11 @@ class ExportKeysDialog {
val textWatcher = object : SimpleTextWatcher() { val textWatcher = object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
when { when {
TextUtils.isEmpty(passPhrase1EditText.text) -> { passPhrase1EditText.text.isNullOrEmpty() -> {
exportButton.isEnabled = false exportButton.isEnabled = false
passPhrase2Til.error = null passPhrase2Til.error = null
} }
TextUtils.equals(passPhrase1EditText.text, passPhrase2EditText.text) -> { passPhrase1EditText.text == passPhrase2EditText.text -> {
exportButton.isEnabled = true exportButton.isEnabled = true
passPhrase2Til.error = null passPhrase2Til.error = null
} }

View file

@ -0,0 +1,27 @@
/*
* 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.riotx.core.dialogs
import androidx.annotation.ColorRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import im.vector.riotx.R
fun AlertDialog.withColoredButton(whichButton: Int, @ColorRes color: Int = R.color.vector_error_color): AlertDialog {
getButton(whichButton)?.setTextColor(ContextCompat.getColor(context, color))
return this
}

View file

@ -21,9 +21,7 @@ import android.content.ClipDescription
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.text.TextUtils
import androidx.core.util.PatternsCompat.WEB_URL import androidx.core.util.PatternsCompat.WEB_URL
import java.util.*
/** /**
* Inspired from Riot code: RoomMediaMessage.java * Inspired from Riot code: RoomMediaMessage.java
@ -69,34 +67,28 @@ fun analyseIntent(intent: Intent): List<ExternalIntentData> {
// chrome adds many items when sharing an web page link // chrome adds many items when sharing an web page link
// so, test first the type // so, test first the type
if (TextUtils.equals(intent.type, ClipDescription.MIMETYPE_TEXT_PLAIN)) { if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT) var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
?: intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
if (null == message) {
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
if (null != sequence) {
message = sequence.toString()
}
}
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
if (!TextUtils.isEmpty(subject)) { if (!subject.isNullOrEmpty()) {
if (TextUtils.isEmpty(message)) { if (message.isNullOrEmpty()) {
message = subject message = subject
} else if (WEB_URL.matcher(message!!).matches()) { } else if (WEB_URL.matcher(message).matches()) {
message = subject + "\n" + message message = subject + "\n" + message
} }
} }
if (!TextUtils.isEmpty(message)) { if (!message.isNullOrEmpty()) {
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type)) externalIntentDataList.add(ExternalIntentData.IntentDataText(message, null, intent.type))
return externalIntentDataList return externalIntentDataList
} }
} }
var clipData: ClipData? = null var clipData: ClipData? = null
var mimetypes: MutableList<String>? = null var mimeTypes: List<String>? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
clipData = intent.clipData clipData = intent.clipData
@ -106,41 +98,26 @@ fun analyseIntent(intent: Intent): List<ExternalIntentData> {
if (null != clipData) { if (null != clipData) {
if (null != clipData.description) { if (null != clipData.description) {
if (0 != clipData.description.mimeTypeCount) { if (0 != clipData.description.mimeTypeCount) {
mimetypes = ArrayList() mimeTypes = with(clipData.description) {
List(mimeTypeCount) { getMimeType(it) }
for (i in 0 until clipData.description.mimeTypeCount) {
mimetypes.add(clipData.description.getMimeType(i))
} }
// if the filter is "accept anything" the mimetype does not make sense // if the filter is "accept anything" the mimetype does not make sense
if (1 == mimetypes.size) { if (1 == mimeTypes.size) {
if (mimetypes[0].endsWith("/*")) { if (mimeTypes[0].endsWith("/*")) {
mimetypes = null mimeTypes = null
} }
} }
} }
} }
val count = clipData.itemCount for (i in 0 until clipData.itemCount) {
for (i in 0 until count) {
val item = clipData.getItemAt(i) val item = clipData.getItemAt(i)
var mimetype: String? = null val mimeType = mimeTypes?.getOrElse(i) { mimeTypes[0] }
if (null != mimetypes) {
if (i < mimetypes.size) {
mimetype = mimetypes[i]
} else {
mimetype = mimetypes[0]
}
// uris list is not a valid mimetype // uris list is not a valid mimetype
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) { .takeUnless { it == ClipDescription.MIMETYPE_TEXT_URILIST }
mimetype = null
}
}
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype)) externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimeType))
} }
} else if (null != intent.data) { } else if (null != intent.data) {
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!)) externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))

View file

@ -13,18 +13,23 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home.room.detail.timeline.action package im.vector.riotx.core.platform
import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.MvRxView import com.airbnb.mvrx.MvRxView
import com.airbnb.mvrx.MvRxViewModelStore import com.airbnb.mvrx.MvRxViewModelStore
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.DimensionConverter
import java.util.* import java.util.*
/** /**
@ -37,10 +42,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
private lateinit var screenComponent: ScreenComponent private lateinit var screenComponent: ScreenComponent
final override val mvrxViewId: String by lazy { mvrxPersistedViewId } final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
private var bottomSheetBehavior: BottomSheetBehavior<FrameLayout>? = null
val vectorBaseActivity: VectorBaseActivity by lazy { val vectorBaseActivity: VectorBaseActivity by lazy {
activity as VectorBaseActivity activity as VectorBaseActivity
} }
open val showExpanded = false
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
super.onAttach(context) super.onAttach(context)
@ -57,6 +66,17 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
val dialog = this as? BottomSheetDialog
bottomSheetBehavior = dialog?.behavior
bottomSheetBehavior?.setPeekHeight(DimensionConverter(resources).dpToPx(400), false)
if (showExpanded) {
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
mvrxViewModelStore.saveViewModels(outState) mvrxViewModelStore.saveViewModels(outState)
@ -70,6 +90,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
postInvalidate() postInvalidate()
} }
@CallSuper
override fun invalidate() {
if (showExpanded) {
// Force the bottom sheet to be expanded
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
}
}
protected fun setArguments(args: Parcelable? = null) { protected fun setArguments(args: Parcelable? = null) {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
} }

View file

@ -17,7 +17,6 @@
package im.vector.riotx.core.preference package im.vector.riotx.core.preference
import android.content.Context import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.RadioGroup import android.widget.RadioGroup
@ -84,7 +83,7 @@ class BingRulePreference : VectorPreference {
val ruleStatusIndex: Int val ruleStatusIndex: Int
get() { get() {
if (null != rule) { if (null != rule) {
if (TextUtils.equals(rule!!.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { if (rule!!.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
if (rule!!.shouldNotNotify()) { if (rule!!.shouldNotNotify()) {
return if (rule!!.isEnabled) { return if (rule!!.isEnabled) {
NOTIFICATION_OFF_INDEX NOTIFICATION_OFF_INDEX
@ -143,7 +142,7 @@ class BingRulePreference : VectorPreference {
if (null != this.rule && index != ruleStatusIndex) { if (null != this.rule && index != ruleStatusIndex) {
rule = BingRule(this.rule!!) rule = BingRule(this.rule!!)
if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { if (rule.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
when (index) { when (index) {
NOTIFICATION_OFF_INDEX -> { NOTIFICATION_OFF_INDEX -> {
rule.isEnabled = true rule.isEnabled = true
@ -164,8 +163,8 @@ class BingRulePreference : VectorPreference {
} }
if (NOTIFICATION_OFF_INDEX == index) { if (NOTIFICATION_OFF_INDEX == index) {
if (TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE) if (this.rule!!.kind == BingRule.KIND_UNDERRIDE
|| TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { || rule.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
rule.setNotify(false) rule.setNotify(false)
} else { } else {
rule.isEnabled = false rule.isEnabled = false
@ -173,11 +172,11 @@ class BingRulePreference : VectorPreference {
} else { } else {
rule.isEnabled = true rule.isEnabled = true
rule.setNotify(true) rule.setNotify(true)
rule.setHighlight(!TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE) rule.setHighlight(this.rule!!.kind != BingRule.KIND_UNDERRIDE
&& !TextUtils.equals(rule.ruleId, BingRule.RULE_ID_INVITE_ME) && rule.ruleId != BingRule.RULE_ID_INVITE_ME
&& NOTIFICATION_NOISY_INDEX == index) && NOTIFICATION_NOISY_INDEX == index)
if (NOTIFICATION_NOISY_INDEX == index) { if (NOTIFICATION_NOISY_INDEX == index) {
rule.notificationSound = if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_CALL)) { rule.notificationSound = if (rule.ruleId == BingRule.RULE_ID_CALL) {
BingRule.ACTION_VALUE_RING BingRule.ACTION_VALUE_RING
} else { } else {
BingRule.ACTION_VALUE_DEFAULT BingRule.ACTION_VALUE_DEFAULT

View file

@ -18,7 +18,6 @@ package im.vector.riotx.core.resources
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.text.TextUtils
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import im.vector.riotx.core.utils.getFileExtension import im.vector.riotx.core.utils.getFileExtension
import timber.log.Timber import timber.log.Timber
@ -73,7 +72,7 @@ fun openResource(context: Context, uri: Uri, providedMimetype: String?): Resourc
var mimetype = providedMimetype var mimetype = providedMimetype
try { try {
// if the mime type is not provided, try to find it out // if the mime type is not provided, try to find it out
if (TextUtils.isEmpty(mimetype)) { if (mimetype.isNullOrEmpty()) {
mimetype = context.contentResolver.getType(uri) mimetype = context.contentResolver.getType(uri)
// try to find the mimetype from the filename // try to find the mimetype from the filename

View file

@ -20,7 +20,6 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.text.SpannableString import android.text.SpannableString
import android.text.TextPaint import android.text.TextPaint
import android.text.TextUtils
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.util.AttributeSet import android.util.AttributeSet
@ -168,7 +167,7 @@ class NotificationAreaView @JvmOverloads constructor(
} else { } else {
imageView.setImageResource(R.drawable.scrolldown) imageView.setImageResource(R.drawable.scrolldown)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
if (!TextUtils.isEmpty(state.message)) { if (!state.message.isNullOrEmpty()) {
messageView.text = SpannableString(state.message) messageView.text = SpannableString(state.message)
} }
} }

View file

@ -50,6 +50,7 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) {
uri?.let { uri?.let {
val browserIntent = Intent(Intent.ACTION_VIEW, it).apply { val browserIntent = Intent(Intent.ACTION_VIEW, it).apply {
putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName) putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
putExtra(Browser.EXTRA_CREATE_NEW_TAB, true)
} }
try { try {

View file

@ -17,7 +17,6 @@
package im.vector.riotx.core.utils package im.vector.riotx.core.utils
import android.content.Context import android.content.Context
import android.text.TextUtils
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -60,7 +59,7 @@ private fun logAction(file: File): Boolean {
if (file.isDirectory) { if (file.isDirectory) {
Timber.v(file.toString()) Timber.v(file.toString())
} else { } else {
Timber.v(file.toString() + " " + file.length() + " bytes") Timber.v("$file ${file.length()} bytes")
} }
return true return true
} }
@ -96,26 +95,19 @@ private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean {
fun getFileExtension(fileUri: String): String? { fun getFileExtension(fileUri: String): String? {
var reducedStr = fileUri var reducedStr = fileUri
if (!TextUtils.isEmpty(reducedStr)) { if (reducedStr.isNotEmpty()) {
// Remove fragment // Remove fragment
val fragment = fileUri.lastIndexOf('#') reducedStr = reducedStr.substringBeforeLast('#')
if (fragment > 0) {
reducedStr = fileUri.substring(0, fragment)
}
// Remove query // Remove query
val query = reducedStr.lastIndexOf('?') reducedStr = reducedStr.substringBeforeLast('?')
if (query > 0) {
reducedStr = reducedStr.substring(0, query)
}
// Remove path // Remove path
val filenamePos = reducedStr.lastIndexOf('/') val filename = reducedStr.substringAfterLast('/')
val filename = if (0 <= filenamePos) reducedStr.substring(filenamePos + 1) else reducedStr
// Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern // Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern
// See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs // See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs
if (!filename.isEmpty()) { if (filename.isNotEmpty()) {
val dotPos = filename.lastIndexOf('.') val dotPos = filename.lastIndexOf('.')
if (0 <= dotPos) { if (0 <= dotPos) {
val ext = filename.substring(dotPos + 1) val ext = filename.substring(dotPos + 1)
@ -134,15 +126,11 @@ fun getFileExtension(fileUri: String): String? {
* Size * Size
* ========================================================================================== */ * ========================================================================================== */
fun getSizeOfFiles(context: Context, root: File): Int { fun getSizeOfFiles(root: File): Int {
Timber.v("Get size of " + root.absolutePath) return root.walkTopDown()
return if (root.isDirectory) { .onEnter {
root.list() Timber.v("Get size of ${it.absolutePath}")
.map { true
getSizeOfFiles(context, File(root, it))
}
.fold(0, { acc, other -> acc + other })
} else {
root.length().toInt()
} }
.sumBy { it.length().toInt() }
} }

View file

@ -29,7 +29,6 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R import im.vector.riotx.R
import timber.log.Timber import timber.log.Timber
import java.util.*
private const val LOG_TAG = "PermissionUtils" private const val LOG_TAG = "PermissionUtils"
@ -74,7 +73,7 @@ const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
*/ */
fun logPermissionStatuses(context: Context) { fun logPermissionStatuses(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val permissions = Arrays.asList( val permissions = listOf(
Manifest.permission.CAMERA, Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO, Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
@ -213,7 +212,7 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
.setMessage(rationaleMessage) .setMessage(rationaleMessage)
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() } .setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
if (!permissionsListToBeGranted.isEmpty()) { if (permissionsListToBeGranted.isNotEmpty()) {
fragment?.requestPermissions(permissionsListToBeGranted.toTypedArray(), requestCode) fragment?.requestPermissions(permissionsListToBeGranted.toTypedArray(), requestCode)
?: run { ?: run {
ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode) ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode)

View file

@ -24,9 +24,9 @@ import java.util.*
object TextUtils { object TextUtils {
private val suffixes = TreeMap<Int, String>().also { private val suffixes = TreeMap<Int, String>().also {
it.put(1000, "k") it[1000] = "k"
it.put(1000000, "M") it[1000000] = "M"
it.put(1000000000, "G") it[1000000000] = "G"
} }
fun formatCountToShortDecimal(value: Int): String { fun formatCountToShortDecimal(value: Int): String {

View file

@ -17,7 +17,6 @@ package im.vector.riotx.features.crypto.keysbackup.setup
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
@ -122,7 +121,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
}) })
viewModel.passphrase.observe(this, Observer<String> { newValue -> viewModel.passphrase.observe(this, Observer<String> { newValue ->
if (TextUtils.isEmpty(newValue)) { if (newValue.isEmpty()) {
viewModel.passwordStrength.value = null viewModel.passwordStrength.value = null
} else { } else {
AsyncTask.execute { AsyncTask.execute {
@ -172,7 +171,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
@OnClick(R.id.keys_backup_setup_step2_button) @OnClick(R.id.keys_backup_setup_step2_button)
fun doNext() { fun doNext() {
when { when {
TextUtils.isEmpty(viewModel.passphrase.value) -> { viewModel.passphrase.value.isNullOrEmpty() -> {
viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message) viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message)
} }
viewModel.passphrase.value != viewModel.confirmPassphrase.value -> { viewModel.passphrase.value != viewModel.confirmPassphrase.value -> {
@ -192,7 +191,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
@OnClick(R.id.keys_backup_setup_step2_skip_button) @OnClick(R.id.keys_backup_setup_step2_skip_button)
fun skipPassphrase() { fun skipPassphrase() {
when { when {
TextUtils.isEmpty(viewModel.passphrase.value) -> { viewModel.passphrase.value.isNullOrEmpty() -> {
// Generate a recovery key for the user // Generate a recovery key for the user
viewModel.megolmBackupCreationInfo = null viewModel.megolmBackupCreationInfo = null

View file

@ -20,7 +20,6 @@
package im.vector.riotx.features.crypto.keysrequest package im.vector.riotx.features.crypto.keysrequest
import android.content.Context import android.content.Context
import android.text.TextUtils
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
@ -39,7 +38,8 @@ import im.vector.riotx.features.popup.PopupAlertManager
import timber.log.Timber import timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Locale
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -100,7 +100,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
alertsToRequests[mappingKey] = ArrayList<IncomingRoomKeyRequest>().apply { this.add(request) } alertsToRequests[mappingKey] = ArrayList<IncomingRoomKeyRequest>().apply { this.add(request) }
// Add a notification for every incoming request // Add a notification for every incoming request
session?.downloadKeys(Arrays.asList(userId), false, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> { session?.downloadKeys(listOf(userId), false, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) { override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
val deviceInfo = data.getObject(userId, deviceId) val deviceInfo = data.getObject(userId, deviceId)
@ -147,7 +147,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
wasNewDevice: Boolean, wasNewDevice: Boolean,
deviceInfo: MXDeviceInfo?, deviceInfo: MXDeviceInfo?,
moreInfo: DeviceInfo? = null) { moreInfo: DeviceInfo? = null) {
val deviceName = if (TextUtils.isEmpty(deviceInfo!!.displayName())) deviceInfo.deviceId else deviceInfo.displayName() val deviceName = if (deviceInfo!!.displayName().isNullOrEmpty()) deviceInfo.deviceId else deviceInfo.displayName()
val dialogText: String? val dialogText: String?
if (moreInfo != null) { if (moreInfo != null) {
@ -244,12 +244,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
val deviceId = request.deviceId val deviceId = request.deviceId
val requestId = request.requestId val requestId = request.requestId
if (TextUtils.isEmpty(userId) || TextUtils.isEmpty(deviceId) || TextUtils.isEmpty(requestId)) { if (userId.isNullOrEmpty() || deviceId.isNullOrEmpty() || requestId.isNullOrEmpty()) {
Timber.e("## handleKeyRequestCancellation() : invalid parameters") Timber.e("## handleKeyRequestCancellation() : invalid parameters")
return return
} }
val alertMgrUniqueKey = alertManagerId(deviceId!!, userId!!) val alertMgrUniqueKey = alertManagerId(deviceId, userId)
alertsToRequests[alertMgrUniqueKey]?.removeAll { alertsToRequests[alertMgrUniqueKey]?.removeAll {
it.deviceId == request.deviceId it.deviceId == request.deviceId
&& it.userId == request.userId && it.userId == request.userId

View file

@ -179,7 +179,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
} }
} }
return true return super.onOptionsItemSelected(item)
} }
override fun onBackPressed() { override fun onBackPressed() {

View file

@ -30,9 +30,9 @@ sealed class RoomDetailActions {
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class SendReaction(val targetEventId: String, val reaction: String) : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val reaction: String, val reason: String? = "") : RoomDetailActions()
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
data class SetReadMarkerAction(val eventId: String) : RoomDetailActions() data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
@ -49,6 +49,9 @@ sealed class RoomDetailActions {
data class ResendMessage(val eventId: String) : RoomDetailActions() data class ResendMessage(val eventId: String) : RoomDetailActions()
data class RemoveFailedEcho(val eventId: String) : RoomDetailActions() data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
data class ReportContent(val eventId: String, val reason: String, val spam: Boolean = false, val inappropriate: Boolean = false) : RoomDetailActions()
object ClearSendQueue : RoomDetailActions() object ClearSendQueue : RoomDetailActions()
object ResendAll : RoomDetailActions() object ResendAll : RoomDetailActions()
} }

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
@ -50,6 +51,7 @@ import com.airbnb.mvrx.*
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
@ -66,6 +68,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.hideKeyboard
@ -96,8 +99,12 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
@ -274,6 +281,10 @@ class RoomDetailFragment :
syncStateView.render(syncState) syncStateView.render(syncState)
} }
roomDetailViewModel.requestLiveData.observeEvent(this) {
displayRoomDetailActionResult(it)
}
if (savedInstanceState == null) { if (savedInstanceState == null) {
when (val sharedData = roomDetailArgs.sharedData) { when (val sharedData = roomDetailArgs.sharedData) {
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false)) is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
@ -281,6 +292,7 @@ class RoomDetailFragment :
null -> Timber.v("No share data to process") null -> Timber.v("No share data to process")
} }
} }
} }
override fun onDestroy() { override fun onDestroy() {
@ -438,7 +450,7 @@ class RoomDetailFragment :
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return ?: return
// TODO check if already reacted with that? // TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
} }
} }
} }
@ -732,17 +744,81 @@ class RoomDetailFragment :
} }
private fun displayCommandError(message: String) { private fun displayCommandError(message: String) {
AlertDialog.Builder(activity!!) AlertDialog.Builder(requireActivity())
.setTitle(R.string.command_error) .setTitle(R.string.command_error)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
private fun promptReasonToReportContent(action: SimpleAction.ReportContentCustom) {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_report_content, null)
val input = layout.findViewById<TextInputEditText>(R.id.dialog_report_content_input)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.report_content_custom_title)
.setView(layout)
.setPositiveButton(R.string.report_content_custom_submit) { _, _ ->
val reason = input.text.toString()
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, reason))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun displayRoomDetailActionResult(result: Async<RoomDetailActions>) {
when (result) {
is Fail -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.error))
.setPositiveButton(R.string.ok, null)
.show()
}
is Success -> {
when (val data = result.invoke()) {
is RoomDetailActions.ReportContent -> {
when {
data.spam -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
data.inappropriate -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_inappropriate_title)
.setMessage(R.string.content_reported_as_inappropriate_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
else -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
}
}
}
}
}
}
// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String): Boolean { override fun onUrlClicked(url: String): Boolean {
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { val managed = permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
override fun navToRoom(roomId: String, eventId: String?): Boolean { override fun navToRoom(roomId: String, eventId: String?): Boolean {
// Same room? // Same room?
if (roomId == roomDetailArgs.roomId) { if (roomId == roomDetailArgs.roomId) {
@ -760,6 +836,14 @@ class RoomDetailFragment :
return false return false
} }
}) })
if (!managed) {
// Open in external browser, in a new Tab
openUrlInExternalBrowser(requireContext(), url)
}
// In fact it is always managed
return true
} }
override fun onUrlLongClicked(url: String): Boolean { override fun onUrlLongClicked(url: String): Boolean {
@ -872,7 +956,7 @@ class RoomDetailFragment :
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
if (on) { if (on) {
// we should test the current real state of reaction on this event // we should test the current real state of reaction on this event
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) roomDetailViewModel.process(RoomDetailActions.SendReaction(informationData.eventId, reaction))
} else { } else {
// I need to redact a reaction // I need to redact a reaction
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
@ -880,7 +964,7 @@ class RoomDetailFragment :
} }
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData) ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
} }
@ -933,7 +1017,7 @@ class RoomDetailFragment :
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
} }
is SimpleAction.ViewReactions -> { is SimpleAction.ViewReactions -> {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
} }
is SimpleAction.Copy -> { is SimpleAction.Copy -> {
@ -1022,6 +1106,15 @@ class RoomDetailFragment :
is SimpleAction.Remove -> { is SimpleAction.Remove -> {
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
} }
is SimpleAction.ReportContentSpam -> {
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam", spam = true))
}
is SimpleAction.ReportContentInappropriate -> {
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate", inappropriate = true))
}
is SimpleAction.ReportContentCustom -> {
promptReasonToReportContent(action)
}
else -> { else -> {
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
} }

View file

@ -16,14 +16,10 @@
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.home.room.detail
import android.text.TextUtils
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.*
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -92,6 +88,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private var timeline = room.createTimeline(eventId, timelineSettings) private var timeline = room.createTimeline(eventId, timelineSettings)
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<RoomDetailActions>>>()
val requestLiveData: LiveData<LiveEvent<Async<RoomDetailActions>>>
get() = _requestLiveData
// Slot to keep a pending action during permission request // Slot to keep a pending action during permission request
var pendingAction: RoomDetailActions? = null var pendingAction: RoomDetailActions? = null
@ -150,6 +151,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.ResendAll -> handleResendAll() is RoomDetailActions.ResendAll -> handleResendAll()
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action) is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead() is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailActions.ReportContent -> handleReportContent(action)
} }
} }
@ -371,7 +373,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val document = parser.parse(finalText) val document = parser.parse(finalText)
val renderer = HtmlRenderer.builder().build() val renderer = HtmlRenderer.builder().build()
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (TextUtils.equals(finalText, htmlText)) { if (finalText == htmlText) {
room.sendTextMessage(finalText) room.sendTextMessage(finalText)
} else { } else {
room.sendFormattedTextMessage(finalText, htmlText) room.sendFormattedTextMessage(finalText, htmlText)
@ -396,19 +398,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
val quotedTextMsg = StringBuilder() return buildString {
if (messageParagraphs != null) { if (messageParagraphs != null) {
for (i in messageParagraphs.indices) { for (i in messageParagraphs.indices) {
if (messageParagraphs[i].trim() != "") { if (messageParagraphs[i].isNotBlank()) {
quotedTextMsg.append("> ").append(messageParagraphs[i]) append("> ")
append(messageParagraphs[i])
} }
if (i + 1 != messageParagraphs.size) { if (i != messageParagraphs.lastIndex) {
quotedTextMsg.append("\n\n") append("\n\n")
} }
} }
} }
return "$quotedTextMsg\n\n$myText" append("\n\n")
append(myText)
}
} }
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
@ -440,7 +445,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleSendReaction(action: RoomDetailActions.SendReaction) { private fun handleSendReaction(action: RoomDetailActions.SendReaction) {
room.sendReaction(action.reaction, action.targetEventId) room.sendReaction(action.targetEventId, action.reaction)
} }
private fun handleRedactEvent(action: RoomDetailActions.RedactAction) { private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
@ -449,14 +454,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleUndoReact(action: RoomDetailActions.UndoReaction) { private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
room.undoReaction(action.key, action.targetEventId, session.myUserId) room.undoReaction(action.targetEventId, action.reaction)
} }
private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) { private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
if (action.add) { if (action.add) {
room.sendReaction(action.selectedReaction, action.targetEventId) room.sendReaction(action.targetEventId, action.selectedReaction)
} else { } else {
room.undoReaction(action.selectedReaction, action.targetEventId, session.myUserId) room.undoReaction(action.targetEventId, action.selectedReaction)
} }
} }
@ -680,6 +685,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.markAllAsRead(object : MatrixCallback<Any> {}) room.markAllAsRead(object : MatrixCallback<Any> {})
} }
private fun handleReportContent(action: RoomDetailActions.ReportContent) {
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action)))
}
override fun onFailure(failure: Throwable) {
_requestLiveData.postValue(LiveEvent(Fail(failure)))
}
})
}
private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()

View file

@ -21,19 +21,18 @@ import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -48,8 +47,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var epoxyController: DisplayReadReceiptsController @Inject lateinit var epoxyController: DisplayReadReceiptsController
@BindView(R.id.bottom_sheet_display_reactions_list) @BindView(R.id.bottomSheetRecyclerView)
lateinit var epoxyRecyclerView: EpoxyRecyclerView lateinit var recyclerView: RecyclerView
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args() private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
@ -58,24 +57,20 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false) val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
ButterKnife.bind(this, view) ButterKnife.bind(this, view)
return view return view
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController) recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, recyclerView.adapter = epoxyController.adapter
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = getString(R.string.read_at) bottomSheetTitle.text = getString(R.string.read_at)
epoxyController.setData(displayReadReceiptArgs.readReceipts) epoxyController.setData(displayReadReceiptArgs.readReceipts)
} }
override fun invalidate() { // we are not using state for this one as it's static, so no need to override invalidate()
// we are not using state for this one as it's static
}
companion object { companion object {
fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet { fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet {

View file

@ -0,0 +1,69 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
/**
* A action for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action)
abstract class BottomSheetItemAction : VectorEpoxyModel<BottomSheetItemAction.Holder>() {
@EpoxyAttribute
@DrawableRes
var iconRes: Int = 0
@EpoxyAttribute
var textRes: Int = 0
@EpoxyAttribute
var showExpand = false
@EpoxyAttribute
var expanded = false
@EpoxyAttribute
var subMenuItem = false
@EpoxyAttribute
lateinit var listener: View.OnClickListener
override fun bind(holder: Holder) {
holder.view.setOnClickListener {
listener.onClick(it)
}
holder.startSpace.isVisible = subMenuItem
holder.icon.setImageResource(iconRes)
holder.text.setText(textRes)
holder.expand.isVisible = showExpand
if (showExpand) {
holder.expand.setImageResource(if (expanded) R.drawable.ic_material_expand_less_black else R.drawable.ic_material_expand_more_black)
}
}
class Holder : VectorEpoxyHolder() {
val startSpace by bind<View>(R.id.action_start_space)
val icon by bind<ImageView>(R.id.action_icon)
val text by bind<TextView>(R.id.action_title)
val expand by bind<ImageView>(R.id.action_expand)
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
/**
* A message preview for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview)
abstract class BottomSheetItemMessagePreview : VectorEpoxyModel<BottomSheetItemMessagePreview.Holder>() {
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
lateinit var informationData: MessageInformationData
@EpoxyAttribute
var senderName: String? = null
@EpoxyAttribute
lateinit var body: CharSequence
@EpoxyAttribute
var time: CharSequence? = null
override fun bind(holder: Holder) {
avatarRenderer.render(informationData.avatarUrl, informationData.senderId, senderName, holder.avatar)
holder.sender.setTextOrHide(senderName)
holder.body.text = body
holder.timestamp.setTextOrHide(time)
}
class Holder : VectorEpoxyHolder() {
val avatar by bind<ImageView>(R.id.bottom_sheet_message_preview_avatar)
val sender by bind<TextView>(R.id.bottom_sheet_message_preview_sender)
val body by bind<TextView>(R.id.bottom_sheet_message_preview_body)
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
}
}

View file

@ -0,0 +1,80 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.graphics.Typeface
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
/**
* A quick reaction list for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction)
abstract class BottomSheetItemQuickReactions : VectorEpoxyModel<BottomSheetItemQuickReactions.Holder>() {
@EpoxyAttribute
lateinit var fontProvider: EmojiCompatFontProvider
@EpoxyAttribute
lateinit var texts: List<String>
@EpoxyAttribute
lateinit var selecteds: List<Boolean>
@EpoxyAttribute
var listener: Listener? = null
override fun bind(holder: Holder) {
holder.textViews.forEachIndexed { index, textView ->
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
textView.text = texts[index]
textView.alpha = if (selecteds[index]) 0.2f else 1f
textView.setOnClickListener {
listener?.didSelect(texts[index], !selecteds[index])
}
}
}
class Holder : VectorEpoxyHolder() {
private val quickReaction0 by bind<TextView>(R.id.quickReaction0)
private val quickReaction1 by bind<TextView>(R.id.quickReaction1)
private val quickReaction2 by bind<TextView>(R.id.quickReaction2)
private val quickReaction3 by bind<TextView>(R.id.quickReaction3)
private val quickReaction4 by bind<TextView>(R.id.quickReaction4)
private val quickReaction5 by bind<TextView>(R.id.quickReaction5)
private val quickReaction6 by bind<TextView>(R.id.quickReaction6)
private val quickReaction7 by bind<TextView>(R.id.quickReaction7)
val textViews
get() = listOf(
quickReaction0,
quickReaction1,
quickReaction2,
quickReaction3,
quickReaction4,
quickReaction5,
quickReaction6,
quickReaction7
)
}
interface Listener {
fun didSelect(emoji: String, selected: Boolean)
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.view.View
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
/**
* A send state for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status)
abstract class BottomSheetItemSendState : VectorEpoxyModel<BottomSheetItemSendState.Holder>() {
@EpoxyAttribute
var showProgress: Boolean = false
@EpoxyAttribute
lateinit var text: CharSequence
@EpoxyAttribute
@DrawableRes
var drawableStart: Int = 0
override fun bind(holder: Holder) {
holder.progress.isVisible = showProgress
holder.text.setCompoundDrawablesWithIntrinsicBounds(drawableStart, 0, 0, 0)
holder.text.text = text
}
class Holder : VectorEpoxyHolder() {
val progress by bind<View>(R.id.messageStatusProgress)
val text by bind<TextView>(R.id.messageStatusText)
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider)
abstract class BottomSheetItemSeparator : VectorEpoxyModel<BottomSheetItemSeparator.Holder>() {
class Holder : VectorEpoxyHolder()
}

View file

@ -15,62 +15,46 @@
*/ */
package im.vector.riotx.features.home.room.detail.timeline.action package im.vector.riotx.features.home.room.detail.timeline.action
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.synthetic.main.bottom_sheet_message_actions.*
import javax.inject.Inject import javax.inject.Inject
/** /**
* Bottom sheet fragment that shows a message preview with list of contextual actions * Bottom sheet fragment that shows a message preview with list of contextual actions
* (Includes fragments for quick reactions and list of actions)
*/ */
class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener {
@Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
@Inject lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
@Inject
lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
@Inject
lateinit var avatarRenderer: AvatarRenderer
private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class) private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
override val showExpanded = true
private lateinit var actionHandlerModel: ActionsHandler private lateinit var actionHandlerModel: ActionsHandler
@BindView(R.id.bottom_sheet_message_preview_avatar)
lateinit var senderAvatarImageView: ImageView
@BindView(R.id.bottom_sheet_message_preview_sender)
lateinit var senderNameTextView: TextView
@BindView(R.id.bottom_sheet_message_preview_timestamp)
lateinit var messageTimestampText: TextView
@BindView(R.id.bottom_sheet_message_preview_body)
lateinit var messageBodyTextView: TextView
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this) screenComponent.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false) val view = inflater.inflate(R.layout.bottom_sheet_generic_list, container, false)
ButterKnife.bind(this, view) ButterKnife.bind(this, view)
return view return view
} }
@ -78,78 +62,26 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
val cfm = childFragmentManager recyclerView.adapter = messageActionsEpoxyController.adapter
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment // Disable item animation
if (menuActionFragment == null) { recyclerView.itemAnimator = null
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs) messageActionsEpoxyController.listener = this
cfm.beginTransaction()
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
.commit()
} }
menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener {
override fun didSelectMenuAction(simpleAction: SimpleAction) { override fun didSelectMenuAction(simpleAction: SimpleAction) {
if (simpleAction is SimpleAction.ReportContent) {
// Toggle report menu
viewModel.toggleReportMenu()
} else {
actionHandlerModel.fireAction(simpleAction) actionHandlerModel.fireAction(simpleAction)
dismiss() dismiss()
} }
} }
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
if (quickReactionFragment == null) {
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction()
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
.commit()
}
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) {
actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add))
dismiss()
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
// We want to force the bottom sheet initial state to expanded
(dialog as? BottomSheetDialog)?.let { bottomSheetDialog ->
bottomSheetDialog.setOnShowListener { dialog ->
val d = dialog as BottomSheetDialog
(d.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout)?.let {
BottomSheetBehavior.from(it).state = BottomSheetBehavior.STATE_COLLAPSED
}
}
}
return dialog
}
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
val body = viewModel.resolveBody(it) messageActionsEpoxyController.setData(it)
if (body != null) { super.invalidate()
bottom_sheet_message_preview.isVisible = true
senderNameTextView.text = it.senderName()
messageBodyTextView.text = body
messageTimestampText.text = it.time()
avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView)
} else {
bottom_sheet_message_preview.isVisible = false
}
quickReactBottomDivider.isVisible = it.canReact()
bottom_sheet_quick_reaction_container.isVisible = it.canReact()
if (it.informationData.sendState.isSending()) {
messageStatusInfo.isVisible = true
messageStatusProgress.isVisible = true
messageStatusText.text = getString(R.string.event_status_sending_message)
messageStatusText.setCompoundDrawables(null, null, null, null)
} else if (it.informationData.sendState.hasFailed()) {
messageStatusInfo.isVisible = true
messageStatusProgress.isVisible = false
messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0)
messageStatusText.text = getString(R.string.unable_to_send_message)
} else {
messageStatusInfo.isVisible = false
}
return@withState
} }
companion object { companion object {

View file

@ -0,0 +1,124 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Success
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
/**
* Epoxy controller for message action list
*/
class MessageActionsEpoxyController @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController<MessageActionState>() {
var listener: MessageActionsEpoxyControllerListener? = null
override fun buildModels(state: MessageActionState) {
// Message preview
val body = state.messageBody
if (body != null) {
bottomSheetItemMessagePreview {
id("preview")
avatarRenderer(avatarRenderer)
informationData(state.informationData)
senderName(state.senderName())
body(body)
time(state.time())
}
}
// Send state
if (state.informationData.sendState.isSending()) {
bottomSheetItemSendState {
id("send_state")
showProgress(true)
text(stringProvider.getString(R.string.event_status_sending_message))
}
} else if (state.informationData.sendState.hasFailed()) {
bottomSheetItemSendState {
id("send_state")
showProgress(false)
text(stringProvider.getString(R.string.unable_to_send_message))
drawableStart(R.drawable.ic_warning_small)
}
}
// Quick reactions
if (state.canReact() && state.quickStates is Success) {
// Separator
bottomSheetItemSeparator {
id("reaction_separator")
}
bottomSheetItemQuickReactions {
id("quick_reaction")
fontProvider(fontProvider)
texts(state.quickStates()?.map { it.reaction }.orEmpty())
selecteds(state.quickStates.invoke().map { it.isSelected })
listener(object : BottomSheetItemQuickReactions.Listener {
override fun didSelect(emoji: String, selected: Boolean) {
listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected))
}
})
}
}
// Separator
bottomSheetItemSeparator {
id("actions_separator")
}
// Action
state.actions()?.forEachIndexed { index, action ->
bottomSheetItemAction {
id("action_$index")
iconRes(action.iconResId)
textRes(action.titleRes)
showExpand(action is SimpleAction.ReportContent)
expanded(state.expendedReportContentMenu)
listener(View.OnClickListener { listener?.didSelectMenuAction(action) })
}
if (action is SimpleAction.ReportContent && state.expendedReportContentMenu) {
// Special case for report content menu: add the submenu
listOf(
SimpleAction.ReportContentSpam(action.eventId),
SimpleAction.ReportContentInappropriate(action.eventId),
SimpleAction.ReportContentCustom(action.eventId)
).forEachIndexed { indexReport, actionReport ->
bottomSheetItemAction {
id("actionReport_$indexReport")
subMenuItem(true)
iconRes(actionReport.iconResId)
textRes(actionReport.titleRes)
listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) })
}
}
}
}
}
interface MessageActionsEpoxyControllerListener {
fun didSelectMenuAction(simpleAction: SimpleAction)
}
}

View file

@ -21,26 +21,48 @@ import com.squareup.inject.assisted.AssistedInject
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.RxRoom
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
/**
* Quick reactions state
*/
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class MessageActionState( data class MessageActionState(
val roomId: String, val roomId: String,
val eventId: String, val eventId: String,
val informationData: MessageInformationData, val informationData: MessageInformationData,
val timelineEvent: Async<TimelineEvent> = Uninitialized val timelineEvent: Async<TimelineEvent> = Uninitialized,
val messageBody: CharSequence? = null,
// For quick reactions
val quickStates: Async<List<ToggleState>> = Uninitialized,
// For actions
val actions: Async<List<SimpleAction>> = Uninitialized,
val expendedReportContentMenu: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
@ -49,17 +71,100 @@ data class MessageActionState(
fun senderName(): String = informationData.memberName?.toString() ?: "" fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
?: ""
fun canReact() = timelineEvent()?.canReact() == true fun canReact() = timelineEvent()?.canReact() == true
}
fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? { /**
* Information related to an event and used to display preview in contextual bottomsheet.
*/
class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState,
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider
) : VectorViewModel<MessageActionState>(initialState) {
private val eventId = initialState.eventId
private val informationData = initialState.informationData
private val room = session.getRoom(initialState.roomId)
@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel
}
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageActionViewModelFactory.create(state)
}
}
init {
observeEvent()
observeReactions()
observeEventAction()
}
fun toggleReportMenu() = withState {
setState {
copy(
expendedReportContentMenu = it.expendedReportContentMenu.not()
)
}
}
private fun observeEvent() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.unwrap()
.execute {
copy(
timelineEvent = it,
messageBody = computeMessageBody(it)
)
}
}
private fun observeEventAction() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.map {
actionsForEvent(it)
}
.execute {
copy(actions = it)
}
}
private fun observeReactions() {
if (room == null) return
RxRoom(room)
.liveAnnotationSummary(eventId)
.map { annotations ->
quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
}
}
.execute {
copy(quickStates = it)
}
}
private fun computeMessageBody(timelineEvent: Async<TimelineEvent>): CharSequence? {
return when (timelineEvent()?.root?.getClearType()) { return when (timelineEvent()?.root?.getClearType()) {
EventType.MESSAGE -> { EventType.MESSAGE -> {
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent() val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer?.render(messageContent.formattedBody eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
} else { } else {
messageContent?.body messageContent?.body
@ -72,54 +177,177 @@ data class MessageActionState(
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {
timelineEvent()?.let { noticeEventFormatter?.format(it) } timelineEvent()?.let { noticeEventFormatter.format(it) }
} }
else -> null else -> null
} }
} }
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
val event = optionalEvent.getOrNull() ?: return emptyList()
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel()
val type = messageContent?.type
return arrayListOf<SimpleAction>().apply {
if (event.root.sendState.hasFailed()) {
if (canRetry(event)) {
add(SimpleAction.Resend(eventId))
}
add(SimpleAction.Remove(eventId))
} else if (event.root.sendState.isSending()) {
// TODO is uploading attachment?
if (canCancel(event)) {
add(SimpleAction.Cancel(eventId))
}
} else {
if (!event.root.isRedacted()) {
if (canReply(event, messageContent)) {
add(SimpleAction.Reply(eventId))
} }
/** if (canEdit(event, session.myUserId)) {
* Information related to an event and used to display preview in contextual bottomsheet. add(SimpleAction.Edit(eventId))
*/
class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState,
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
session: Session,
private val noticeEventFormatter: NoticeEventFormatter
) : VectorViewModel<MessageActionState>(initialState) {
private val eventId = initialState.eventId
private val room = session.getRoom(initialState.roomId)
@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel
} }
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> { if (canRedact(event, session.myUserId)) {
add(SimpleAction.Delete(eventId))
}
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { if (canCopy(type)) {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() // TODO copy images? html? see ClipBoard
return fragment.messageActionViewModelFactory.create(state) add(SimpleAction.Copy(messageContent!!.body))
}
if (event.canReact()) {
add(SimpleAction.AddReaction(eventId))
}
if (canQuote(event, messageContent)) {
add(SimpleAction.Quote(eventId))
}
if (canViewReactions(event)) {
add(SimpleAction.ViewReactions(informationData))
}
if (event.hasBeenEdited()) {
add(SimpleAction.ViewEditHistory(informationData))
}
if (canShare(type)) {
if (messageContent is MessageImageContent) {
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
add(SimpleAction.Share(url))
}
}
// TODO
}
if (event.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
} }
} }
init { add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
observeEvent() if (event.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(SimpleAction.ViewDecryptedSource(decryptedContent))
} }
add(SimpleAction.CopyPermalink(eventId))
private fun observeEvent() { if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
if (room == null) return // not sent by me
RxRoom(room) add(SimpleAction.ReportContent(eventId))
.liveTimelineEvent(eventId) }
.unwrap() }
.execute {
copy(timelineEvent = it)
} }
} }
fun resolveBody(state: MessageActionState): CharSequence? { private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter) return false
}
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> {
true
}
else -> false
}
}
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.root.senderId == myUserId
}
private fun canRetry(event: TimelineEvent): Boolean {
return event.root.sendState.hasFailed() && event.root.isTextMessage()
}
private fun canViewReactions(event: TimelineEvent): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
val messageContent = event.root.getClearContent().toModel<MessageContent>()
return event.root.senderId == myUserId && (
messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
)
}
private fun canCopy(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> true
else -> false
}
}
private fun canShare(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
} }
} }

View file

@ -1,104 +0,0 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.themes.ThemeUtils
import javax.inject.Inject
/**
* Fragment showing the list of available contextual action for a given message.
*/
class MessageMenuFragment : VectorBaseFragment() {
@Inject lateinit var messageMenuViewModelFactory: MessageMenuViewModel.Factory
private val viewModel: MessageMenuViewModel by fragmentViewModel(MessageMenuViewModel::class)
private var addSeparators = false
var interactionListener: InteractionListener? = null
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun getLayoutResId() = R.layout.fragment_message_menu
override fun invalidate() = withState(viewModel) { state ->
val linearLayout = view as? LinearLayout
if (linearLayout != null) {
val inflater = LayoutInflater.from(linearLayout.context)
linearLayout.removeAllViews()
var insertIndex = 0
val actions = state.actions()
actions?.forEachIndexed { index, action ->
inflateActionView(action, inflater, linearLayout)?.let {
it.setOnClickListener {
interactionListener?.didSelectMenuAction(action)
}
linearLayout.addView(it, insertIndex)
insertIndex++
if (addSeparators) {
if (index < actions.size - 1) {
linearLayout.addView(inflateSeparatorView(), insertIndex)
insertIndex++
}
}
}
}
}
}
private fun inflateActionView(action: SimpleAction, inflater: LayoutInflater, container: ViewGroup?): View? {
return inflater.inflate(R.layout.adapter_item_action, container, false)?.apply {
findViewById<ImageView>(R.id.action_icon)?.setImageResource(action.iconResId)
findViewById<TextView>(R.id.action_title)?.setText(action.titleRes)
}
}
private fun inflateSeparatorView(): View {
val frame = FrameLayout(requireContext())
frame.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_list_divider_color))
frame.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, requireContext().resources.displayMetrics.density.toInt())
return frame
}
interface InteractionListener {
fun didSelectMenuAction(simpleAction: SimpleAction)
}
companion object {
fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment {
val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = MessageMenuFragment()
fragment.arguments = args
return fragment
}
}
}

View file

@ -1,279 +0,0 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.rx.RxRoom
import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
}
data class MessageMenuState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val actions: Async<List<SimpleAction>> = Uninitialized
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
}
/**
* Manages list actions for a given message (copy / paste / forward...)
*/
class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState,
private val session: Session,
private val stringProvider: StringProvider) : VectorViewModel<MessageMenuState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageMenuState): MessageMenuViewModel
}
private val room = session.getRoom(initialState.roomId)
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
private val eventId = initialState.eventId
private val informationData: MessageInformationData = initialState.informationData
companion object : MvRxViewModelFactory<MessageMenuViewModel, MessageMenuState> {
override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? {
val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageMenuViewModelFactory.create(state)
}
}
init {
observeEvent()
}
private fun observeEvent() {
RxRoom(room)
.liveTimelineEvent(eventId)
.map {
actionsForEvent(it)
}
.execute {
copy(actions = it)
}
}
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
val event = optionalEvent.getOrNull() ?: return emptyList()
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel()
val type = messageContent?.type
return arrayListOf<SimpleAction>().apply {
if (event.root.sendState.hasFailed()) {
if (canRetry(event)) {
add(SimpleAction.Resend(eventId))
}
add(SimpleAction.Remove(eventId))
} else if (event.root.sendState.isSending()) {
// TODO is uploading attachment?
if (canCancel(event)) {
add(SimpleAction.Cancel(eventId))
}
} else {
if (!event.root.isRedacted()) {
if (canReply(event, messageContent)) {
add(SimpleAction.Reply(eventId))
}
if (canEdit(event, session.myUserId)) {
add(SimpleAction.Edit(eventId))
}
if (canRedact(event, session.myUserId)) {
add(SimpleAction.Delete(eventId))
}
if (canCopy(type)) {
// TODO copy images? html? see ClipBoard
add(SimpleAction.Copy(messageContent!!.body))
}
if (event.canReact()) {
add(SimpleAction.AddReaction(eventId))
}
if (canQuote(event, messageContent)) {
add(SimpleAction.Quote(eventId))
}
if (canViewReactions(event)) {
add(SimpleAction.ViewReactions(informationData))
}
if (event.hasBeenEdited()) {
add(SimpleAction.ViewEditHistory(informationData))
}
if (canShare(type)) {
if (messageContent is MessageImageContent) {
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
add(SimpleAction.Share(url))
}
}
// TODO
}
if (event.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
}
add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
if (event.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(SimpleAction.ViewDecryptedSource(decryptedContent))
}
add(SimpleAction.CopyPermalink(eventId))
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
// not sent by me
add(SimpleAction.Flag(eventId))
}
}
}
}
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
return false
}
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> {
true
}
else -> false
}
}
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.root.senderId == myUserId
}
private fun canRetry(event: TimelineEvent): Boolean {
return event.root.sendState.hasFailed() && event.root.isTextMessage()
}
private fun canViewReactions(event: TimelineEvent): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
val messageContent = event.root.getClearContent().toModel<MessageContent>()
return event.root.senderId == myUserId && (
messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
)
}
private fun canCopy(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> true
else -> false
}
}
private fun canShare(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}
}

View file

@ -1,89 +0,0 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.graphics.Typeface
import android.os.Bundle
import android.view.View
import android.widget.TextView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.*
import javax.inject.Inject
/**
* Quick Reaction Fragment (agree / like reactions)
*/
class QuickReactionFragment : VectorBaseFragment() {
private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class)
var interactionListener: InteractionListener? = null
@Inject lateinit var fontProvider: EmojiCompatFontProvider
@Inject lateinit var quickReactionViewModelFactory: QuickReactionViewModel.Factory
override fun getLayoutResId() = R.layout.adapter_item_action_quick_reaction
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
private lateinit var textViews: List<TextView>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3,
quickReaction4, quickReaction5, quickReaction6, quickReaction7)
textViews.forEachIndexed { index, textView ->
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
textView.setOnClickListener {
viewModel.didSelect(index)
}
}
}
override fun invalidate() = withState(viewModel) {
val quickReactionsStates = it.quickStates() ?: return@withState
quickReactionsStates.forEachIndexed { index, qs ->
textViews[index].text = qs.reaction
textViews[index].alpha = if (qs.isSelected) 0.2f else 1f
}
if (it.result != null) {
interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId)
}
}
interface InteractionListener {
fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String)
}
companion object {
fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment {
val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = QuickReactionFragment()
fragment.arguments = args
return fragment
}
}
}

View file

@ -1,96 +0,0 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.RxRoom
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
/**
* Quick reactions state, it's a toggle with 3rd state
*/
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class QuickReactionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val quickStates: Async<List<ToggleState>> = Uninitialized,
val result: ToggleState? = null
/** Pair of 'clickedOn' and current toggles state*/
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
}
/**
* Quick reaction view model
*/
class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState: QuickReactionState,
private val session: Session) : VectorViewModel<QuickReactionState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: QuickReactionState): QuickReactionViewModel
}
private val room = session.getRoom(initialState.roomId)
private val eventId = initialState.eventId
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? {
val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.quickReactionViewModelFactory.create(state)
}
}
init {
observeReactions()
}
private fun observeReactions() {
if (room == null) return
RxRoom(room)
.liveAnnotationSummary(eventId)
.map { annotations ->
quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe
?: false)
}
}
.execute {
copy(quickStates = it)
}
}
fun didSelect(index: Int) = withState {
val selectedReaction = it.quickStates()?.get(index) ?: return@withState
val isSelected = selectedReaction.isSelected
setState {
copy(result = ToggleState(selectedReaction.reaction, !isSelected))
}
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
data class ReportContent(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
data class ReportContentSpam(val eventId: String) : SimpleAction(R.string.report_content_spam, R.drawable.ic_report_spam)
data class ReportContentInappropriate(val eventId: String) : SimpleAction(R.string.report_content_inappropriate, R.drawable.ic_report_inappropriate)
data class ReportContentCustom(val eventId: String) : SimpleAction(R.string.report_content_custom, R.drawable.ic_report_custom)
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home.room.detail.timeline.action package im.vector.riotx.features.home.room.detail.timeline.edithistory
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -21,17 +21,20 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -44,8 +47,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory @Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
@BindView(R.id.bottom_sheet_display_reactions_list) @BindView(R.id.bottomSheetRecyclerView)
lateinit var epoxyRecyclerView: EpoxyRecyclerView lateinit var recyclerView: RecyclerView
private val epoxyController by lazy { private val epoxyController by lazy {
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
@ -56,22 +59,23 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false) val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
ButterKnife.bind(this, view) ButterKnife.bind(this, view)
return view return view
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController) recyclerView.adapter = epoxyController.adapter
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
LinearLayout.VERTICAL) val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration) recyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = context?.getString(R.string.message_edits) bottomSheetTitle.text = context?.getString(R.string.message_edits)
} }
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
epoxyController.setData(it) epoxyController.setData(it)
super.invalidate()
} }
companion object { companion object {

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