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

View file

@ -10,6 +10,8 @@ Improvements:
- Handle read markers (#84)
- Attachments: start using system pickers (#52)
- Attachments: start handling incoming share (#58)
- Mark all messages as read (#396)
- Add ability to report content (#515)
Other changes:
- 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)
- 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)
- Opening links from RiotX reuses browser tab (#599)
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:
> ./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
> ./ktlint --android -v
<pre>
./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
> ./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
Make sure the following commands execute without any error:
> ./gradlew testGplayReleaseUnitTest
<pre>
./gradlew testGplayReleaseUnitTest
</pre>
### 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.
### Internationalisation

View file

@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
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
#vector.debugPrivateData=true

View file

@ -102,7 +102,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
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"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
@ -110,8 +110,8 @@ dependencies {
// Network
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
implementation 'com.squareup.okhttp3:okhttp:4.2.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation 'com.novoda:merlin:1.2.0'
implementation "com.squareup.moshi:moshi-adapters:$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.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import java.util.Collections
/* ==========================================================================================
* MXDeviceInfo
@ -29,6 +28,6 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4)
?.joinToString(separator = " ")
fun List<DeviceInfo>.sortByLastSeen() {
Collections.sort(this, DatedObjectComparators.descComparator)
fun MutableList<DeviceInfo>.sortByLastSeen() {
sortWith(DatedObjectComparators.descComparator)
}

View file

@ -30,9 +30,9 @@ object MatrixLinkify {
*
* @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
if (spannable.isNullOrEmpty()) {
if (spannable.isEmpty()) {
return false
}
val text = spannable.toString()

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.api.permalinks
import android.text.TextUtils
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
*/
fun createPermalink(id: String): String? {
return if (TextUtils.isEmpty(id)) {
return if (id.isEmpty()) {
null
} 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"
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
*/
fun getLinkedId(url: String?): String? {
val isSupported = url != null && url.startsWith(MATRIX_TO_URL_BASE)
fun getLinkedId(url: String): String? {
val isSupported = url.startsWith(MATRIX_TO_URL_BASE)
return if (isSupported) {
url!!.substring(MATRIX_TO_URL_BASE.length)
url.substring(MATRIX_TO_URL_BASE.length)
} else null
}
@ -86,6 +85,6 @@ object PermalinkFactory {
* @return the escaped id
*/
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
import android.text.TextUtils
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.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import timber.log.Timber
import java.util.regex.Pattern
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 {
var message = when (event.type) {
val message = when (event.type) {
EventType.MESSAGE -> {
event.content.toModel<MessageContent>()
}
@ -59,20 +57,18 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
*/
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
// add sanity checks
if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) {
if (subString.isEmpty() || longString.isEmpty()) {
return false
}
var res = false
try {
val pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE)
res = pattern.matcher(longString).find()
val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE)
return regex.containsMatchIn(longString)
} catch (e: Exception) {
Timber.e(e, "## caseInsensitiveFind() : failed")
}
return res
return false
}
}
}

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.api.session.events.model
import android.text.TextUtils
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.MXCryptoError
@ -35,18 +34,16 @@ typealias Content = JsonDict
* This methods is a facility method to map a json content to a model.
*/
inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
return this?.let {
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
return try {
moshiAdapter.fromJsonValue(it)
} catch (e: Exception) {
if (catchError) {
Timber.e(e, "To model failed : $e")
null
} else {
throw e
}
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
return try {
moshiAdapter.fromJsonValue(this)
} catch (e: Exception) {
if (catchError) {
Timber.e(e, "To model failed : $e")
null
} else {
throw e
}
}
}
@ -55,12 +52,10 @@ inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
* This methods is a facility method to map a model to a json Content
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> T?.toContent(): Content? {
return this?.let {
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
return moshiAdapter.toJsonValue(it) as Content
}
inline fun <reified T> T.toContent(): Content {
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
return moshiAdapter.toJsonValue(this) as Content
}
/**
@ -106,7 +101,7 @@ data class Event(
* @return true if this event is encrypted.
*/
fun isEncrypted(): Boolean {
return TextUtils.equals(type, EventType.ENCRYPTED)
return type == EventType.ENCRYPTED
}
/**
@ -139,7 +134,7 @@ data class Event(
}
fun toContentStringWithIndent(): String {
val contentMap = toContent()?.toMutableMap() ?: HashMap()
val contentMap = toContent().toMutableMap()
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.model.RoomSummary
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.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
@ -38,6 +39,7 @@ interface Room :
ReadService,
MembershipService,
StateService,
ReportingService,
RelationService,
RoomCryptoService {

View file

@ -53,4 +53,9 @@ interface RoomService {
* @return the [LiveData] of [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
import android.text.TextUtils
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
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.
@ -45,14 +43,8 @@ data class PowerLevels(
* @param userId the user id
* @return the power level
*/
fun getUserPowerLevel(userId: String): Int {
// sanity check
if (!TextUtils.isEmpty(userId)) {
val powerLevel = users[userId]
return powerLevel ?: usersDefault
}
return usersDefault
fun getUserPowerLevel(userId: String): Int {
return users.getOrElse(userId) { usersDefault }
}
/**
@ -61,10 +53,8 @@ data class PowerLevels(
* @param userId the user
* @param powerLevel the new power level
*/
fun setUserPowerLevel(userId: String?, powerLevel: Int) {
if (null != userId) {
users[userId] = Integer.valueOf(powerLevel)
}
fun setUserPowerLevel(userId: String, powerLevel: Int) {
users[userId] = powerLevel
}
/**
@ -74,8 +64,8 @@ data class PowerLevels(
* @param userId the user id
* @return true if the user can send the event
*/
fun maySendEventOfType(eventTypeString: String, userId: String): Boolean {
return if (!TextUtils.isEmpty(eventTypeString) && !TextUtils.isEmpty(userId)) {
fun maySendEventOfType(eventTypeString: String, userId: String): Boolean {
return if (eventTypeString.isNotEmpty() && userId.isNotEmpty()) {
getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString)
} else false
}
@ -86,8 +76,8 @@ data class PowerLevels(
* @param userId the user id
* @return true if the user can send a room message
*/
fun maySendMessage(userId: String): Boolean {
return maySendEventOfType(EventType.MESSAGE, userId)
fun maySendMessage(userId: String): Boolean {
return maySendEventOfType(EventType.MESSAGE, userId)
}
/**
@ -97,7 +87,7 @@ data class PowerLevels(
* @param eventTypeString the type of event (in Event.EVENT_TYPE_XXX values)
* @return the required minimum power level.
*/
fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int {
fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int {
return events[eventTypeString] ?: eventsDefault
}
@ -108,7 +98,7 @@ data class PowerLevels(
* @param eventTypeString the type of event (in Event.EVENT_TYPE_STATE_ values).
* @return the required minimum power level.
*/
fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int {
fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int {
return events[eventTypeString] ?: stateDefault
}
@ -118,18 +108,14 @@ data class PowerLevels(
* @param key the notification key
* @return the level
*/
fun notificationLevel(key: String?): Int {
if (null != key && notifications.containsKey(key)) {
val valAsVoid = notifications[key]
fun notificationLevel(key: String): Int {
val valAsVoid = notifications[key] ?: return 50
// the first implementation was a string value
return if (valAsVoid is String) {
Integer.parseInt(valAsVoid)
} else {
valAsVoid as Int
}
// the first implementation was a string value
return if (valAsVoid is String) {
valAsVoid.toInt()
} else {
valAsVoid as Int
}
return 50
}
}

View file

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

View file

@ -50,21 +50,19 @@ interface RelationService {
/**
* Sends a reaction (emoji) to the targetedEvent.
* @param reaction the reaction (preferably emoji)
* @param targetEventId the id of the event being reacted
* @param reaction the reaction (preferably emoji)
*/
fun sendReaction(reaction: String,
targetEventId: String): Cancelable
fun sendReaction(targetEventId: String,
reaction: String): Cancelable
/**
* Undo a reaction (emoji) to the targetedEvent.
* @param reaction the reaction (preferably emoji)
* @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,
targetEventId: String,
myUserId: String) // : Cancelable
fun undoReaction(targetEventId: String,
reaction: String): Cancelable
/**
* Edit a text message body. Limited to "m.text" contentType
@ -92,7 +90,7 @@ interface RelationService {
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>>)

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

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.auth.data.Credentials
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.sync.SyncTokenStore
import timber.log.Timber
import java.util.*
import javax.inject.Inject
// Legacy name: MXDeviceList
@ -39,13 +37,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
// HS not ready for retry
private val notReadyToRetryHS = HashSet<String>()
private val notReadyToRetryHS = mutableSetOf<String>()
init {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in deviceTrackingStatuses.keys) {
val status = deviceTrackingStatuses[userId]!!
for ((userId, status) in deviceTrackingStatuses) {
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.
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 {
var res = false
if (!TextUtils.isEmpty(userId) && userId.contains(":")) {
if (':' in userId) {
try {
synchronized(notReadyToRetryHS) {
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
@ -119,27 +116,23 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param changed the user ids list which have new devices
* @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
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
if (changed?.isNotEmpty() == true) {
for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
}
if (left?.isNotEmpty() == true) {
for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
isUpdated = true
}
for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
isUpdated = true
}
}
@ -153,7 +146,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* + update
*/
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>) {
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in userIds) {
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
}
userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
@ -177,21 +168,15 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
*/
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<MXDeviceInfo> {
if (failures != null) {
val keys = failures.keys
for (k in keys) {
val value = failures[k]
if (value!!.containsKey("status")) {
val statusCodeAsVoid = value["status"]
var statusCode = 0
if (statusCodeAsVoid is Double) {
statusCode = statusCodeAsVoid.toInt()
} else if (statusCodeAsVoid is Int) {
statusCode = statusCodeAsVoid.toInt()
}
if (statusCode == 503) {
synchronized(notReadyToRetryHS) {
notReadyToRetryHS.add(k)
}
for ((k, value) in failures) {
val statusCode = when (val status = value["status"]) {
is Double -> status.toInt()
is Int -> status.toInt()
else -> 0
}
if (statusCode == 503) {
synchronized(notReadyToRetryHS) {
notReadyToRetryHS.add(k)
}
}
}
@ -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.
* It must be called in getEncryptingThreadHandler() thread.
* The callback is called in the UI thread.
*
* @param userIds The users to fetch.
* @param forceDownload Always download the keys even if cached.
* @param callback the asynchronous callback
*/
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
@ -270,7 +253,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
Timber.v("## downloadKeys() : starts")
val t0 = System.currentTimeMillis()
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 {
it.addEntriesFromMap(stored)
}
@ -303,16 +286,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val devices = response.deviceKeys?.get(userId)
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
if (devices != null) {
val mutableDevices = HashMap(devices)
val deviceIds = ArrayList(mutableDevices.keys)
for (deviceId in deviceIds) {
val mutableDevices = devices.toMutableMap()
for ((deviceId, deviceInfo) in devices) {
// Get the potential previously store device keys for this device
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
val deviceInfo = mutableDevices[deviceId]
// in some race conditions (like unit tests)
// 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
}
// 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
if (!TextUtils.equals(deviceKeys.userId, userId)) {
Timber.e("## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId)
if (deviceKeys.userId != userId) {
Timber.e("## validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
return false
}
if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) {
Timber.e("## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId)
if (deviceKeys.deviceId != deviceId) {
Timber.e("## validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
return false
}
@ -379,21 +360,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val signKey = deviceKeys.keys?.get(signKeyId)
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
}
val signatureMap = deviceKeys.signatures?.get(userId)
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
}
val signature = signatureMap[signKeyId]
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
}
@ -414,7 +395,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
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
// best off sticking with the original keys.
//
@ -424,7 +405,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
Timber.e("## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys)
Timber.e("## validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
return false
}
@ -438,27 +419,18 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* This method must be called on getEncryptingThreadHandler() thread.
*/
suspend fun refreshOutdatedDeviceLists() {
val users = ArrayList<String>()
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in deviceTrackingStatuses.keys) {
if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]) {
users.add(userId)
}
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
}
if (users.size == 0) {
if (users.isEmpty()) {
return
}
// update the statuses
for (userId in users) {
val status = deviceTrackingStatuses[userId]
if (null != status && TRACKING_STATUS_PENDING_DOWNLOAD == status) {
deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS)
}
}
users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
runCatching {

View file

@ -16,7 +16,6 @@
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.session.crypto.keyshare.RoomKeysRequestListener
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.session.SessionScope
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
@ -58,7 +56,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
when (roomKeyShare?.action) {
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(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
*/
fun processReceivedRoomKeyRequests() {
val roomKeyRequestsToProcess = ArrayList(receivedRoomKeyRequests)
val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList()
receivedRoomKeyRequests.clear()
for (request in roomKeyRequestsToProcess) {
val userId = request.userId
@ -77,7 +75,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
val roomId = body!!.roomId
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) {
// TODO: determine if we sent this device the keys already: in
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
@ -92,12 +90,12 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
continue
}
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)
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")
cryptoStore.deleteIncomingRoomKeyRequest(request)
continue
@ -132,7 +130,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
synchronized(this.receivedRoomKeyRequestCancellations) {
if (!this.receivedRoomKeyRequestCancellations.isEmpty()) {
if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) {
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
this.receivedRoomKeyRequestCancellations.clear()
}

View file

@ -16,20 +16,19 @@
package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import android.util.Base64
import im.vector.matrix.android.internal.extensions.toUnsignedInt
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.nio.charset.Charset
import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.and
import kotlin.experimental.xor
import kotlin.math.min
/**
* Utility class to import/export the crypto data
@ -51,7 +50,7 @@ object MXMegolmExportEncryption {
* @return the AES key
*/
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.
*/
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)
// check we have a version byte
if (null == body || body.size == 0) {
if (null == body || body.isEmpty()) {
Timber.e("## decryptMegolmKeyFile() : Invalid file: too short")
throw Exception("Invalid file: too short")
}
@ -93,27 +92,27 @@ object MXMegolmExportEncryption {
throw Exception("Invalid file: too short")
}
if (TextUtils.isEmpty(password)) {
if (password.isEmpty()) {
throw Exception("Empty password is not supported")
}
val salt = Arrays.copyOfRange(body, 1, 1 + 16)
val iv = Arrays.copyOfRange(body, 17, 17 + 16)
val salt = body.copyOfRange(1, 1 + 16)
val iv = body.copyOfRange(17, 17 + 16)
val iterations =
(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 hmac = Arrays.copyOfRange(body, body.size - 32, body.size)
val ciphertext = body.copyOfRange(37, 37 + ciphertextLength)
val hmac = body.copyOfRange(body.size - 32, body.size)
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 mac = Mac.getInstance("HmacSHA256")
mac.init(macKey)
val digest = mac.doFinal(toVerify)
if (!Arrays.equals(hmac, digest)) {
if (!hmac.contentEquals(digest)) {
Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?")
throw Exception("Authentication check failed: incorrect password?")
}
@ -146,7 +145,7 @@ object MXMegolmExportEncryption {
@Throws(Exception::class)
@JvmOverloads
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")
}
@ -196,7 +195,7 @@ object MXMegolmExportEncryption {
System.arraycopy(cipherArray, 0, resultBuffer, 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 mac = Mac.getInstance("HmacSHA256")
@ -234,7 +233,7 @@ object MXMegolmExportEncryption {
// start the next line after the newline
lineStart = lineEnd + 1
if (TextUtils.equals(line, HEADER_LINE)) {
if (line == HEADER_LINE) {
break
}
}
@ -244,15 +243,13 @@ object MXMegolmExportEncryption {
// look for the end line
while (true) {
val lineEnd = fileStr.indexOf('\n', lineStart)
val line: String
if (lineEnd < 0) {
line = fileStr.substring(lineStart).trim()
val line = if (lineEnd < 0) {
fileStr.substring(lineStart)
} else {
line = fileStr.substring(lineStart, lineEnd).trim()
}
fileStr.substring(lineStart, lineEnd)
}.trim()
if (TextUtils.equals(line, TRAILER_LINE)) {
if (line == TRAILER_LINE) {
break
}
@ -290,7 +287,7 @@ object MXMegolmExportEncryption {
for (i in 1..nLines) {
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))
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.
// noticed as dklen/hlen
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
val key = ByteArray(64)
@ -326,8 +323,7 @@ object MXMegolmExportEncryption {
// U1 = PRF(Password, Salt || INT_32_BE(i))
prf.update(salt)
val int32BE = ByteArray(4)
Arrays.fill(int32BE, 0.toByte())
val int32BE = ByteArray(4) { 0.toByte() }
int32BE[3] = 1.toByte()
prf.update(int32BE)
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
}

View file

@ -17,7 +17,6 @@
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.util.JSON_DICT_PARAMETERIZED_TYPE
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 timber.log.Timber
import java.net.URLEncoder
import java.util.*
import javax.inject.Inject
// The libolm wrapper.
@ -434,7 +432,7 @@ internal class MXOlmDevice @Inject constructor(
* @return the base64-encoded secret key.
*/
fun getSessionKey(sessionId: String): String? {
if (!TextUtils.isEmpty(sessionId)) {
if (sessionId.isNotEmpty()) {
try {
return outboundGroupSessionStore[sessionId]!!.sessionKey()
} catch (e: Exception) {
@ -451,7 +449,7 @@ internal class MXOlmDevice @Inject constructor(
* @return the current chain index.
*/
fun getMessageIndex(sessionId: String): Int {
return if (!TextUtils.isEmpty(sessionId)) {
return if (sessionId.isNotEmpty()) {
outboundGroupSessionStore[sessionId]!!.messageIndex()
} else 0
}
@ -464,7 +462,7 @@ internal class MXOlmDevice @Inject constructor(
* @return ciphertext
*/
fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) {
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
try {
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString)
} catch (e: Exception) {
@ -523,7 +521,7 @@ internal class MXOlmDevice @Inject constructor(
}
try {
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) {
if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
session.olmInboundGroupSession!!.releaseSession()
return false
@ -573,7 +571,7 @@ internal class MXOlmDevice @Inject constructor(
}
try {
if (!TextUtils.equals(session.olmInboundGroupSession?.sessionIdentifier(), sessionId)) {
if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
continue
@ -758,7 +756,7 @@ internal class MXOlmDevice @Inject constructor(
if (session != null) {
// Check that the room id matches the original one for the session. This stops
// 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)
Timber.e("## getInboundGroupSession() : $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
import android.text.TextUtils
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.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope
import java.util.*
import javax.inject.Inject
@SessionScope
@ -42,11 +40,11 @@ internal class MyDeviceInfoHolder @Inject constructor(
init {
val keys = HashMap<String, String>()
if (!TextUtils.isEmpty(olmDevice.deviceEd25519Key)) {
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
}
if (!TextUtils.isEmpty(olmDevice.deviceCurve25519Key)) {
if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
}
@ -58,13 +56,7 @@ internal class MyDeviceInfoHolder @Inject constructor(
// Add our own deviceinfo to the store
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
val myDevices: MutableMap<String, MXDeviceInfo>
if (null != endToEndDevicesForUser) {
myDevices = HashMap(endToEndDevicesForUser)
} else {
myDevices = HashMap()
}
val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
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 org.matrix.olm.OlmAccount
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import kotlin.math.floor
import kotlin.math.min
@SessionScope
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
// discard the oldest private keys first. This will eventually clean
// 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) {
uploadOTK(oneTimeKeyCount!!, keyLimit)
} else {
@ -116,7 +117,7 @@ internal class OneTimeKeysUploader @Inject constructor(
// If we don't need to generate any more keys then we are done.
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)
val response = uploadOneTimeKeys()
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
@ -132,14 +133,14 @@ internal class OneTimeKeysUploader @Inject constructor(
*/
private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
val oneTimeKeys = olmDevice.getOneTimeKeys()
val oneTimeJson = HashMap<String, Any>()
val oneTimeJson = mutableMapOf<String, Any>()
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
if (null != curve25519Map) {
for (key_id in curve25519Map.keys) {
val k = HashMap<String, Any>()
k["key"] = curve25519Map.getValue(key_id)
for ((key_id, value) in curve25519Map) {
val k = mutableMapOf<String, Any>()
k["key"] = value
// the key is also signed
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)

View file

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

View file

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

View file

@ -16,14 +16,11 @@
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.model.MXDeviceInfo
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.store.IMXCryptoStore
import timber.log.Timber
import java.util.*
import javax.inject.Inject
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> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>()
for (userId in users) {
devicesByUser[userId] = ArrayList()
val devicesByUser = users.associateWith { userId ->
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
for (device in devices) {
val key = device.identityKey()
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
// Don't bother setting up session to ourself
continue
}
if (device.isVerified) {
// Don't bother setting up sessions with blocked users
continue
}
devicesByUser[userId]!!.add(device)
devices.filter {
// Don't bother setting up session to ourself
it.identityKey() != olmDevice.deviceCurve25519Key
// Don't bother setting up sessions with blocked users
&& !it.isVerified
}
}
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)

View file

@ -16,7 +16,6 @@
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.internal.crypto.MXCRYPTO_ALGORITHM_OLM
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.convertToUTF8
import timber.log.Timber
import java.util.*
import javax.inject.Inject
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.
*/
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage {
val deviceInfoParticipantKey = HashMap<String, MXDeviceInfo>()
val participantKeys = ArrayList<String>()
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
for (di in deviceInfos) {
participantKeys.add(di.identityKey()!!)
deviceInfoParticipantKey[di.identityKey()!!] = di
}
val payloadJson = HashMap(payloadFields)
val payloadJson = payloadFields.toMutableMap()
payloadJson["sender"] = credentials.userId
payloadJson["sender_device"] = credentials.deviceId
payloadJson["sender_device"] = credentials.deviceId!!
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
@ -67,30 +59,24 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
val ciphertext = HashMap<String, Any>()
for (deviceKey in participantKeys) {
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
val sessionId = olmDevice.getSessionId(deviceKey)
if (!TextUtils.isEmpty(sessionId)) {
if (!sessionId.isNullOrEmpty()) {
Timber.v("Using sessionid $sessionId for device $deviceKey")
val deviceInfo = deviceInfoParticipantKey[deviceKey]
payloadJson["recipient"] = deviceInfo!!.userId
val recipientsKeysMap = HashMap<String, String>()
recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!!
payloadJson["recipient_keys"] = recipientsKeysMap
payloadJson["recipient"] = deviceInfo.userId
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
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()
res.algorithm = MXCRYPTO_ALGORITHM_OLM
res.senderKey = olmDevice.deviceCurve25519Key
res.cipherText = ciphertext
return res
return EncryptedMessage(
algorithm = MXCRYPTO_ALGORITHM_OLM,
senderKey = olmDevice.deviceCurve25519Key,
cipherText = ciphertext
)
}
}

View file

@ -17,7 +17,6 @@
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.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
@ -148,7 +147,7 @@ internal class MXMegolmDecryption(private val userId: String,
selfMap["deviceId"] = "*"
recipients.add(selfMap)
if (!TextUtils.equals(sender, userId)) {
if (sender != userId) {
val senderMap = HashMap<String, String>()
senderMap["userId"] = sender
senderMap["deviceId"] = encryptedEventContent.deviceId!!
@ -176,17 +175,12 @@ internal class MXMegolmDecryption(private val userId: String,
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
if (!pendingEvents.containsKey(pendingEventsKey)) {
pendingEvents[pendingEventsKey] = HashMap()
}
val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
val events = timeline.getOrPut(timelineId) { ArrayList() }
if (pendingEvents[pendingEventsKey]?.containsKey(timelineId) == false) {
pendingEvents[pendingEventsKey]?.put(timelineId, ArrayList())
}
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)
if (event !in events) {
Timber.v("## addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
events.add(event)
}
}
@ -203,7 +197,7 @@ internal class MXMegolmDecryption(private val userId: String,
var keysClaimed: MutableMap<String, String> = HashMap()
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")
return
}
@ -250,13 +244,6 @@ internal class MXMegolmDecryption(private val userId: String,
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,
roomKeyContent.sessionKey,
roomKeyContent.roomId,

View file

@ -18,7 +18,6 @@
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.session.crypto.MXCryptoError
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.convertToUTF8
import timber.log.Timber
import java.util.*
internal class MXMegolmEncryption(
// The id of the room we will be sending to.
@ -85,7 +83,7 @@ internal class MXMegolmEncryption(
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
ArrayList(), keysClaimedMap, false)
emptyList(), keysClaimedMap, false)
keysBackup.maybeBackupKeys()
@ -115,10 +113,8 @@ internal class MXMegolmEncryption(
for (deviceId in deviceIds!!) {
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
if (!shareMap.containsKey(userId)) {
shareMap[userId] = ArrayList()
}
shareMap[userId]!!.add(deviceInfo)
val devices = shareMap.getOrPut(userId) { ArrayList() }
devices.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)
val subMap = HashMap<String, List<MXDeviceInfo>>()
val userIds = ArrayList<String>()
var devicesCount = 0
for (userId in devicesByUsers.keys) {
devicesByUsers[userId]?.let {
userIds.add(userId)
subMap[userId] = it
devicesCount += it.size
}
for ((userId, devices) in devicesByUsers) {
subMap[userId] = devices
devicesCount += devices.size
if (devicesCount > 100) {
break
}
}
Timber.v("## shareKey() ; userId $userIds")
Timber.v("## shareKey() ; userId ${subMap.keys}")
shareUserDevicesKey(session, subMap)
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() }
val remainingDevices = devicesByUsers - subMap.keys
shareKey(session, remainingDevices)
}
@ -164,7 +156,6 @@ internal class MXMegolmEncryption(
*
* @param session the session info
* @param devicesByUser the devices map
* @param callback the asynchronous callback
*/
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
devicesByUser: Map<String, List<MXDeviceInfo>>) {
@ -210,8 +201,7 @@ internal class MXMegolmEncryption(
continue
}
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
haveTargets = true
}
}
@ -228,9 +218,8 @@ internal class MXMegolmEncryption(
// 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
// for dead devices on every message.
for (userId in devicesByUser.keys) {
val devicesToShareWith = devicesByUser[userId]
for ((deviceId) in devicesToShareWith!!) {
for ((userId, devicesToShareWith) in devicesByUser) {
for ((deviceId) in devicesToShareWith) {
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
}
}
@ -272,7 +261,6 @@ internal class MXMegolmEncryption(
* This method must be called in getDecryptingThreadHandler() thread.
*
* @param userIds the user ids whose devices must be checked.
* @param callback the asynchronous callback
*/
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> {
// We are happy to use a cached version here: we assume that if we already
@ -304,7 +292,7 @@ internal class MXMegolmEncryption(
continue
}
if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) {
if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) {
// Don't bother sending to ourself
continue
}

View file

@ -18,7 +18,6 @@
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.toContent
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.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import java.util.*
internal class MXOlmEncryption(
private var roomId: String,
@ -49,7 +47,7 @@ internal class MXOlmEncryption(
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
for (device in devices) {
val key = device.identityKey()
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
if (key == olmDevice.deviceCurve25519Key) {
// Don't bother setting up session to ourself
continue
}
@ -61,13 +59,14 @@ internal class MXOlmEncryption(
}
}
val messageMap = HashMap<String, Any>()
messageMap["room_id"] = roomId
messageMap["type"] = eventType
messageMap["content"] = eventContent
val messageMap = mapOf(
"room_id" to roomId,
"type" to eventType,
"content" to eventContent
)
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.VisibleForTesting
import androidx.annotation.WorkerThread
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
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.db.model.KeysBackupDataEntity
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.session.SessionScope
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.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.awaitCallback
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -77,6 +78,7 @@ import kotlin.random.Random
@SessionScope
internal class KeysBackup @Inject constructor(
@UserId private val userId: String,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val olmDevice: MXOlmDevice,
@ -142,8 +144,8 @@ internal class KeysBackup @Inject constructor(
progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>) {
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
runCatching {
withContext(coroutineDispatchers.crypto) {
val olmPkDecryption = OlmPkDecryption()
val megolmBackupAuthData = MegolmBackupAuthData()
@ -375,8 +377,6 @@ internal class KeysBackup @Inject constructor(
*/
@WorkerThread
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
val myUserId = credentials.userId
val keysBackupVersionTrust = KeysBackupVersionTrust()
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
@ -388,13 +388,13 @@ internal class KeysBackup @Inject constructor(
return keysBackupVersionTrust
}
val mySigs = authData.signatures?.get(myUserId)
val mySigs = authData.signatures?.get(userId)
if (mySigs.isNullOrEmpty()) {
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
return keysBackupVersionTrust
}
for (keyId in mySigs.keys) {
for ((keyId, mySignature) in mySigs) {
// XXX: is this how we're supposed to get the device id?
var deviceId: String? = null
val components = keyId.split(":")
@ -403,7 +403,7 @@ internal class KeysBackup @Inject constructor(
}
if (deviceId != null) {
val device = cryptoStore.getUserDevice(deviceId, myUserId)
val device = cryptoStore.getUserDevice(deviceId, userId)
var isSignatureValid = false
if (device == null) {
@ -412,7 +412,7 @@ internal class KeysBackup @Inject constructor(
val fingerprint = device.fingerprint()
if (fingerprint != null) {
try {
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySigs[keyId] as String)
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
isSignatureValid = true
} catch (e: OlmException) {
Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
@ -450,10 +450,8 @@ internal class KeysBackup @Inject constructor(
} else {
GlobalScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
val myUserId = credentials.userId
// Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap()
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
?: HashMap()
if (trust) {
@ -462,7 +460,7 @@ internal class KeysBackup @Inject constructor(
val deviceSignatures = objectSigner.signObject(canonicalJson)
deviceSignatures[myUserId]?.forEach { entry ->
deviceSignatures[userId]?.forEach { entry ->
myUserSignatures[entry.key] = entry.value
}
} else {
@ -478,7 +476,7 @@ internal class KeysBackup @Inject constructor(
val newMegolmBackupAuthData = authData.copy()
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
newSignatures[myUserId] = myUserSignatures
newSignatures[userId] = myUserSignatures
newMegolmBackupAuthData.signatures = newSignatures
@ -617,8 +615,8 @@ internal class KeysBackup @Inject constructor(
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try<OlmPkDecryption> {
runCatching {
val decryption = withContext(coroutineDispatchers.crypto) {
// Check if the recovery is valid before going any further
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
@ -626,85 +624,66 @@ internal class KeysBackup @Inject constructor(
}
// Get a PK decryption instance
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
if (decryption == null) {
// This should not happen anymore
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
throw InvalidParameterException("Invalid recovery key")
}
decryption
pkDecryptionFromRecoveryKey(recoveryKey)
}
if (decryption == null) {
// This should not happen anymore
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
throw InvalidParameterException("Invalid recovery key")
}
}.fold(
{
callback.onFailure(it)
},
{ decryption ->
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
// Get backed up keys from the homeserver
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> {
override fun onSuccess(data: KeysBackupData) {
GlobalScope.launch(coroutineDispatchers.main) {
val importRoomKeysResult = withContext(coroutineDispatchers.crypto) {
val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data
var sessionsFromHsCount = 0
for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) {
for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) {
sessionsFromHsCount++
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!!
// Get backed up keys from the homeserver
val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
withContext(coroutineDispatchers.crypto) {
val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data
var sessionsFromHsCount = 0
for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
sessionsFromHsCount++
sessionData?.let {
sessionsData.add(it)
}
}
}
Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" +
" of $sessionsFromHsCount from the backup store on the homeserver")
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
// Do not trigger a backup for them if they come from the backup version we are using
val backUp = keysVersionResult.version != keysBackupVersion?.version
if (backUp) {
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" +
" to backup version: ${keysBackupVersion?.version}")
}
// Import them into the crypto store
val progressListener = if (stepProgressListener != null) {
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
}
}
} else {
null
}
val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener)
// Do not back up the key if it comes from a backup recovery
if (backUp) {
maybeBackupKeys()
}
result
}
callback.onSuccess(importRoomKeysResult)
}
sessionData?.let {
sessionsData.add(it)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
}
}
)
Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" +
" of $sessionsFromHsCount from the backup store on the homeserver")
// Do not trigger a backup for them if they come from the backup version we are using
val backUp = keysVersionResult.version != keysBackupVersion?.version
if (backUp) {
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" +
" to backup version: ${keysBackupVersion?.version}")
}
// Import them into the crypto store
val progressListener = if (stepProgressListener != null) {
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
}
}
} else {
null
}
val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener)
// Do not back up the key if it comes from a backup recovery
if (backUp) {
maybeBackupKeys()
}
result
}
}.foldToCallback(callback)
}
}
@ -717,7 +696,7 @@ internal class KeysBackup @Inject constructor(
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
runCatching {
val progressListener = if (stepProgressListener != null) {
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
@ -730,22 +709,18 @@ internal class KeysBackup @Inject constructor(
null
}
Try {
val recoveryKey = withContext(coroutineDispatchers.crypto) {
recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
}
}.fold(
{
callback.onFailure(it)
},
{ recoveryKey ->
if (recoveryKey == null) {
Timber.v("backupKeys: Invalid configuration")
callback.onFailure(IllegalStateException("Invalid configuration"))
} else {
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback)
}
if (recoveryKey == null) {
Timber.v("backupKeys: Invalid configuration")
throw IllegalStateException("Invalid configuration")
} else {
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
* parameters and always returns a KeysBackupData object through the Callback
*/
private fun getKeys(sessionId: String?,
private suspend fun getKeys(sessionId: String?,
roomId: String?,
version: String,
callback: MatrixCallback<KeysBackupData>) {
if (roomId != null && sessionId != null) {
version: String): KeysBackupData {
return if (roomId != null && sessionId != null) {
// Get key for the room and for the session
getRoomSessionDataTask
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) {
this.callback = object : MatrixCallback<KeyBackupData> {
override fun onSuccess(data: KeyBackupData) {
// Convert to KeysBackupData
val keysBackupData = KeysBackupData()
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
val roomKeysBackupData = RoomKeysBackupData()
roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData
callback.onSuccess(keysBackupData)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
// Convert to KeysBackupData
KeysBackupData(mutableMapOf(
roomId to RoomKeysBackupData(mutableMapOf(
sessionId to data
))
))
} else if (roomId != null) {
// Get all keys for the room
getRoomSessionsDataTask
.configureWith(GetRoomSessionsDataTask.Params(roomId, version)) {
this.callback = object : MatrixCallback<RoomKeysBackupData> {
override fun onSuccess(data: RoomKeysBackupData) {
// Convert to KeysBackupData
val keysBackupData = KeysBackupData()
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data
callback.onSuccess(keysBackupData)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
// Convert to KeysBackupData
KeysBackupData(mutableMapOf(roomId to data))
} else {
// Get all keys
getSessionsDataTask
.configureWith(GetSessionsDataTask.Params(version)) {
this.callback = callback
}
.executeBy(taskExecutor)
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
}
}
@ -1411,5 +1352,5 @@ internal class KeysBackup @Inject constructor(
* 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
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.MegolmSessionData
import org.matrix.olm.OlmInboundGroupSession
import timber.log.Timber
import java.io.Serializable
import java.util.*
/**
* This class adds more context to a OlmInboundGroupSession object.
@ -91,7 +89,7 @@ class OlmInboundGroupSessionWrapper : Serializable {
try {
olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!)
if (!TextUtils.equals(olmInboundGroupSession!!.sessionIdentifier(), megolmSessionData.sessionId)) {
if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) {
throw Exception("Mismatched group session Id")
}

View file

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

View file

@ -44,18 +44,14 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private
}
val map = MXUsersDevicesMap<MXKey>()
keysClaimResponse.oneTimeKeys?.let { oneTimeKeys ->
for (userId in oneTimeKeys.keys) {
val mapByUserId = oneTimeKeys[userId]
for ((userId, mapByUserId) in oneTimeKeys) {
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) {
map.setObject(userId, deviceId, mxKey)
} else {
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
}
if (mxKey != null) {
map.setObject(userId, deviceId, mxKey)
} else {
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
}
}
}

View file

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

View file

@ -16,7 +16,6 @@
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.model.rest.UpdateDeviceInfoBody
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) {
val body = UpdateDeviceInfoBody(
displayName = if (TextUtils.isEmpty(params.deviceName)) "" else params.deviceName
displayName = params.deviceName
)
return executeRequest {
apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body)

View file

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

View file

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

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.network
import android.content.Context
import android.text.TextUtils
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.di.MatrixScope
import timber.log.Timber
@ -60,10 +59,10 @@ internal class UserAgentHolder @Inject constructor(private val context: Context)
Timber.e(e, "## initUserAgent() : failed")
}
var systemUserAgent = System.getProperty("http.agent")
val systemUserAgent = System.getProperty("http.agent")
// cannot retrieve the application version
if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(appVersion)) {
if (appName.isEmpty() || appVersion.isEmpty()) {
if (null == systemUserAgent) {
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) {
states[key] = state
mainHandler.post {
listeners[key]?.also { listeners ->
listeners.forEach { it.onUpdate(state) }
}
listeners[key]?.forEach { it.onUpdate(state) }
}
}
}

View file

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

View file

@ -50,19 +50,15 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
defaultPushRuleService.dispatchRoomJoined(it)
}
val newJoinEvents = params.syncResponse.join
.map { entries ->
entries.value.timeline?.events?.map { it.copy(roomId = entries.key) }
.mapNotNull { (key, value) ->
value.timeline?.events?.map { it.copy(roomId = key) }
}
.fold(emptyList<Event>(), { acc, next ->
acc + (next ?: emptyList())
})
.flatten()
val inviteEvents = params.syncResponse.invite
.map { entries ->
entries.value.inviteState?.events?.map { it.copy(roomId = entries.key) }
.mapNotNull { (key, value) ->
value.inviteState?.events?.map { it.copy(roomId = key) }
}
.fold(emptyList<Event>(), { acc, next ->
acc + (next ?: emptyList())
})
.flatten()
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
when (event.type) {
EventType.MESSAGE,
@ -84,16 +80,12 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
}
val allRedactedEvents = params.syncResponse.join
.map { entries ->
entries.value.timeline?.events?.filter {
it.type == EventType.REDACTION
}
.orEmpty()
.mapNotNull { it.redacts }
}
.fold(emptyList<String>(), { acc, next ->
acc + next
})
.asSequence()
.mapNotNull { (_, value) -> value.timeline?.events }
.flatten()
.filter { it.type == EventType.REDACTION }
.mapNotNull { it.redacts }
.toList()
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? {
// TODO This should be injected
val conditionResolver = DefaultConditionResolver(event, roomService, userId)
rules.filter { it.enabled }.forEach { rule ->
val isFullfilled = rule.conditions?.map {
return rules.firstOrNull { rule ->
// All conditions must hold true for an event in order to apply the action for the event.
rule.enabled && rule.conditions?.all {
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.
acc && next
}) ?: false
if (isFullfilled) {
return rule
}
} ?: false
}
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.model.RoomSummary
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.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
@ -44,18 +45,20 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val sendService: SendService,
private val draftService: DraftService,
private val stateService: StateService,
private val reportingService: ReportingService,
private val readService: ReadService,
private val cryptoService: CryptoService,
private val relationService: RelationService,
private val roomMembersService: MembershipService
) : Room,
TimelineService by timelineService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,
ReadService by readService,
RelationService by relationService,
MembershipService by roomMembersService {
private val roomMembersService: MembershipService) :
Room,
TimelineService by timelineService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,
ReportingService by reportingService,
ReadService by readService,
RelationService by relationService,
MembershipService by roomMembersService {
override fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> {
val liveData = monarchy.findAllMappedWithChanges(

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.session.room.create.CreateRoomTask
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.configureWith
import io.realm.Realm
@ -41,6 +42,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val roomSummaryMapper: RoomSummaryMapper,
private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService {
@ -80,4 +82,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
}
.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.joining.InviteBody
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.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
@ -245,4 +246,16 @@ internal interface RoomAPI {
@Path("eventId") parent_id: String,
@Body reason: Map<String, String>
): 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.read.DefaultReadService
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.state.DefaultStateService
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 draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
private val reportingServiceFactory: DefaultReportingService.Factory,
private val readServiceFactory: DefaultReadService.Factory,
private val relationServiceFactory: DefaultRelationService.Factory,
private val membershipServiceFactory: DefaultMembershipService.Factory) :
@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
sendServiceFactory.create(roomId),
draftServiceFactory.create(roomId),
stateServiceFactory.create(roomId),
reportingServiceFactory.create(roomId),
readServiceFactory.create(roomId),
cryptoService,
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.prune.DefaultPruneEventTask
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.MarkAllRoomsReadTask
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.DefaultFindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
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.relation.*
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
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.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
@Module
@ -110,6 +104,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
@Binds
abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
@Binds
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
@ -119,6 +116,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
@Binds
abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
@Binds
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask

View file

@ -132,8 +132,7 @@ internal class RoomMembers(private val realm: Realm,
.findAll()
.map { it.asDomain() }
.associateBy { it.stateKey!! }
.mapValues { it.value.content.toModel<RoomMember>()!! }
.filterValues { predicate(it) }
.filterValues { predicate(it.content.toModel<RoomMember>()!!) }
.keys
.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
}
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also {
saveLocalEcho(it)
@ -75,13 +75,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
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(
roomId,
targetEventId,
reaction,
myUserId
reaction
)
// TODO We should avoid using MatrixCallback internally
val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
if (data.redactEventId == null) {
@ -89,7 +89,6 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
// TODO?
}
data.redactEventId?.let { toRedact ->
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
saveLocalEcho(it)
}
@ -99,7 +98,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
}
}
}
findReactionEventForUndoTask
return findReactionEventForUndoTask
.configureWith(params) {
this.retryCount = Int.MAX_VALUE
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.ReactionAggregatedSummaryEntityFields
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 io.realm.Realm
import javax.inject.Inject
@ -29,8 +30,7 @@ internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoT
data class Params(
val roomId: String,
val eventId: String,
val reaction: String,
val myUserId: String
val reaction: String
)
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 {
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)
}
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? {
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
if (summary != null) {
summary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
.findFirst()?.let {
// want to find the event orignated by me!
it.sourceEvents.forEach {
// find source event
EventEntity.where(realm, it).findFirst()?.let { eventEntity ->
// is it mine?
if (eventEntity.sender == userId) {
return eventEntity
}
}
}
}
}
return null
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? {
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null
val rase = summary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
.findFirst() ?: return null
// want to find the event orignated by me!
return rase.sourceEvents
.asSequence()
.mapNotNull {
// find source event
EventEntity.where(realm, it).findFirst()
}
.firstOrNull { eventEntity ->
// is it mine?
eventEntity.sender == userId
}
}
}

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.ReactionAggregatedSummaryEntityFields
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 io.realm.Realm
import javax.inject.Inject
@ -30,8 +31,7 @@ internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params
val roomId: String,
val eventId: String,
val reaction: String,
val oppositeReaction: String,
val myUserId: String
val oppositeReaction: String
)
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 {
var res: Pair<String?, List<String>?>? = null
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())
}
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
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
?: return Pair(reaction, null)
@ -68,7 +69,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
// find source event
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)
} else {
@ -77,7 +78,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
// find source event
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)
}

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

View file

@ -16,7 +16,6 @@
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.events.model.Event
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())
// Decrypt event if necessary
decryptEvent(event, null)
if (TextUtils.equals(event.getClearType(), EventType.MESSAGE)
if (event.getClearType() == EventType.MESSAGE
&& 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 {
sasVerificationService.onToDeviceEvent(event)
cryptoService.onToDeviceEvent(event)

View file

@ -25,7 +25,7 @@ import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
// the receipts dictionnaries
// the receipts dictionaries
// key : $EventId
// value : dict key $UserId
// 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 timber.log.Timber
import java.net.SocketTimeoutException
import java.util.*
import java.util.Timer
import java.util.TimerTask
/**
* 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>> {
return Realm.getInstance(realmConfiguration).use { realm ->
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
val directChatsMap = mutableMapOf<String, MutableList<String>>()
for (directRoom in currentDirectRooms) {
if (directRoom.roomId == filterRoomId) continue
val directUserId = directRoom.directUserId ?: continue
directChatsMap
.getOrPut(directUserId, { arrayListOf() })
.apply {
add(directRoom.roomId)
}
}
directChatsMap
RoomSummaryEntity.getDirectRooms(realm)
.asSequence()
.filter { it.roomId != filterRoomId && it.directUserId != null }
.groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId })
}
}
}

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.util.Cancelable
import java.util.*
import java.util.UUID
internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS,
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 var sSecretKeyAndVersion: SecretKeyAndVersion? = null
private var sPrng: SecureRandom? = null
/**
* Returns the unique SecureRandom instance shared for all local storage encryption operations.
*/
private val prng: SecureRandom
get() {
if (sPrng == null) {
sPrng = SecureRandom()
}
return sPrng!!
}
private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() }
/**
* Create a GZIPOutputStream instance

View file

@ -24,12 +24,9 @@ import java.security.MessageDigest
fun String.md5() = try {
val digest = MessageDigest.getInstance("md5")
digest.update(toByteArray())
val bytes = digest.digest()
val sb = StringBuilder()
for (i in bytes.indices) {
sb.append(String.format("%02X", bytes[i]))
}
sb.toString().toLowerCase()
digest.digest()
.joinToString("") { String.format("%02X", it) }
.toLowerCase()
} catch (exc: Exception) {
// Should not happen, but just in case
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.TimelineSettings
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import org.junit.Assert
import org.junit.Test
@ -172,7 +173,6 @@ class PushrulesConditionTest {
}
class MockRoomService() : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
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>> {
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 {
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? {
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 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.
}
@ -250,7 +262,7 @@ class PushrulesConditionTest {
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.
}
@ -330,11 +342,11 @@ class PushrulesConditionTest {
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.
}
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.
}
@ -347,7 +359,7 @@ class PushrulesConditionTest {
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.
}

View file

@ -281,7 +281,7 @@ dependencies {
// UI
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 "ru.noties.markwon:core:$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 {
menuInflater.inflate(R.menu.vector_home, menu)
menuInflater.inflate(R.menu.home, menu)
return true
}
}

View file

@ -21,7 +21,6 @@ package im.vector.riotx.gplay.push.fcm
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.google.firebase.messaging.FirebaseMessagingService
@ -214,10 +213,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
}
} else {
if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
if (notifiableEvent.senderName.isNullOrEmpty()) {
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
}
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
if (notifiableEvent.roomName.isNullOrEmpty()) {
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.readreceipts.DisplayReadReceiptsBottomSheet
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.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView
@ -104,12 +106,10 @@ interface ScreenComponent {
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
fun inject(messageMenuFragment: MessageMenuFragment)
fun inject(vectorSettingsActivity: VectorSettingsActivity)
fun inject(createRoomFragment: CreateRoomFragment)
@ -136,8 +136,6 @@ interface ScreenComponent {
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
fun inject(quickReactionFragment: QuickReactionFragment)
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
fun inject(loginActivity: LoginActivity)

View file

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

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.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.core.util.PatternsCompat.WEB_URL
import java.util.*
/**
* 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
// 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)
if (null == message) {
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
if (null != sequence) {
message = sequence.toString()
}
}
?: intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
if (!TextUtils.isEmpty(subject)) {
if (TextUtils.isEmpty(message)) {
if (!subject.isNullOrEmpty()) {
if (message.isNullOrEmpty()) {
message = subject
} else if (WEB_URL.matcher(message!!).matches()) {
} else if (WEB_URL.matcher(message).matches()) {
message = subject + "\n" + message
}
}
if (!TextUtils.isEmpty(message)) {
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type))
if (!message.isNullOrEmpty()) {
externalIntentDataList.add(ExternalIntentData.IntentDataText(message, null, intent.type))
return externalIntentDataList
}
}
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) {
clipData = intent.clipData
@ -106,41 +98,26 @@ fun analyseIntent(intent: Intent): List<ExternalIntentData> {
if (null != clipData) {
if (null != clipData.description) {
if (0 != clipData.description.mimeTypeCount) {
mimetypes = ArrayList()
for (i in 0 until clipData.description.mimeTypeCount) {
mimetypes.add(clipData.description.getMimeType(i))
mimeTypes = with(clipData.description) {
List(mimeTypeCount) { getMimeType(it) }
}
// if the filter is "accept anything" the mimetype does not make sense
if (1 == mimetypes.size) {
if (mimetypes[0].endsWith("/*")) {
mimetypes = null
if (1 == mimeTypes.size) {
if (mimeTypes[0].endsWith("/*")) {
mimeTypes = null
}
}
}
}
val count = clipData.itemCount
for (i in 0 until count) {
for (i in 0 until clipData.itemCount) {
val item = clipData.getItemAt(i)
var mimetype: String? = null
val mimeType = mimeTypes?.getOrElse(i) { mimeTypes[0] }
// uris list is not a valid mimetype
.takeUnless { it == ClipDescription.MIMETYPE_TEXT_URILIST }
if (null != mimetypes) {
if (i < mimetypes.size) {
mimetype = mimetypes[i]
} else {
mimetype = mimetypes[0]
}
// uris list is not a valid mimetype
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
mimetype = null
}
}
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype))
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimeType))
}
} else if (null != intent.data) {
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))

View file

@ -13,18 +13,23 @@
* See the License for the specific language governing permissions and
* 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.os.Bundle
import android.os.Parcelable
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.MvRxView
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 im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.DimensionConverter
import java.util.*
/**
@ -37,10 +42,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
private lateinit var screenComponent: ScreenComponent
final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
private var bottomSheetBehavior: BottomSheetBehavior<FrameLayout>? = null
val vectorBaseActivity: VectorBaseActivity by lazy {
activity as VectorBaseActivity
}
open val showExpanded = false
override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
super.onAttach(context)
@ -57,6 +66,17 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
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) {
super.onSaveInstanceState(outState)
mvrxViewModelStore.saveViewModels(outState)
@ -70,6 +90,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
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) {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
}

View file

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

View file

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

View file

@ -20,7 +20,6 @@ import android.content.Context
import android.graphics.Color
import android.text.SpannableString
import android.text.TextPaint
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.AttributeSet
@ -168,7 +167,7 @@ class NotificationAreaView @JvmOverloads constructor(
} else {
imageView.setImageResource(R.drawable.scrolldown)
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)
}
}

View file

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

View file

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

View file

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

View file

@ -24,9 +24,9 @@ import java.util.*
object TextUtils {
private val suffixes = TreeMap<Int, String>().also {
it.put(1000, "k")
it.put(1000000, "M")
it.put(1000000000, "G")
it[1000] = "k"
it[1000000] = "M"
it[1000000000] = "G"
}
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.Bundle
import android.text.TextUtils
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
@ -122,7 +121,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
})
viewModel.passphrase.observe(this, Observer<String> { newValue ->
if (TextUtils.isEmpty(newValue)) {
if (newValue.isEmpty()) {
viewModel.passwordStrength.value = null
} else {
AsyncTask.execute {
@ -172,7 +171,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
@OnClick(R.id.keys_backup_setup_step2_button)
fun doNext() {
when {
TextUtils.isEmpty(viewModel.passphrase.value) -> {
viewModel.passphrase.value.isNullOrEmpty() -> {
viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message)
}
viewModel.passphrase.value != viewModel.confirmPassphrase.value -> {
@ -192,7 +191,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
@OnClick(R.id.keys_backup_setup_step2_skip_button)
fun skipPassphrase() {
when {
TextUtils.isEmpty(viewModel.passphrase.value) -> {
viewModel.passphrase.value.isNullOrEmpty() -> {
// Generate a recovery key for the user
viewModel.megolmBackupCreationInfo = null

View file

@ -20,7 +20,6 @@
package im.vector.riotx.features.crypto.keysrequest
import android.content.Context
import android.text.TextUtils
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
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 java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.ArrayList
@ -100,7 +100,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
alertsToRequests[mappingKey] = ArrayList<IncomingRoomKeyRequest>().apply { this.add(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>) {
val deviceInfo = data.getObject(userId, deviceId)
@ -147,7 +147,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
wasNewDevice: Boolean,
deviceInfo: MXDeviceInfo?,
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?
if (moreInfo != null) {
@ -244,12 +244,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
val deviceId = request.deviceId
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")
return
}
val alertMgrUniqueKey = alertManagerId(deviceId!!, userId!!)
val alertMgrUniqueKey = alertManagerId(deviceId, userId)
alertsToRequests[alertMgrUniqueKey]?.removeAll {
it.deviceId == request.deviceId
&& it.userId == request.userId

View file

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

View file

@ -30,9 +30,9 @@ sealed class RoomDetailActions {
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : 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 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 NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
@ -49,6 +49,9 @@ sealed class RoomDetailActions {
data class ResendMessage(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 ResendAll : RoomDetailActions()
}

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.net.Uri
@ -50,6 +51,7 @@ import com.airbnb.mvrx.*
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
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.riotx.R
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.error.ErrorFormatter
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.readreceipts.DisplayReadReceiptsBottomSheet
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.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView
@ -274,6 +281,10 @@ class RoomDetailFragment :
syncStateView.render(syncState)
}
roomDetailViewModel.requestLiveData.observeEvent(this) {
displayRoomDetailActionResult(it)
}
if (savedInstanceState == null) {
when (val sharedData = roomDetailArgs.sharedData) {
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
@ -281,6 +292,7 @@ class RoomDetailFragment :
null -> Timber.v("No share data to process")
}
}
}
override fun onDestroy() {
@ -438,7 +450,7 @@ class RoomDetailFragment :
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
// 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) {
AlertDialog.Builder(activity!!)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.command_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.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 ************************************************************
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 {
// Same room?
if (roomId == roomDetailArgs.roomId) {
@ -760,6 +836,14 @@ class RoomDetailFragment :
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 {
@ -872,7 +956,7 @@ class RoomDetailFragment :
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
if (on) {
// 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 {
// I need to redact a reaction
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
@ -880,7 +964,7 @@ class RoomDetailFragment :
}
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
@ -929,23 +1013,23 @@ class RoomDetailFragment :
private fun handleActions(action: SimpleAction) {
when (action) {
is SimpleAction.AddReaction -> {
is SimpleAction.AddReaction -> {
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
}
is SimpleAction.ViewReactions -> {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
is SimpleAction.ViewReactions -> {
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
is SimpleAction.Copy -> {
is SimpleAction.Copy -> {
// I need info about the current selected message :/
copyToClipboard(requireContext(), action.content, false)
val msg = requireContext().getString(R.string.copied_to_clipboard)
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
}
is SimpleAction.Delete -> {
is SimpleAction.Delete -> {
roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
}
is SimpleAction.Share -> {
is SimpleAction.Share -> {
// TODO current data communication is too limited
// Need to now the media type
// TODO bad, just POC
@ -973,10 +1057,10 @@ class RoomDetailFragment :
}
)
}
is SimpleAction.ViewEditHistory -> {
is SimpleAction.ViewEditHistory -> {
onEditedDecorationClicked(action.messageInformationData)
}
is SimpleAction.ViewSource -> {
is SimpleAction.ViewSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content
@ -987,7 +1071,7 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok, null)
.show()
}
is SimpleAction.ViewDecryptedSource -> {
is SimpleAction.ViewDecryptedSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content
@ -998,31 +1082,40 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok, null)
.show()
}
is SimpleAction.QuickReact -> {
is SimpleAction.QuickReact -> {
// eventId,ClickedOn,Add
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
}
is SimpleAction.Edit -> {
is SimpleAction.Edit -> {
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString()))
}
is SimpleAction.Quote -> {
is SimpleAction.Quote -> {
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString()))
}
is SimpleAction.Reply -> {
is SimpleAction.Reply -> {
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString()))
}
is SimpleAction.CopyPermalink -> {
is SimpleAction.CopyPermalink -> {
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
}
is SimpleAction.Resend -> {
is SimpleAction.Resend -> {
roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId))
}
is SimpleAction.Remove -> {
is SimpleAction.Remove -> {
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
}
else -> {
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 -> {
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
import android.text.TextUtils
import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.*
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
@ -92,6 +88,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
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
var pendingAction: RoomDetailActions? = null
@ -150,6 +151,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.ResendAll -> handleResendAll()
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
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 renderer = HtmlRenderer.builder().build()
val htmlText = renderer.render(document)
if (TextUtils.equals(finalText, htmlText)) {
if (finalText == htmlText) {
room.sendTextMessage(finalText)
} else {
room.sendFormattedTextMessage(finalText, htmlText)
@ -396,19 +398,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
val quotedTextMsg = StringBuilder()
if (messageParagraphs != null) {
for (i in messageParagraphs.indices) {
if (messageParagraphs[i].trim() != "") {
quotedTextMsg.append("> ").append(messageParagraphs[i])
}
return buildString {
if (messageParagraphs != null) {
for (i in messageParagraphs.indices) {
if (messageParagraphs[i].isNotBlank()) {
append("> ")
append(messageParagraphs[i])
}
if (i + 1 != messageParagraphs.size) {
quotedTextMsg.append("\n\n")
if (i != messageParagraphs.lastIndex) {
append("\n\n")
}
}
}
append("\n\n")
append(myText)
}
return "$quotedTextMsg\n\n$myText"
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
@ -440,7 +445,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleSendReaction(action: RoomDetailActions.SendReaction) {
room.sendReaction(action.reaction, action.targetEventId)
room.sendReaction(action.targetEventId, action.reaction)
}
private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
@ -449,14 +454,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
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) {
if (action.add) {
room.sendReaction(action.selectedReaction, action.targetEventId)
room.sendReaction(action.targetEventId, action.selectedReaction)
} 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> {})
}
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() {
session.rx()
.liveSyncState()

View file

@ -21,19 +21,18 @@ import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args
import im.vector.riotx.R
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 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
@Parcelize
@ -48,8 +47,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var epoxyController: DisplayReadReceiptsController
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
@ -58,24 +57,20 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
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)
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recyclerView.adapter = epoxyController.adapter
bottomSheetTitle.text = getString(R.string.read_at)
epoxyController.setData(displayReadReceiptArgs.readReceipts)
}
override fun invalidate() {
// we are not using state for this one as it's static
}
// we are not using state for this one as it's static, so no need to override invalidate()
companion object {
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
import android.app.Dialog
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.TextView
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
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.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 kotlinx.android.synthetic.main.bottom_sheet_message_actions.*
import javax.inject.Inject
/**
* 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)
override val showExpanded = true
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) {
screenComponent.inject(this)
}
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)
return view
}
@ -78,78 +62,26 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
val cfm = childFragmentManager
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
if (menuActionFragment == null) {
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction()
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
.commit()
}
menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener {
override fun didSelectMenuAction(simpleAction: SimpleAction) {
actionHandlerModel.fireAction(simpleAction)
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()
}
}
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
recyclerView.adapter = messageActionsEpoxyController.adapter
// Disable item animation
recyclerView.itemAnimator = null
messageActionsEpoxyController.listener = this
}
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
}
}
override fun didSelectMenuAction(simpleAction: SimpleAction) {
if (simpleAction is SimpleAction.ReportContent) {
// Toggle report menu
viewModel.toggleReportMenu()
} else {
actionHandlerModel.fireAction(simpleAction)
dismiss()
}
return dialog
}
override fun invalidate() = withState(viewModel) {
val body = viewModel.resolveBody(it)
if (body != null) {
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
messageActionsEpoxyController.setData(it)
super.invalidate()
}
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 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.MessageTextContent
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.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.unwrap
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.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
import java.text.SimpleDateFormat
import java.util.*
/**
* Quick reactions state
*/
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class MessageActionState(
val roomId: String,
val eventId: String,
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 {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
@ -49,18 +71,101 @@ data class MessageActionState(
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 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()) {
EventType.MESSAGE -> {
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer?.render(messageContent.formattedBody
?: messageContent.body)
eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body)
} else {
messageContent?.body
}
@ -72,54 +177,177 @@ data class MessageActionState(
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> {
timelineEvent()?.let { noticeEventFormatter?.format(it) }
timelineEvent()?.let { noticeEventFormatter.format(it) }
}
else -> null
}
}
}
/**
* 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>,
session: Session,
private val noticeEventFormatter: NoticeEventFormatter
) : VectorViewModel<MessageActionState>(initialState) {
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
val event = optionalEvent.getOrNull() ?: return emptyList()
private val eventId = initialState.eventId
private val room = session.getRoom(initialState.roomId)
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel()
val type = messageContent?.type
@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel
}
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))
}
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
if (canEdit(event, session.myUserId)) {
add(SimpleAction.Edit(eventId))
}
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageActionViewModelFactory.create(state)
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.ReportContent(eventId))
}
}
}
}
init {
observeEvent()
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
return false
}
private fun observeEvent() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.unwrap()
.execute {
copy(timelineEvent = it)
}
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
}
}
fun resolveBody(state: MessageActionState): CharSequence? {
return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter)
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
* 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.view.LayoutInflater
@ -21,17 +21,20 @@ import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
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.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.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
/**
@ -44,8 +47,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
private val epoxyController by lazy {
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
@ -56,22 +59,23 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
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)
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
recyclerView.adapter = epoxyController.adapter
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = context?.getString(R.string.message_edits)
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate()
}
companion object {

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