Merge remote-tracking branch 'upstream/master' into sc_1.0.7

Change-Id: Iccb1b1f0bee6b77c9f15d917c8073910841b6ff8

Conflicts:
	vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
	vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt
	vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt
This commit is contained in:
SpiritCroc 2020-09-17 12:03:17 +02:00
commit 2e9195df2b
197 changed files with 5319 additions and 1067 deletions

View File

@ -1,3 +1,25 @@
Changes in Element 1.0.7 (2020-09-17)
===================================================
Improvements 🙌:
- Handle date formatting properly (show time am/pm if needed, display year when needed)
- Improve F-Droid Notification (#2055)
Bugfix 🐛:
- Clear the notification when the event is read elsewhere (#1822)
- Speakerphone is not used for ringback tone (#1644, #1645)
- Back camera preview is not mirrored anymore (#1776)
- Various report of people that cannot play video (#2107)
- Rooms incorrectly marked as unread (#588)
- Allow users to show/hide room member state events (#1231)
- Fix stuck on loader when launching home
SDK API changes ⚠️:
- Create a new RawService to get plain data from the server.
Other changes:
- Performance: share Realm instance used on UI thread and improve SharedPreferences reading time.
Changes in Element 1.0.6 (2020-09-08) Changes in Element 1.0.6 (2020-09-08)
=================================================== ===================================================

View File

@ -24,6 +24,7 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.common.DaggerTestMatrixComponent
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.network.UserAgentHolder
@ -41,6 +42,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var legacySessionImporter: LegacySessionImporter
@Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var rawService: RawService
@Inject internal lateinit var userAgentHolder: UserAgentHolder @Inject internal lateinit var userAgentHolder: UserAgentHolder
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var olmManager: OlmManager
@ -61,6 +63,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return authenticationService return authenticationService
} }
fun rawService() = rawService
fun legacySessionImporter(): LegacySessionImporter { fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter return legacySessionImporter
} }

View File

@ -25,8 +25,16 @@ import org.matrix.android.sdk.internal.di.MatrixComponent
import org.matrix.android.sdk.internal.di.MatrixModule import org.matrix.android.sdk.internal.di.MatrixModule
import org.matrix.android.sdk.internal.di.MatrixScope import org.matrix.android.sdk.internal.di.MatrixScope
import org.matrix.android.sdk.internal.di.NetworkModule import org.matrix.android.sdk.internal.di.NetworkModule
import org.matrix.android.sdk.internal.raw.RawModule
@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class]) @Component(modules = [
TestModule::class,
MatrixModule::class,
NetworkModule::class,
AuthModule::class,
RawModule::class,
TestNetworkModule::class
])
@MatrixScope @MatrixScope
internal interface TestMatrixComponent : MatrixComponent { internal interface TestMatrixComponent : MatrixComponent {

View File

@ -25,6 +25,7 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.network.UserAgentHolder
@ -42,6 +43,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var legacySessionImporter: LegacySessionImporter
@Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var rawService: RawService
@Inject internal lateinit var userAgentHolder: UserAgentHolder @Inject internal lateinit var userAgentHolder: UserAgentHolder
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var olmManager: OlmManager
@ -62,6 +64,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return authenticationService return authenticationService
} }
fun rawService() = rawService
fun legacySessionImporter(): LegacySessionImporter { fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter return legacySessionImporter
} }

View File

@ -42,9 +42,6 @@ import org.matrix.android.sdk.api.util.JsonDict
* } * }
* ] * ]
* } * }
* "im.vector.riot.jitsi": {
* "preferredDomain": "https://jitsi.riot.im/"
* }
* } * }
* </pre> * </pre>
*/ */
@ -57,24 +54,5 @@ data class WellKnown(
val identityServer: WellKnownBaseConfig? = null, val identityServer: WellKnownBaseConfig? = null,
@Json(name = "m.integrations") @Json(name = "m.integrations")
val integrations: JsonDict? = null, val integrations: JsonDict? = null
@Json(name = "im.vector.riot.e2ee")
val e2eAdminSetting: E2EWellKnownConfig? = null,
@Json(name = "im.vector.riot.jitsi")
val jitsiServer: WellKnownPreferredConfig? = null
)
@JsonClass(generateAdapter = true)
data class E2EWellKnownConfig(
@Json(name = "default")
val e2eDefault: Boolean = true
)
@JsonClass(generateAdapter = true)
data class WellKnownPreferredConfig(
@Json(name = "preferredDomain")
val preferredDomain: String? = null
) )

View File

@ -132,6 +132,8 @@ data class MatrixError(
const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
/** (Not documented yet) */ /** (Not documented yet) */
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
/** (Not documented yet) */
const val M_WEAK_PASSWORD = "M_WEAK_PASSWORD"
const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED" const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED"

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.raw
sealed class RawCacheStrategy {
// Data is always fetched from the server
object NoCache: RawCacheStrategy()
// Once data is retrieved, it is stored for the provided amount of time.
// In case of error, and if strict is set to false, the cache can be returned if available
data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean): RawCacheStrategy()
// Once retrieved, the data is stored in cache and will be always get from the cache
object InfiniteCache: RawCacheStrategy()
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.raw
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
/**
* Useful methods to fetch raw data from the server. The access token will not be used to fetched the data
*/
interface RawService {
/**
* Get a URL, either from cache or from the remote server, depending on the cache strategy
*/
fun getUrl(url: String,
rawCacheStrategy: RawCacheStrategy,
matrixCallback: MatrixCallback<String>): Cancelable
/**
* Specific case for the well-known file. Cache validity is 8 hours
*/
fun getWellknown(userId: String, matrixCallback: MatrixCallback<String>): Cancelable
/**
* Clear all the cache data
*/
fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable
}

View File

@ -110,7 +110,7 @@ interface Session :
* This does not work in doze mode :/ * This does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/ * If battery optimization is on it can work in app standby but that's all :/
*/ */
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long)
fun stopAnyBackgroundSync() fun stopAnyBackgroundSync()

View File

@ -33,16 +33,7 @@ data class HomeServerCapabilities(
/** /**
* Default identity server url, provided in Wellknown * Default identity server url, provided in Wellknown
*/ */
val defaultIdentityServerUrl: String? = null, val defaultIdentityServerUrl: String? = null
/**
* Option to allow homeserver admins to set the default E2EE behaviour back to disabled for DMs / private rooms
* (as it was before) for various environments where this is desired.
*/
val adminE2EByDefault: Boolean = true,
/**
* Preferred Jitsi domain, provided in Wellknown
*/
val preferredJitsiDomain: String? = null
) { ) {
companion object { companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.summary
import org.matrix.android.sdk.api.session.events.model.EventType
object RoomSummaryConstants {
val PREVIEWABLE_TYPES = listOf(
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
EventType.MESSAGE,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.STICKER,
EventType.REACTION
)
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.timeline
data class TimelineEventFilters(
/**
* A flag to filter edit events
*/
val filterEdits: Boolean = false,
/**
* A flag to filter redacted events
*/
val filterRedacted: Boolean = false,
/**
* A flag to filter useless events, such as membership events without any change
*/
val filterUseless: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/
val filterTypes: Boolean = false,
/**
* If [filterTypes] is true, the list of types allowed by the list.
*/
val allowedTypes: List<String> = emptyList()
)

View File

@ -26,25 +26,9 @@ data class TimelineSettings(
*/ */
val initialSize: Int, val initialSize: Int,
/** /**
* A flag to filter edit events * Filters for timeline event
*/ */
val filterEdits: Boolean = false, val filters: TimelineEventFilters = TimelineEventFilters(),
/**
* A flag to filter redacted events
*/
val filterRedacted: Boolean = false,
/**
* A flag to filter useless events, such as membership events without any change
*/
val filterUseless: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/
val filterTypes: Boolean = false,
/**
* If [filterTypes] is true, the list of types allowed by the list.
*/
val allowedTypes: List<String> = emptyList(),
/** /**
* If true, will build read receipts for each event. * If true, will build read receipts for each event.
*/ */

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database
import io.realm.Realm
import java.io.Closeable
internal class RealmInstanceWrapper(private val realm: Realm, private val closeRealmOnClose: Boolean) : Closeable {
override fun close() {
if (closeRealmOnClose) {
realm.close()
}
}
fun <R> withRealm(block: (Realm) -> R): R {
return use {
block(it.realm)
}
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database
import android.os.Looper
import androidx.annotation.MainThread
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
import kotlin.concurrent.getOrSet
/**
* This class keeps an instance of realm open in the main thread so you can grab it whenever you want to get a realm
* instance. This does check each time if you are on the main thread or not and returns the appropriate realm instance.
*/
@SessionScope
internal class RealmSessionProvider @Inject constructor(@SessionDatabase private val monarchy: Monarchy)
: SessionLifecycleObserver {
private val realmThreadLocal = ThreadLocal<Realm>()
/**
* Allow you to execute a block with an opened realm. It automatically closes it if necessary (ie. when not in main thread)
*/
fun <R> withRealm(block: (Realm) -> R): R {
return getRealmWrapper().withRealm(block)
}
@MainThread
override fun onStart() {
realmThreadLocal.getOrSet {
Realm.getInstance(monarchy.realmConfiguration)
}
}
@MainThread
override fun onStop() {
realmThreadLocal.get()?.close()
realmThreadLocal.remove()
}
private fun getRealmWrapper(): RealmInstanceWrapper {
val isOnMainThread = isOnMainThread()
val realm = if (isOnMainThread) {
realmThreadLocal.getOrSet {
Realm.getInstance(monarchy.realmConfiguration)
}
} else {
Realm.getInstance(monarchy.realmConfiguration)
}
return RealmInstanceWrapper(realm, closeRealmOnClose = !isOnMainThread)
}
private fun isOnMainThread() = Looper.myLooper() == Looper.getMainLooper()
}

View File

@ -28,7 +28,7 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration { class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object { companion object {
const val SESSION_STORE_SCHEMA_VERSION = 4L const val SESSION_STORE_SCHEMA_VERSION = 5L
} }
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -38,6 +38,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -54,16 +55,16 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
private fun migrateTo2(realm: DynamicRealm) { private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2") Timber.d("Step 1 -> 2")
realm.schema.get("HomeServerCapabilitiesEntity") realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, Boolean::class.java) ?.addField("adminE2EByDefault", Boolean::class.java)
?.transform { obj -> ?.transform { obj ->
obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true) obj.setBoolean("adminE2EByDefault", true)
} }
} }
private fun migrateTo3(realm: DynamicRealm) { private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Step 2 -> 3") Timber.d("Step 2 -> 3")
realm.schema.get("HomeServerCapabilitiesEntity") realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.PREFERRED_JITSI_DOMAIN, String::class.java) ?.addField("preferredJitsiDomain", String::class.java)
?.transform { obj -> ?.transform { obj ->
// Schedule a refresh of the capabilities // Schedule a refresh of the capabilities
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0) obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
@ -82,4 +83,11 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
.setRequired(PendingThreePidEntityFields.SID, true) .setRequired(PendingThreePidEntityFields.SID, true)
.addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java) .addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
} }
private fun migrateTo5(realm: DynamicRealm) {
Timber.d("Step 4 -> 5")
realm.schema.get("HomeServerCapabilitiesEntity")
?.removeField("adminE2EByDefault")
?.removeField("preferredJitsiDomain")
}
} }

View File

@ -30,9 +30,7 @@ internal object HomeServerCapabilitiesMapper {
canChangePassword = entity.canChangePassword, canChangePassword = entity.canChangePassword,
maxUploadFileSize = entity.maxUploadFileSize, maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl, defaultIdentityServerUrl = entity.defaultIdentityServerUrl
adminE2EByDefault = entity.adminE2EByDefault,
preferredJitsiDomain = entity.preferredJitsiDomain
) )
} }
} }

View File

@ -18,26 +18,24 @@
package org.matrix.android.sdk.internal.database.mapper package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.model.UserEntity import org.matrix.android.sdk.internal.database.model.UserEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject import javax.inject.Inject
internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { internal class ReadReceiptsSummaryMapper @Inject constructor(private val realmSessionProvider: RealmSessionProvider) {
fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> { fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> {
if (readReceiptsSummaryEntity == null) { if (readReceiptsSummaryEntity == null) {
return emptyList() return emptyList()
} }
return Realm.getInstance(realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
val readReceipts = readReceiptsSummaryEntity.readReceipts val readReceipts = readReceiptsSummaryEntity.readReceipts
readReceipts readReceipts
.mapNotNull { .mapNotNull {
val user = UserEntity.where(realm, it.userId).findFirst() val user = UserEntity.where(realm, it.userId).findFirst()
?: return@mapNotNull null ?: return@mapNotNull null
ReadReceipt(user.asDomain(), it.originServerTs.toLong()) ReadReceipt(user.asDomain(), it.originServerTs.toLong())
} }
} }

View File

@ -17,17 +17,15 @@
package org.matrix.android.sdk.internal.database.model package org.matrix.android.sdk.internal.database.model
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import io.realm.RealmObject import io.realm.RealmObject
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
internal open class HomeServerCapabilitiesEntity( internal open class HomeServerCapabilitiesEntity(
var canChangePassword: Boolean = true, var canChangePassword: Boolean = true,
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastVersionIdentityServerSupported: Boolean = false, var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null, var defaultIdentityServerUrl: String? = null,
var adminE2EByDefault: Boolean = true, var lastUpdatedTimestamp: Long = 0L
var lastUpdatedTimestamp: Long = 0L,
var preferredJitsiDomain: String? = null
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class RawCacheEntity(
@PrimaryKey
var url: String = "",
var data: String = "",
var lastUpdatedTimestamp: Long = 0L
) : RealmObject() {
companion object
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.query
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.database.model.RawCacheEntityFields
/**
* Get the current RawCacheEntity, return null if it does not exist
*/
internal fun RawCacheEntity.Companion.get(realm: Realm, url: String): RawCacheEntity? {
return realm.where<RawCacheEntity>()
.equalTo(RawCacheEntityFields.URL, url)
.findFirst()
}
/**
* Get the current RawCacheEntity, create one if it does not exist
*/
internal fun RawCacheEntity.Companion.getOrCreate(realm: Realm, url: String): RawCacheEntity {
return get(realm, url) ?: realm.createObject(url)
}

View File

@ -17,17 +17,18 @@
package org.matrix.android.sdk.internal.database.query package org.matrix.android.sdk.internal.database.query
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> { internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> {
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
@ -56,16 +57,10 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm:
internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
roomId: String, roomId: String,
includesSending: Boolean, includesSending: Boolean,
filterContentRelation: Boolean = false, filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
filterTypes: List<String> = emptyList()): TimelineEventEntity? {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes) val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
if (filterContentRelation) {
liveEvents
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
}
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
sendingTimelineEvents sendingTimelineEvents
} else { } else {
@ -76,6 +71,24 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
?.findFirst() ?.findFirst()
} }
internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEventFilters): RealmQuery<TimelineEventEntity> {
if (filters.filterTypes) {
`in`(TimelineEventEntityFields.ROOT.TYPE, filters.allowedTypes.toTypedArray())
}
if (filters.filterUseless) {
not()
.equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
}
if (filters.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
}
if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
}
return this
}
internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<String>): RealmQuery<TimelineEventEntity> { internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<String>): RealmQuery<TimelineEventEntity> {
return if (filterTypes.isEmpty()) { return if (filterTypes.isEmpty()) {
this this

View File

@ -23,6 +23,10 @@ import javax.inject.Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class AuthDatabase internal annotation class AuthDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class GlobalDatabase
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class SessionDatabase internal annotation class SessionDatabase

View File

@ -22,22 +22,30 @@ import android.content.res.Resources
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component import dagger.Component
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.auth.AuthModule import org.matrix.android.sdk.internal.auth.AuthModule
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.raw.RawModule
import org.matrix.android.sdk.internal.session.MockHttpInterceptor import org.matrix.android.sdk.internal.session.MockHttpInterceptor
import org.matrix.android.sdk.internal.session.TestInterceptor import org.matrix.android.sdk.internal.session.TestInterceptor
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import okhttp3.OkHttpClient
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.File import java.io.File
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class, NoOpTestModule::class]) @Component(modules = [
MatrixModule::class,
NetworkModule::class,
AuthModule::class,
RawModule::class,
NoOpTestModule::class
])
@MatrixScope @MatrixScope
internal interface MatrixComponent { internal interface MatrixComponent {
@ -53,6 +61,8 @@ internal interface MatrixComponent {
fun authenticationService(): AuthenticationService fun authenticationService(): AuthenticationService
fun rawService(): RawService
fun context(): Context fun context(): Context
fun matrixConfiguration(): MatrixConfiguration fun matrixConfiguration(): MatrixConfiguration

View File

@ -0,0 +1,41 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.di.GlobalDatabase
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal interface CleanRawCacheTask : Task<Unit, Unit>
internal class DefaultCleanRawCacheTask @Inject constructor(
@GlobalDatabase private val monarchy: Monarchy
) : CleanRawCacheTask {
override suspend fun execute(params: Unit) {
monarchy.awaitTransaction { realm ->
realm.where<RawCacheEntity>()
.findAll()
.deleteAllFromRealm()
}
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy
import okhttp3.ResponseBody
import org.matrix.android.sdk.api.raw.RawCacheStrategy
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.GlobalDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import java.util.Date
import javax.inject.Inject
internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
data class Params(
val url: String,
val rawCacheStrategy: RawCacheStrategy
)
}
internal class DefaultGetUrlTask @Inject constructor(
private val rawAPI: RawAPI,
@GlobalDatabase private val monarchy: Monarchy
) : GetUrlTask {
override suspend fun execute(params: GetUrlTask.Params): String {
return when (params.rawCacheStrategy) {
RawCacheStrategy.NoCache -> doRequest(params.url)
is RawCacheStrategy.TtlCache -> doRequestWithCache(
params.url,
params.rawCacheStrategy.validityDurationInMillis,
params.rawCacheStrategy.strict
)
RawCacheStrategy.InfiniteCache -> doRequestWithCache(
params.url,
Long.MAX_VALUE,
true
)
}
}
private suspend fun doRequest(url: String): String {
return executeRequest<ResponseBody>(null) {
apiCall = rawAPI.getUrl(url)
}
.string()
}
private suspend fun doRequestWithCache(url: String, validityDurationInMillis: Long, strict: Boolean): String {
// Get data from cache
var dataFromCache: String? = null
var isCacheValid = false
monarchy.doWithRealm { realm ->
val entity = RawCacheEntity.get(realm, url)
dataFromCache = entity?.data
isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis
}
if (dataFromCache != null && isCacheValid) {
return dataFromCache as String
}
// No cache or outdated cache
val data = try {
doRequest(url)
} catch (throwable: Throwable) {
// In case of error, we can return value from cache even if outdated
return dataFromCache
?.takeIf { !strict }
?: throw throwable
}
// Store cache
monarchy.awaitTransaction { realm ->
val rawCacheEntity = RawCacheEntity.getOrCreate(realm, url)
rawCacheEntity.data = data
rawCacheEntity.lastUpdatedTimestamp = Date().time
}
return data
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.raw
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.raw.RawCacheStrategy
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import java.util.concurrent.TimeUnit
import javax.inject.Inject
internal class DefaultRawService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val getUrlTask: GetUrlTask,
private val cleanRawCacheTask: CleanRawCacheTask
) : RawService {
override fun getUrl(url: String,
rawCacheStrategy: RawCacheStrategy,
matrixCallback: MatrixCallback<String>): Cancelable {
return getUrlTask
.configureWith(GetUrlTask.Params(url, rawCacheStrategy)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun getWellknown(userId: String,
matrixCallback: MatrixCallback<String>): Cancelable {
val homeServerDomain = userId.substringAfter(":")
return getUrl(
"https://$homeServerDomain/.well-known/matrix/client",
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false),
matrixCallback
)
}
override fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable {
return cleanRawCacheTask
.configureWith(Unit) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.raw
import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
/**
* Realm module for global classes
*/
@RealmModule(library = true,
classes = [
RawCacheEntity::class
])
internal class GlobalRealmModule

View File

@ -0,0 +1,29 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.internal.raw
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Url
internal interface RawAPI {
@GET
fun getUrl(@Url url: String): Call<ResponseBody>
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import io.realm.RealmConfiguration
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.database.RealmKeysUtils
import org.matrix.android.sdk.internal.di.GlobalDatabase
import org.matrix.android.sdk.internal.di.MatrixScope
import org.matrix.android.sdk.internal.di.Unauthenticated
import org.matrix.android.sdk.internal.network.RetrofitFactory
@Module
internal abstract class RawModule {
@Module
companion object {
private const val DB_ALIAS = "matrix-sdk-global"
@JvmStatic
@Provides
@GlobalDatabase
fun providesMonarchy(@GlobalDatabase realmConfiguration: RealmConfiguration): Monarchy {
return Monarchy.Builder()
.setRealmConfiguration(realmConfiguration)
.build()
}
@JvmStatic
@Provides
@GlobalDatabase
@MatrixScope
fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder()
.apply {
realmKeysUtils.configureEncryption(this, DB_ALIAS)
}
.name("matrix-sdk-global.realm")
.modules(GlobalRealmModule())
.build()
}
@Provides
@JvmStatic
fun providesRawAPI(@Unauthenticated okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory): RawAPI {
return retrofitFactory.create(okHttpClient, "https://example.org").create(RawAPI::class.java)
}
}
@Binds
abstract fun bindRawService(service: DefaultRawService): RawService
@Binds
abstract fun bindGetUrlTask(task: DefaultGetUrlTask): GetUrlTask
@Binds
abstract fun bindCleanRawCacheTask(task: DefaultCleanRawCacheTask): CleanRawCacheTask
}

View File

@ -144,11 +144,13 @@ internal class DefaultFileService @Inject constructor(
if (elementToDecrypt != null) { if (elementToDecrypt != null) {
Timber.v("## FileService: decrypt file") Timber.v("## FileService: decrypt file")
val decryptSuccess = MXEncryptedAttachments.decryptAttachment( val decryptSuccess = destFile.outputStream().buffered().use {
source.inputStream(), MXEncryptedAttachments.decryptAttachment(
elementToDecrypt, source.inputStream(),
destFile.outputStream().buffered() elementToDecrypt,
) it
)
}
response.close() response.close()
if (!decryptSuccess) { if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error")) return@flatMap Try.Failure(IllegalStateException("Decryption error"))

View File

@ -166,8 +166,8 @@ internal class DefaultSession @Inject constructor(
SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) SyncWorker.requireBackgroundSync(workManagerProvider, sessionId)
} }
override fun startAutomaticBackgroundSync(repeatDelay: Long) { override fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) {
SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, 0, repeatDelay) SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, timeOutInSeconds, repeatDelayInSeconds)
} }
override fun stopAnyBackgroundSync() { override fun stopAnyBackgroundSync() {

View File

@ -47,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorage
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
import org.matrix.android.sdk.internal.database.DatabaseCleaner import org.matrix.android.sdk.internal.database.DatabaseCleaner
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.DeviceId
@ -325,23 +326,27 @@ internal abstract class SessionModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindIntegrationManager(observer: IntegrationManager): SessionLifecycleObserver abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindWidgetUrlFormatter(observer: DefaultWidgetURLFormatter): SessionLifecycleObserver abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindShieldTrustUpdated(observer: ShieldTrustUpdater): SessionLifecycleObserver abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver
@Binds
@IntoSet
abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver
@Binds @Binds
abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService

View File

@ -18,6 +18,7 @@
package org.matrix.android.sdk.internal.session.homeserver package org.matrix.android.sdk.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
@ -32,7 +33,6 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@ -109,16 +109,12 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl
homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true
homeServerCapabilitiesEntity.preferredJitsiDomain = getWellknownResult.wellKnown.jitsiServer?.preferredDomain
// We are also checking for integration manager configurations // We are also checking for integration manager configurations
val config = configExtractor.extract(getWellknownResult.wellKnown) val config = configExtractor.extract(getWellknownResult.wellKnown)
if (config != null) { if (config != null) {
Timber.v("Extracted integration config : $config") Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config) realm.insertOrUpdate(config)
} }
} else {
homeServerCapabilitiesEntity.adminE2EByDefault = true
} }
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
} }

View File

@ -17,17 +17,16 @@
package org.matrix.android.sdk.internal.session.room package org.matrix.android.sdk.internal.session.room
import com.zhuinden.monarchy.Monarchy import io.realm.Realm
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import io.realm.Realm
import javax.inject.Inject import javax.inject.Inject
internal interface RoomGetter { internal interface RoomGetter {
@ -38,18 +37,18 @@ internal interface RoomGetter {
@SessionScope @SessionScope
internal class DefaultRoomGetter @Inject constructor( internal class DefaultRoomGetter @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, private val realmSessionProvider: RealmSessionProvider,
private val roomFactory: RoomFactory private val roomFactory: RoomFactory
) : RoomGetter { ) : RoomGetter {
override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
createRoom(realm, roomId) createRoom(realm, roomId)
} }
} }
override fun getDirectRoomWith(otherUserId: String): Room? { override fun getDirectRoomWith(otherUserId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
RoomSummaryEntity.where(realm) RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)

View File

@ -18,6 +18,7 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.helper.nextId import org.matrix.android.sdk.internal.database.helper.nextId
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
@ -43,12 +45,11 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus, private val eventBus: EventBus,
private val timelineEventMapper: TimelineEventMapper) { private val timelineEventMapper: TimelineEventMapper) {
@ -59,7 +60,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
if (event.eventId == null) { if (event.eventId == null) {
throw IllegalStateException("You should have set an eventId for your event") throw IllegalStateException("You should have set an eventId for your event")
} }
val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val timelineEventEntity = realmSessionProvider.withRealm { realm ->
val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis())
val roomMemberHelper = RoomMemberHelper(realm, roomId) val roomMemberHelper = RoomMemberHelper(realm, roomId)
val myUser = roomMemberHelper.getLastRoomMember(senderId) val myUser = roomMemberHelper.getLastRoomMember(senderId)
@ -150,7 +151,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
fun getAllEventsWithStates(roomId: String, states : List<SendState>): List<TimelineEvent> { fun getAllEventsWithStates(roomId: String, states : List<SendState>): List<TimelineEvent> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
TimelineEventEntity TimelineEventEntity
.findAllInRoomWithSendStates(realm, roomId, states) .findAllInRoomWithSendStates(realm, roomId, states)
.sortedByDescending { it.displayIndex } .sortedByDescending { it.displayIndex }

View File

@ -20,24 +20,26 @@ package org.matrix.android.sdk.internal.session.room.state
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.query.process
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import javax.inject.Inject import javax.inject.Inject
internal class StateEventDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { internal class StateEventDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider) {
fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? { fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain() buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain()
} }
} }
@ -53,7 +55,7 @@ internal class StateEventDataSource @Inject constructor(@SessionDatabase private
} }
fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> { fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
buildStateEventQuery(realm, roomId, eventTypes, stateKey) buildStateEventQuery(realm, roomId, eventTypes, stateKey)
.findAll() .findAll()
.mapNotNull { .mapNotNull {

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.summary
import io.realm.Realm
import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants
import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.latestEvent
internal object RoomSummaryEventsHelper {
private val previewFilters = TimelineEventFilters(
filterTypes = true,
allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES,
filterUseless = true,
filterRedacted = false,
filterEdits = true
)
fun getLatestPreviewableEvent(realm: Realm, roomId: String): TimelineEventEntity? {
return TimelineEventEntity.latestEvent(
realm = realm,
roomId = roomId,
includesSending = true,
filters = previewFilters
)
}
}

View File

@ -18,6 +18,8 @@
package org.matrix.android.sdk.internal.session.room.summary package org.matrix.android.sdk.internal.session.room.summary
import dagger.Lazy import dagger.Lazy
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -40,7 +42,6 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.isEventRead
import org.matrix.android.sdk.internal.database.query.latestEvent
import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
@ -49,8 +50,6 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -61,28 +60,6 @@ internal class RoomSummaryUpdater @Inject constructor(
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>, private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
private val eventBus: EventBus) { private val eventBus: EventBus) {
companion object {
// TODO: maybe allow user of SDK to give that list
val PREVIEWABLE_TYPES = listOf(
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
EventType.MESSAGE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_AVATAR,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.STATE_ROOM_ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.REACTION,
EventType.STATE_ROOM_CREATE
)
}
fun update(realm: Realm, fun update(realm: Realm,
roomId: String, roomId: String,
membership: Membership? = null, membership: Membership? = null,
@ -110,9 +87,6 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.membership = membership roomSummaryEntity.membership = membership
} }
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true,
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
@ -123,6 +97,8 @@ internal class RoomSummaryUpdater @Inject constructor(
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst() .findFirst()
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
|| !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId)
@ -178,8 +154,7 @@ internal class RoomSummaryUpdater @Inject constructor(
fun updateSendingInformation(realm: Realm, roomId: String) { fun updateSendingInformation(realm: Realm, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.updateHasFailedSending()
roomSummaryEntity.latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
} }
fun updateShieldTrust(realm: Realm, fun updateShieldTrust(realm: Realm,

View File

@ -39,13 +39,14 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.TimelineEventFilter import org.matrix.android.sdk.internal.database.query.filterEvents
import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.database.query.whereRoomId
@ -76,7 +77,8 @@ internal class DefaultTimeline(
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val eventBus: EventBus, private val eventBus: EventBus,
private val eventDecryptor: TimelineEventDecryptor private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider
) : Timeline, TimelineHiddenReadReceipts.Delegate { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>) data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
@ -136,13 +138,13 @@ internal class DefaultTimeline(
} }
override fun pendingEventCount(): Int { override fun pendingEventCount(): Int {
return Realm.getInstance(realmConfiguration).use { return realmSessionProvider.withRealm {
RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0
} }
} }
override fun failedToDeliverEventCount(): Int { override fun failedToDeliverEventCount(): Int {
return Realm.getInstance(realmConfiguration).use { return realmSessionProvider.withRealm {
TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count()
} }
} }
@ -182,7 +184,7 @@ internal class DefaultTimeline(
} }
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return buildReadReceipts && (filterEdits || filterTypes) return buildReadReceipts && (filters.filterEdits || filters.filterTypes)
} }
override fun dispose() { override fun dispose() {
@ -239,7 +241,7 @@ internal class DefaultTimeline(
return eventId return eventId
} }
// Otherwise, we should check if the event is in the db, but is hidden because of filters // Otherwise, we should check if the event is in the db, but is hidden because of filters
return Realm.getInstance(realmConfiguration).use { localRealm -> return realmSessionProvider.withRealm { localRealm ->
val nonFilteredEvents = buildEventQuery(localRealm) val nonFilteredEvents = buildEventQuery(localRealm)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll() .findAll()
@ -757,29 +759,15 @@ internal class DefaultTimeline(
} }
private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> { private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> {
if (settings.filterTypes) { return filterEvents(settings.filters)
`in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray())
}
if (settings.filterUseless) {
not()
.equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
}
if (settings.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
}
if (settings.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
}
return this
} }
private fun List<TimelineEvent>.filterEventsWithSettings(): List<TimelineEvent> { private fun List<TimelineEvent>.filterEventsWithSettings(): List<TimelineEvent> {
return filter { return filter {
val filterType = !settings.filterTypes || settings.allowedTypes.contains(it.root.type) val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
if (!filterType) return@filter false if (!filterType) return@filter false
val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) {
val messageContent = it.root.content.toModel<MessageContent>() val messageContent = it.root.content.toModel<MessageContent>()
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
} else { } else {
@ -787,7 +775,7 @@ internal class DefaultTimeline(
} }
if (!filterEdits) return@filter false if (!filterEdits) return@filter false
val filterRedacted = !settings.filterRedacted || it.root.isRedacted() val filterRedacted = !settings.filters.filterRedacted || it.root.isRedacted()
filterRedacted filterRedacted
} }

View File

@ -22,6 +22,9 @@ import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Sort
import io.realm.kotlin.where
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -30,7 +33,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.store.db.doWithRealm import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
@ -38,13 +41,10 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.fetchCopyMap
import io.realm.Sort
import io.realm.kotlin.where
import org.greenrobot.eventbus.EventBus
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val eventBus: EventBus, private val eventBus: EventBus,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
@ -73,17 +73,17 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
eventBus = eventBus, eventBus = eventBus,
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider
) )
} }
override fun getTimeLineEvent(eventId: String): TimelineEvent? { override fun getTimeLineEvent(eventId: String): TimelineEvent? {
return monarchy return realmSessionProvider.withRealm { realm ->
.fetchCopyMap({ TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() timelineEventMapper.map(it)
}, { entity, _ -> }
timelineEventMapper.map(entity) }
})
} }
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> { override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
@ -98,7 +98,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
override fun getAttachmentMessages(): List<TimelineEvent> { override fun getAttachmentMessages(): List<TimelineEvent> {
// TODO pretty bad query.. maybe we should denormalize clear type in base? // TODO pretty bad query.. maybe we should denormalize clear type in base?
return doWithRealm(monarchy.realmConfiguration) { realm -> return realmSessionProvider.withRealm { realm ->
realm.where<TimelineEventEntity>() realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)

View File

@ -18,6 +18,10 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import android.util.SparseArray import android.util.SparseArray
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
@ -27,10 +31,6 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.TimelineEventFilter import org.matrix.android.sdk.internal.database.query.TimelineEventFilter
import org.matrix.android.sdk.internal.database.query.whereInRoom import org.matrix.android.sdk.internal.database.query.whereInRoom
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
/** /**
* This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering).
@ -151,23 +151,24 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> { private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
beginGroup() beginGroup()
var needOr = false var needOr = false
if (settings.filterTypes) { if (settings.filters.filterTypes) {
not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) val allowedTypes = settings.filters.allowedTypes.toTypedArray()
not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", allowedTypes)
needOr = true needOr = true
} }
if (settings.filterUseless) { if (settings.filters.filterUseless) {
if (needOr) or() if (needOr) or()
equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.IS_USELESS}", true) equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.IS_USELESS}", true)
needOr = true needOr = true
} }
if (settings.filterEdits) { if (settings.filters.filterEdits) {
if (needOr) or() if (needOr) or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT)
or() or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE) like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE)
needOr = true needOr = true
} }
if (settings.filterRedacted) { if (settings.filters.filterRedacted) {
if (needOr) or() if (needOr) or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED)
} }

View File

@ -18,6 +18,7 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
@ -32,19 +33,16 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.latestEvent
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -177,12 +175,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
currentChunk.isLastForward = true currentChunk.isLastForward = true
currentLastForwardChunk?.deleteOnCascade() currentLastForwardChunk?.deleteOnCascade()
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = TimelineEventEntity.latestEvent( latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
realm,
roomId,
includesSending = true,
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
)
} }
} }
} else { } else {
@ -249,13 +242,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null
|| (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
if (shouldUpdateSummary) { if (shouldUpdateSummary) {
val latestPreviewableEvent = TimelineEventEntity.latestEvent( roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
realm,
roomId,
includesSending = true,
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
)
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
} }
if (currentChunk.isValid) { if (currentChunk.isValid) {
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)

View File

@ -32,7 +32,7 @@ import javax.inject.Inject
internal interface SyncTask : Task<SyncTask.Params, Unit> { internal interface SyncTask : Task<SyncTask.Params, Unit> {
data class Params(var timeout: Long = 30_000L) data class Params(var timeout: Long = 6_000L)
} }
internal class DefaultSyncTask @Inject constructor( internal class DefaultSyncTask @Inject constructor(

View File

@ -19,7 +19,14 @@ package org.matrix.android.sdk.internal.session.sync.job
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.getSystemService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.SyncState
@ -28,10 +35,6 @@ import org.matrix.android.sdk.internal.session.sync.SyncTask
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -46,6 +49,11 @@ abstract class SyncService : Service() {
private var sessionId: String? = null private var sessionId: String? = null
private var mIsSelfDestroyed: Boolean = false private var mIsSelfDestroyed: Boolean = false
private var syncTimeoutSeconds: Int = 6
private var syncDelaySeconds: Int = 60
private var periodic: Boolean = false
private var preventReschedule: Boolean = false
private var isInitialSync: Boolean = false private var isInitialSync: Boolean = false
private lateinit var session: Session private lateinit var session: Session
private lateinit var syncTask: SyncTask private lateinit var syncTask: SyncTask
@ -59,27 +67,60 @@ abstract class SyncService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob()) private val serviceScope = CoroutineScope(SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand $intent") Timber.i("## Sync: onStartCommand [$this] $intent with action: ${intent?.action}")
val isInit = initialize(intent)
if (isInit) { // We should start we have to ensure we fulfill contract to show notification
onStart(isInitialSync) // for foreground service (as per design for this service)
doSyncIfNotAlreadyRunning() // TODO can we check if it's really in foreground
} else { onStart(isInitialSync)
// We should start and stop as we have to ensure to call Service.startForeground() when (intent?.action) {
onStart(isInitialSync) ACTION_STOP -> {
stopMe() Timber.i("## Sync: stop command received")
// If it was periodic we ensure that it will not reschedule itself
preventReschedule = true
// we don't want to cancel initial syncs, let it finish
if (!isInitialSync) {
stopMe()
}
}
else -> {
val isInit = initialize(intent)
if (isInit) {
periodic = intent?.getBooleanExtra(EXTRA_PERIODIC, false) ?: false
val onNetworkBack = intent?.getBooleanExtra(EXTRA_NETWORK_BACK_RESTART, false) ?: false
Timber.d("## Sync: command received, periodic: $periodic networkBack: $onNetworkBack")
if (onNetworkBack && !backgroundDetectionObserver.isInBackground) {
// the restart after network occurs while the app is in foreground
// so just stop. It will be restarted when entering background
preventReschedule = true
stopMe()
} else {
// default is syncing
doSyncIfNotAlreadyRunning()
}
} else {
Timber.d("## Sync: Failed to initialize service")
stopMe()
}
}
} }
// No intent just start the service, an alarm will should call with intent
return START_STICKY // It's ok to be not sticky because we will explicitly start it again on the next alarm?
return START_NOT_STICKY
} }
override fun onDestroy() { override fun onDestroy() {
Timber.i("## onDestroy() : $this") Timber.i("## Sync: onDestroy() [$this] periodic:$periodic preventReschedule:$preventReschedule")
if (!mIsSelfDestroyed) { if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this") Timber.d("## Sync: Destroy by the system : $this")
} }
serviceScope.coroutineContext.cancelChildren()
isRunning.set(false) isRunning.set(false)
// Cancelling the context will trigger the catch close the doSync try
serviceScope.coroutineContext.cancelChildren()
if (!preventReschedule && periodic && sessionId != null && backgroundDetectionObserver.isInBackground) {
Timber.d("## Sync: Reschedule service in $syncDelaySeconds sec")
onRescheduleAsked(sessionId ?: "", false, syncTimeoutSeconds, syncDelaySeconds)
}
super.onDestroy() super.onDestroy()
} }
@ -90,9 +131,15 @@ abstract class SyncService : Service() {
private fun doSyncIfNotAlreadyRunning() { private fun doSyncIfNotAlreadyRunning() {
if (isRunning.get()) { if (isRunning.get()) {
Timber.i("Received a start while was already syncing... ignore") Timber.i("## Sync: Received a start while was already syncing... ignore")
} else { } else {
isRunning.set(true) isRunning.set(true)
// Acquire a lock to give enough time for the sync :/
getSystemService<PowerManager>()?.run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply {
acquire((syncTimeoutSeconds * 1000L + 10_000L))
}
}
serviceScope.launch(coroutineDispatchers.io) { serviceScope.launch(coroutineDispatchers.io) {
doSync() doSync()
} }
@ -100,9 +147,10 @@ abstract class SyncService : Service() {
} }
private suspend fun doSync() { private suspend fun doSync() {
Timber.v("Execute sync request with timeout 0") Timber.v("## Sync: Execute sync request with timeout $syncTimeoutSeconds seconds")
val params = SyncTask.Params(TIME_OUT) val params = SyncTask.Params(syncTimeoutSeconds * 1000L)
try { try {
// never do that in foreground, let the syncThread work
syncTask.execute(params) syncTask.execute(params)
// Start sync if we were doing an initial sync and the syncThread is not launched yet // Start sync if we were doing an initial sync and the syncThread is not launched yet
if (isInitialSync && session.getSyncState() == SyncState.Idle) { if (isInitialSync && session.getSyncState() == SyncState.Idle) {
@ -111,28 +159,34 @@ abstract class SyncService : Service() {
} }
stopMe() stopMe()
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable) Timber.e(throwable, "## Sync: sync service did fail ${isRunning.get()}")
if (throwable.isTokenError()) { if (throwable.isTokenError()) {
stopMe() // no need to retry
} else { preventReschedule = true
Timber.v("Should be rescheduled to avoid wasting resources")
sessionId?.also {
onRescheduleAsked(it, isInitialSync, delay = 10_000L)
}
stopMe()
} }
if (throwable is Failure.NetworkConnection) {
// Network is off, no need to reschedule endless alarms :/
preventReschedule = true
// Instead start a work to restart background sync when network is back
onNetworkError(sessionId ?: "", isInitialSync, syncTimeoutSeconds, syncDelaySeconds)
}
// JobCancellation could be caught here when onDestroy cancels the coroutine context
if (isRunning.get()) stopMe()
} }
} }
private fun initialize(intent: Intent?): Boolean { private fun initialize(intent: Intent?): Boolean {
if (intent == null) { if (intent == null) {
Timber.d("## Sync: initialize intent is null")
return false return false
} }
val matrix = Matrix.getInstance(applicationContext) val matrix = Matrix.getInstance(applicationContext)
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, 6)
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, 60)
try { try {
val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId) val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId)
?: throw IllegalStateException("You should have a session to make it work") ?: throw IllegalStateException("## Sync: You should have a session to make it work")
session = sessionComponent.session() session = sessionComponent.session()
sessionId = safeSessionId sessionId = safeSessionId
syncTask = sessionComponent.syncTask() syncTask = sessionComponent.syncTask()
@ -143,14 +197,16 @@ abstract class SyncService : Service() {
backgroundDetectionObserver = matrix.backgroundDetectionObserver backgroundDetectionObserver = matrix.backgroundDetectionObserver
return true return true
} catch (exception: Exception) { } catch (exception: Exception) {
Timber.e(exception, "An exception occurred during initialisation") Timber.e(exception, "## Sync: An exception occurred during initialisation")
return false return false
} }
} }
abstract fun onStart(isInitialSync: Boolean) abstract fun onStart(isInitialSync: Boolean)
abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int)
abstract fun onNetworkError(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int)
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
@ -158,6 +214,11 @@ abstract class SyncService : Service() {
companion object { companion object {
const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID"
private const val TIME_OUT = 0L const val EXTRA_TIMEOUT_SECONDS = "EXTRA_TIMEOUT_SECONDS"
const val EXTRA_DELAY_SECONDS = "EXTRA_DELAY_SECONDS"
const val EXTRA_PERIODIC = "EXTRA_PERIODIC"
const val EXTRA_NETWORK_BACK_RESTART = "EXTRA_NETWORK_BACK_RESTART"
const val ACTION_STOP = "ACTION_STOP"
} }
} }

View File

@ -34,7 +34,8 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
private const val DEFAULT_LONG_POOL_TIMEOUT = 0L private const val DEFAULT_LONG_POOL_TIMEOUT = 6L
private const val DEFAULT_DELAY_TIMEOUT = 30_000L
/** /**
* Possible previous worker: None * Possible previous worker: None
@ -48,13 +49,15 @@ internal class SyncWorker(context: Context,
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT,
val automaticallyRetry: Boolean = false, val delay: Long = DEFAULT_DELAY_TIMEOUT,
val periodic: Boolean = false,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var syncTask: SyncTask @Inject lateinit var syncTask: SyncTask
@Inject lateinit var taskExecutor: TaskExecutor @Inject lateinit var taskExecutor: TaskExecutor
@Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker
@Inject lateinit var workManagerProvider: WorkManagerProvider
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.i("Sync work starting") Timber.i("Sync work starting")
@ -67,11 +70,21 @@ internal class SyncWorker(context: Context,
return runCatching { return runCatching {
doSync(params.timeout) doSync(params.timeout)
}.fold( }.fold(
{ Result.success() }, {
Result.success().also {
if (params.periodic) {
// we want to schedule another one after delay
automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay)
}
}
},
{ failure -> { failure ->
if (failure.isTokenError() || !params.automaticallyRetry) { if (failure.isTokenError()) {
Result.failure() Result.failure()
} else { } else {
// If the worker was stopped (when going back in foreground), a JobCancellation exception is sent
// but in this case the result is ignored, as the work is considered stopped,
// so don't worry of the retry here for this case
Result.retry() Result.retry()
} }
} }
@ -79,7 +92,7 @@ internal class SyncWorker(context: Context,
} }
private suspend fun doSync(timeout: Long) { private suspend fun doSync(timeout: Long) {
val taskParams = SyncTask.Params(timeout) val taskParams = SyncTask.Params(timeout * 1000)
syncTask.execute(taskParams) syncTask.execute(taskParams)
} }
@ -87,25 +100,27 @@ internal class SyncWorker(context: Context,
private const val BG_SYNC_WORK_NAME = "BG_SYNCP" private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) {
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, false)) val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setInputData(data) .setInputData(data)
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
} }
fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) { fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) {
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true)) val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setInputData(data) .setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS)
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
} }
fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) {

View File

@ -23,9 +23,11 @@ import androidx.paging.DataSource
import androidx.paging.LivePagedListBuilder import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Case
import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity
import org.matrix.android.sdk.internal.database.model.IgnoredUserEntityFields import org.matrix.android.sdk.internal.database.model.IgnoredUserEntityFields
@ -33,11 +35,10 @@ import org.matrix.android.sdk.internal.database.model.UserEntity
import org.matrix.android.sdk.internal.database.model.UserEntityFields import org.matrix.android.sdk.internal.database.model.UserEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.util.fetchCopied
import io.realm.Case
import javax.inject.Inject import javax.inject.Inject
internal class UserDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { internal class UserDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider) {
private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory<UserEntity> by lazy { private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory<UserEntity> by lazy {
monarchy.createDataSourceFactory { realm -> monarchy.createDataSourceFactory { realm ->
@ -58,10 +59,10 @@ internal class UserDataSource @Inject constructor(@SessionDatabase private val m
} }
fun getUser(userId: String): User? { fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } return realmSessionProvider.withRealm {
?: return null val userEntity = UserEntity.where(it, userId).findFirst()
userEntity?.asDomain()
return userEntity.asDomain() }
} }
fun getUserLive(userId: String): LiveData<Optional<User>> { fun getUserLive(userId: String): LiveData<Optional<User>> {

View File

@ -20,18 +20,20 @@ package org.matrix.android.sdk.internal.session.user.accountdata
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.AccountDataMapper import org.matrix.android.sdk.internal.database.mapper.AccountDataMapper
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val accountDataMapper: AccountDataMapper) { private val accountDataMapper: AccountDataMapper) {
fun getAccountDataEvent(type: String): UserAccountDataEvent? { fun getAccountDataEvent(type: String): UserAccountDataEvent? {
@ -45,10 +47,9 @@ internal class AccountDataDataSource @Inject constructor(@SessionDatabase privat
} }
fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> { fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> {
return monarchy.fetchAllMappedSync( return realmSessionProvider.withRealm {
{ accountDataEventsQuery(it, types) }, accountDataEventsQuery(it, types).findAll().map(accountDataMapper::map)
accountDataMapper::map }
)
} }
fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> { fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> {

View File

@ -23,17 +23,15 @@ import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.user.UserDataSource import org.matrix.android.sdk.internal.session.user.UserDataSource
import io.realm.Realm
import io.realm.RealmConfiguration
import java.net.URLEncoder import java.net.URLEncoder
import javax.inject.Inject import javax.inject.Inject
internal class WidgetFactory @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, internal class WidgetFactory @Inject constructor(private val userDataSource: UserDataSource,
private val userDataSource: UserDataSource, private val realmSessionProvider: RealmSessionProvider,
@UserId private val userId: String) { @UserId private val userId: String) {
fun create(widgetEvent: Event): Widget? { fun create(widgetEvent: Event): Widget? {
@ -44,7 +42,7 @@ internal class WidgetFactory @Inject constructor(@SessionDatabase private val re
val senderInfo = if (widgetEvent.senderId == null || widgetEvent.roomId == null) { val senderInfo = if (widgetEvent.senderId == null || widgetEvent.roomId == null) {
null null
} else { } else {
Realm.getInstance(realmConfiguration).use { realmSessionProvider.withRealm {
val roomMemberHelper = RoomMemberHelper(it, widgetEvent.roomId) val roomMemberHelper = RoomMemberHelper(it, widgetEvent.roomId)
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(widgetEvent.senderId) val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(widgetEvent.senderId)
SenderInfo( SenderInfo(

View File

@ -6,21 +6,21 @@
<string name="notice_room_invite_no_invitee">دعوة من %s</string> <string name="notice_room_invite_no_invitee">دعوة من %s</string>
<string name="notice_room_invite">دعى %1$s %2$s</string> <string name="notice_room_invite">دعى %1$s %2$s</string>
<string name="notice_room_invite_you">دعاك %1$s</string> <string name="notice_room_invite_you">دعاك %1$s</string>
<string name="notice_room_join">انضمّ %1$s</string> <string name="notice_room_join">انضمّ %1$s إلى الغرفة</string>
<string name="notice_room_leave">غادر %1$s</string> <string name="notice_room_leave">غادر %1$s الغرفة</string>
<string name="notice_room_reject">رفض %1$s الدعوة</string> <string name="notice_room_reject">رفض %1$s الدعوة</string>
<string name="notice_room_kick">طرد %1$s %2$s</string> <string name="notice_room_kick">طرد %1$s %2$s</string>
<string name="notice_room_unban">رفع %1$s الحظر عن %2$s</string> <string name="notice_room_unban">رفع %1$s المنع عن %2$s</string>
<string name="notice_room_ban">منع %1$s %2$s</string> <string name="notice_room_ban">منع %1$s %2$s</string>
<string name="notice_avatar_url_changed">غيّر %1$s صورته</string> <string name="notice_avatar_url_changed">غيّر %1$s صورته</string>
<string name="notice_display_name_set">ضبط %1$s اسم العرض على %2$s</string> <string name="notice_display_name_set">ضبط %1$s اسم العرض على %2$s</string>
<string name="notice_display_name_changed_from">غيّر %1$s اسم الحساب المعروض من %2$s إلى %3$s</string> <string name="notice_display_name_changed_from">غيّر %1$s اسم العرض من %2$s إلى %3$s</string>
<string name="notice_display_name_removed">أزال %1$s اسم الحساب المعروض (%2$s)</string> <string name="notice_display_name_removed">أزال %1$s اسم العرض (⁨كان %2$s)</string>
<string name="notice_room_topic_changed">غيّر %1$s الموضوع إلى: %2$s</string> <string name="notice_room_topic_changed">غيّر %1$s الموضوع إلى: %2$s</string>
<string name="notice_room_name_changed">غيّر %1$s اسم الغرفة إلى: %2$s</string> <string name="notice_room_name_changed">غيّر %1$s اسم الغرفة إلى: %2$s</string>
<string name="notice_answered_call">ردّ %s على المكالمة.</string> <string name="notice_answered_call">ردّ %s على المكالمة.</string>
<string name="notice_ended_call">أنهى %s المكالمة.</string> <string name="notice_ended_call">أنهى %s المكالمة.</string>
<string name="notice_made_future_room_visibility">جعل %1$s تأريخ الغرفة مستقبلًا ظاهرا على %2$s</string> <string name="notice_made_future_room_visibility">جعل %1$s تأريخ الغرفة مستقبلًا ظاهرًا على %2$s</string>
<string name="notice_room_visibility_invited">كل أعضاء الغرفة من لحظة دعوتهم.</string> <string name="notice_room_visibility_invited">كل أعضاء الغرفة من لحظة دعوتهم.</string>
<string name="notice_room_visibility_joined">كل أعضاء الغرفة من لحظة انضمامهم.</string> <string name="notice_room_visibility_joined">كل أعضاء الغرفة من لحظة انضمامهم.</string>
<string name="notice_room_visibility_shared">كل أعضاء الغرفة.</string> <string name="notice_room_visibility_shared">كل أعضاء الغرفة.</string>
@ -46,7 +46,7 @@
<string name="network_error">خطأ في الشبكة</string> <string name="network_error">خطأ في الشبكة</string>
<string name="matrix_error">خطأ في «ماترِكس»</string> <string name="matrix_error">خطأ في «ماترِكس»</string>
<string name="room_error_join_failed_empty_room">ليس ممكنا الانضمام ثانيةً إلى غرفة فارغة.</string> <string name="room_error_join_failed_empty_room">لا يمكنك حاليًا الانضمام ثانيةً إلى غرفة فارغة.</string>
<string name="encrypted_message">رسالة معمّاة</string> <string name="encrypted_message">رسالة معمّاة</string>
@ -54,13 +54,13 @@
<string name="medium_phone_number">رقم الهاتف</string> <string name="medium_phone_number">رقم الهاتف</string>
<string name="summary_message">%1$s: %2$s</string> <string name="summary_message">%1$s: %2$s</string>
<string name="notice_room_withdraw">انسحب %1$s من الدعوة %2$s</string> <string name="notice_room_withdraw">انسحب %1$s من دعوة %2$s</string>
<string name="notice_placed_video_call">أجرى %s مكالمة مرئية.</string> <string name="notice_placed_video_call">أجرى %s مكالمة مرئية.</string>
<string name="notice_placed_voice_call">أجرى %s مكالمة صوتية.</string> <string name="notice_placed_voice_call">أجرى %s مكالمة صوتية.</string>
<string name="notice_room_third_party_registered_invite">قبل %1$s دعوة %2$s</string> <string name="notice_room_third_party_registered_invite">قَبِل %1$s دعوة %2$s</string>
<string name="could_not_redact">تعذر التهذيب</string> <string name="could_not_redact">تعذر التهذيب</string>
<string name="summary_user_sent_sticker">أرسل %1$s ملصقا.</string> <string name="summary_user_sent_sticker">أرسل %1$s ملصقًا.</string>
<string name="notice_avatar_changed_too">(تغيّرت الصورة أيضا)</string> <string name="notice_avatar_changed_too">(تغيّرت الصورة أيضا)</string>
@ -71,12 +71,77 @@
<string name="room_displayname_room_invite">دعوة إلى غرفة</string> <string name="room_displayname_room_invite">دعوة إلى غرفة</string>
<plurals name="room_displayname_three_and_more_members"> <plurals name="room_displayname_three_and_more_members">
<item quantity="zero">صفر</item> <item quantity="zero"></item>
<item quantity="one">واحد</item> <item quantity="one"></item>
<item quantity="two">اثنان</item> <item quantity="two"></item>
<item quantity="few">قليل</item> <item quantity="few"></item>
<item quantity="many">كثير</item> <item quantity="many"></item>
<item quantity="other">اخرى</item> <item quantity="other"></item>
</plurals> </plurals>
<string name="summary_you_sent_image">أرسلت صورة.</string>
<string name="summary_you_sent_sticker">أرسلت ملصقًا.</string>
<string name="notice_room_invite_no_invitee_by_you">دعوة منك أنت</string>
<string name="notice_room_created">أنشأ %1$s الغرفة</string>
<string name="notice_room_created_by_you">أنشأت الغرفة</string>
<string name="notice_room_invite_by_you">دعوت %1$s</string>
<string name="notice_room_join_by_you">انضممت إلى الغرفة</string>
<string name="notice_room_leave_by_you">غادرت الغرفة</string>
<string name="notice_room_reject_by_you">رفضت الدعوة</string>
<string name="notice_room_kick_by_you">طردت %1$s</string>
<string name="notice_room_unban_by_you">رفعت المنع عن %1$s</string>
<string name="notice_room_ban_by_you">منعت %1$s</string>
<string name="notice_room_withdraw_by_you">انسحبت من دعوة %1$s</string>
<string name="notice_avatar_url_changed_by_you">غيّرت صورتك</string>
<string name="notice_display_name_set_by_you">ضبطت اسم العرض على %1$s</string>
<string name="notice_display_name_changed_from_by_you">غيّرت اسم العرض من %1$s إلى %2$s</string>
<string name="notice_display_name_removed_by_you">أزلت اسم العرض (كان %1$s)</string>
<string name="notice_room_topic_changed_by_you">غيّرت الموضوع إلى: %1$s</string>
<string name="notice_room_avatar_changed">غيّر %1$s صورة الغرفة</string>
<string name="notice_room_avatar_changed_by_you">غيّرت صورة الغرفة</string>
<string name="notice_room_name_changed_by_you">غيّرت اسم الغرفة إلى: %1$s</string>
<string name="notice_placed_video_call_by_you">أجريت مكالمة مرئية.</string>
<string name="notice_placed_voice_call_by_you">أجريت مكالمة صوتية.</string>
<string name="notice_call_candidates">أرسل %s البيانات لإعداد المكالمة.</string>
<string name="notice_call_candidates_by_you">أرسلت البيانات لإعداد المكالمة.</string>
<string name="notice_answered_call_by_you">رددت على المكالمة.</string>
<string name="notice_ended_call_by_you">أنهيت المكالمة.</string>
<string name="notice_made_future_room_visibility_by_you">جعلت تأريخ الغرفة مستقبلًا ظاهرًا على %1$s</string>
<string name="notice_end_to_end_by_you">فعّلت تعمية الطرفين (%1$s)</string>
<string name="notice_room_update">رقّى %s هذه الغرفة.</string>
<string name="notice_room_update_by_you">رقّيت هذه الغرفة.</string>
<string name="notice_requested_voip_conference_by_you">طلبت اجتماع VoIP</string>
<string name="notice_room_name_removed_by_you">أزلت اسم الغرفة</string>
<string name="notice_room_topic_removed_by_you">أزلت موضوع الغرفة</string>
<string name="notice_room_avatar_removed">أزال %1$s صورة الغرفة</string>
<string name="notice_room_avatar_removed_by_you">أزلت صورة الغرفة</string>
<string name="notice_event_redacted">أُزيلت الرسالة</string>
<string name="notice_event_redacted_by">أزال %1$s الرسالة</string>
<string name="notice_event_redacted_with_reason">أُزيلت الرسالة [السبب: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">أزال %1$s الرسالة [السبب: %2$s]</string>
<string name="notice_room_third_party_invite_by_you">أرسلت دعوة إلى %1$s للانضمام إلى الغرفة</string>
<string name="notice_room_third_party_revoked_invite">سحب %1$s دعوة %2$s للانضمام إلى الغرفة</string>
<string name="notice_room_third_party_revoked_invite_by_you">سحبت دعوة %1$s للانضمام إلى الغرفة</string>
<string name="notice_room_third_party_registered_invite_by_you">قَبِلت دعوة %1$s</string>
<string name="notice_widget_added">أضاف %1$s الودجة %2$s</string>
<string name="notice_widget_added_by_you">أضفت الودجة %1$s</string>
<string name="notice_widget_removed">أزال %1$s الودجة %2$s</string>
<string name="notice_widget_removed_by_you">أزلت الودجة %1$s</string>
<string name="notice_widget_modified">عدّل %1$s الودجة %2$s</string>
<string name="notice_widget_modified_by_you">عدّلت الودجة %1$s</string>
<string name="power_level_admin">مدير</string>
<string name="power_level_default">المبدئي</string>
<string name="power_level_custom">مخصّص (%1$d)</string>
<string name="power_level_custom_no_value">مخصّص</string>
<string name="notice_power_level_changed_by_you">غيّرت مستوى قوّة %1$s.</string>
<string name="notice_power_level_changed">غيّر %1$s مستوى قوّة %2$s.</string>
<string name="notice_power_level_diff">%1$s من %2$s إلى %3$s</string>
<string name="initial_sync_start_importing_account">المزامنة الأولية:
\nيستورد الحساب…</string>
</resources> </resources>

View File

@ -147,4 +147,98 @@
<string name="key_verification_request_fallback_message">%s solicita verificar su clave, pero su cliente no soporta la verificación de la clave en chat. Necesitará usar la verificación de claves clásica para poder verificar las claves.</string> <string name="key_verification_request_fallback_message">%s solicita verificar su clave, pero su cliente no soporta la verificación de la clave en chat. Necesitará usar la verificación de claves clásica para poder verificar las claves.</string>
<string name="summary_you_sent_image">Enviaste una imagen.</string>
<string name="summary_you_sent_sticker">Enviaste un sticker.</string>
<string name="notice_room_invite_no_invitee_by_you">Tu invitación</string>
<string name="notice_room_created">%1$s creó la habitación</string>
<string name="notice_room_created_by_you">Tu creaste la habitación</string>
<string name="notice_room_invite_by_you">Invitaste a %1$s</string>
<string name="notice_room_join_by_you">Te uniste a la Sala</string>
<string name="notice_room_leave_by_you">Dejaste la Sala</string>
<string name="notice_room_reject_by_you">Rechazaste la invitación</string>
<string name="notice_room_kick_by_you">Tu pateaste a %1$s</string>
<string name="notice_room_unban_by_you">Tu desbanaste a %1$s</string>
<string name="notice_room_ban_by_you">Usted prohibió a %1$s</string>
<string name="notice_room_withdraw_by_you">Retiró la invitación de %1$s\'s</string>
<string name="notice_avatar_url_changed_by_you">Cambiaste tu avatar</string>
<string name="notice_display_name_set_by_you">Establece su nombre de visualización en %1$s</string>
<string name="notice_display_name_changed_from_by_you">Cambiaste tu nombre para mostrar de %1$s a %2$s</string>
<string name="notice_display_name_removed_by_you">Quitaste tu nombre para mostrar (era %1$s)</string>
<string name="notice_room_topic_changed_by_you">Cambiaste el tema a: %1$s</string>
<string name="notice_room_avatar_changed">%1$s cambió el avatar de la sala</string>
<string name="notice_room_avatar_changed_by_you">Cambiaste el avatar de la habitación</string>
<string name="notice_room_name_changed_by_you">Cambiaste el nombre de la habitación a: %1$s</string>
<string name="notice_placed_video_call_by_you">Hiciste una videollamada.</string>
<string name="notice_placed_voice_call_by_you">Hiciste una llamada de voz.</string>
<string name="notice_call_candidates">%s envió datos para configurar la llamada.</string>
<string name="notice_call_candidates_by_you">Enviaste datos para configurar la llamada.</string>
<string name="notice_answered_call_by_you">Respondiste la llamada.</string>
<string name="notice_ended_call_by_you">Terminaste la llamada.</string>
<string name="notice_made_future_room_visibility_by_you">Hiciste visible el futuro historial de la %1$s</string>
<string name="notice_end_to_end_by_you">Activó el cifrado de un extremo a otro (%1$s)</string>
<string name="notice_room_update_by_you">Has mejorado esta habitación.</string>
<string name="notice_requested_voip_conference_by_you">Solicitaste una conferencia de VoIP</string>
<string name="notice_room_name_removed_by_you">Quitaste el nombre de la sala</string>
<string name="notice_room_topic_removed_by_you">Quitaste el tema de la sala</string>
<string name="notice_room_avatar_removed">%1$s eliminó el avatar de la habitación</string>
<string name="notice_room_avatar_removed_by_you">Quitaste el avatar de la habitación</string>
<string name="notice_profile_change_redacted_by_you">Actualizaste tu perfil %1$s</string>
<string name="notice_room_third_party_invite_by_you">Enviaste una invitación a %1$s para unirse a la sala</string>
<string name="notice_room_third_party_revoked_invite_by_you">Revocaste la invitación para que %1$s se una a la sala</string>
<string name="notice_room_third_party_registered_invite_by_you">Aceptaste la invitación para %1$s</string>
<string name="notice_widget_added">%1$s agrego el widget %2$s</string>
<string name="notice_widget_added_by_you">Agregaste el widget %1$s</string>
<string name="notice_widget_removed">%1$s eliminó el widget %2$s</string>
<string name="notice_widget_removed_by_you">Quitaste el widget %1$s</string>
<string name="notice_widget_modified">%1$s modifico el widget %2$s</string>
<string name="notice_widget_modified_by_you">Modificaste el widget %1$s</string>
<string name="power_level_admin">Administrador</string>
<string name="power_level_moderator">Moderador</string>
<string name="power_level_default">Por defecto</string>
<string name="power_level_custom">Personalizado (%1$d)</string>
<string name="power_level_custom_no_value">Personalizado</string>
<string name="notice_power_level_changed_by_you">Cambiaste el nivel de potencia de %1$s.</string>
<string name="notice_power_level_changed">%1$s cambió el nivel de potencia de %2$s.</string>
<string name="notice_power_level_diff">%1$s de %2$s a %3$s</string>
<string name="notice_room_invite_no_invitee_with_reason_by_you">Tu invitación. Razón: %1$s</string>
<string name="notice_room_invite_with_reason_by_you">"nvitaste a %1$s. Razón: %2$s"</string>
<string name="notice_room_join_with_reason_by_you">Te uniste a la habitación. Razón: %1$s</string>
<string name="notice_room_leave_with_reason_by_you">Dejaste la habitación. Razón: %1$s</string>
<string name="notice_room_reject_with_reason_by_you">Rechazaste la invitación. Razón: %1$s</string>
<string name="notice_room_kick_with_reason_by_you">Pateaste a %1$s. Motivo: %2$s</string>
<string name="notice_room_unban_with_reason_by_you">Has desactivado a %1$s. Motivo: %2$s</string>
<string name="notice_room_ban_with_reason_by_you">Prohibiste a %1$s. Motivo: %2$s</string>
<string name="notice_room_third_party_invite_with_reason_by_you">Enviaste una invitación a %1$s para unirse a la sala. Motivo: %2$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Revocaste la invitación para que %1$s se una a la sala. Motivo: %2$s</string>
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Aceptaste la invitación para %1$s. Motivo: %2$s</string>
<string name="notice_room_withdraw_with_reason_by_you">Retiró la invitación de %1$s\'s. Motivo: %2$s</string>
<plurals name="notice_room_aliases_added_by_you">
<item quantity="one">Agregaste %1$s como dirección para esta sala.</item>
<item quantity="other">Agregaste %1$s como direcciones para esta sala.</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">Quitaste %1$s como dirección para esta sala.</item>
<item quantity="other">Quitaste %2$s como direcciones para esta sala.</item>
</plurals>
<string name="notice_room_aliases_added_and_removed">"%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala."</string>
<string name="notice_room_aliases_added_and_removed_by_you">Agregaste %1$s y quitaste %2$s como direcciones para esta sala.</string>
<string name="notice_room_canonical_alias_set_by_you">Estableciste la dirección principal de esta sala en %1$s.</string>
<string name="notice_room_canonical_alias_unset_by_you">Quitaste la dirección principal de esta sala.</string>
<string name="notice_room_guest_access_can_join_by_you">Ha permitido que los invitados se unan a la sala.</string>
<string name="notice_room_guest_access_forbidden_by_you">Ha impedido que los invitados se unan a la sala.</string>
<string name="notice_end_to_end_ok_by_you">Activó el cifrado de extremo a extremo.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">Activó el cifrado de un extremo a otro (algoritmo %1$s no reconocido).</string>
</resources> </resources>

View File

@ -151,7 +151,7 @@ android\.app\.AlertDialog
new Gson\(\) new Gson\(\)
### Use matrixOneTimeWorkRequestBuilder ### Use matrixOneTimeWorkRequestBuilder
import androidx.work.OneTimeWorkRequestBuilder===1 import androidx.work.OneTimeWorkRequestBuilder===2
### Use TextUtils.formatFileSize ### Use TextUtils.formatFileSize
Formatter\.formatFileSize===1 Formatter\.formatFileSize===1
@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt
enum class===76 enum class===78
### Do not import temporary legacy classes ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3
@ -172,3 +172,6 @@ import org.matrix.androidsdk.crypto.data===2
### Use `Context#getSystemService` extension function provided by `core-ktx` ### Use `Context#getSystemService` extension function provided by `core-ktx`
getSystemService\(Context getSystemService\(Context
### Use DefaultSharedPreferences.getInstance() instead for better performance
PreferenceManager\.getDefaultSharedPreferences==2

View File

@ -17,7 +17,7 @@ androidExtensions {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 0 ext.versionMinor = 0
ext.versionPatch = 6 ext.versionPatch = 7
ext.scVersion = 19 ext.scVersion = 19
@ -194,6 +194,8 @@ android {
resValue "bool", "debug_mode", "true" resValue "bool", "debug_mode", "true"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
// Set to true if you want to enable strict mode in debug
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
@ -203,6 +205,7 @@ android {
resValue "bool", "debug_mode", "false" resValue "bool", "debug_mode", "false"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
postprocessing { postprocessing {
removeUnusedCode true removeUnusedCode true

View File

@ -22,7 +22,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.core.utils.lsFiles import im.vector.app.core.utils.lsFiles
import timber.log.Timber import timber.log.Timber
@ -44,7 +44,7 @@ class DebugReceiver : BroadcastReceiver() {
} }
private fun dumpPreferences(context: Context) { private fun dumpPreferences(context: Context) {
logPrefs("DefaultSharedPreferences", PreferenceManager.getDefaultSharedPreferences(context)) logPrefs("DefaultSharedPreferences", DefaultSharedPreferences.getInstance(context))
} }
private fun logPrefs(name: String, sharedPreferences: SharedPreferences?) { private fun logPrefs(name: String, sharedPreferences: SharedPreferences?) {
@ -58,7 +58,7 @@ class DebugReceiver : BroadcastReceiver() {
} }
private fun alterScalarToken(context: Context) { private fun alterScalarToken(context: Context) {
PreferenceManager.getDefaultSharedPreferences(context).edit { DefaultSharedPreferences.getInstance(context).edit {
// putString("SCALAR_TOKEN_PREFERENCE_KEY" + Matrix.getInstance(context).defaultSession.myUserId, "bad_token") // putString("SCALAR_TOKEN_PREFERENCE_KEY" + Matrix.getInstance(context).defaultSession.myUserId, "bad_token")
} }
} }

View File

@ -4,6 +4,11 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--
Required for long polling account synchronisation in background.
If not present ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent action won't work
-->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application> <application>

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.fdroid
import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import timber.log.Timber
object BackgroundSyncStarter {
fun start(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
if (vectorPreferences.areNotificationEnabledForDevice()) {
val activeSession = activeSessionHolder.getSafeActiveSession() ?: return
when (vectorPreferences.getFdroidSyncBackgroundMode()) {
BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> {
// we rely on periodic worker
Timber.i("## Sync: Work scheduled to periodically sync in ${vectorPreferences.backgroundSyncDelay()}s")
activeSession.startAutomaticBackgroundSync(
vectorPreferences.backgroundSyncTimeOut().toLong(),
vectorPreferences.backgroundSyncDelay().toLong()
)
}
BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME -> {
// We need to use alarm in this mode
AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, vectorPreferences.backgroundSyncDelay())
Timber.i("## Sync: Alarm scheduled to start syncing")
}
BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> {
// we do nothing
Timber.i("## Sync: background sync is disabled")
}
}
}
}
}

View File

@ -15,29 +15,30 @@
*/ */
package im.vector.app.fdroid.features.settings.troubleshoot package im.vector.app.fdroid.features.settings.troubleshoot
import androidx.fragment.app.Fragment import androidx.appcompat.app.AppCompatActivity
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.isIgnoringBatteryOptimizations import im.vector.app.core.utils.isIgnoringBatteryOptimizations
import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.core.utils.requestDisablingBatteryOptimization
import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.app.features.settings.troubleshoot.TroubleshootTest import im.vector.app.features.settings.troubleshoot.TroubleshootTest
import javax.inject.Inject
// Not used anymore class TestBatteryOptimization @Inject constructor(
class TestBatteryOptimization(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { private val context: AppCompatActivity,
private val stringProvider: StringProvider
) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) {
override fun perform() { override fun perform() {
val context = fragment.context if (isIgnoringBatteryOptimizations(context)) {
if (context != null && isIgnoringBatteryOptimizations(context)) { description = stringProvider.getString(R.string.settings_troubleshoot_test_battery_success)
description = fragment.getString(R.string.settings_troubleshoot_test_battery_success)
status = TestStatus.SUCCESS status = TestStatus.SUCCESS
quickFix = null quickFix = null
} else { } else {
description = fragment.getString(R.string.settings_troubleshoot_test_battery_failed) description = stringProvider.getString(R.string.settings_troubleshoot_test_battery_failed)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_battery_quickfix) { quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_battery_quickfix) {
override fun doFix() { override fun doFix() {
fragment.activity?.let { requestDisablingBatteryOptimization(context, null, NotificationTroubleshootTestManager.REQ_CODE_FIX)
requestDisablingBatteryOptimization(it, fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX)
}
} }
} }
status = TestStatus.FAILED status = TestStatus.FAILED

View File

@ -22,16 +22,18 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.PowerManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.HasVectorInjector
import im.vector.app.core.services.VectorSyncService import im.vector.app.core.services.VectorSyncService
import androidx.core.content.getSystemService import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.internal.session.sync.job.SyncService import org.matrix.android.sdk.internal.session.sync.job.SyncService
import timber.log.Timber import timber.log.Timber
class AlarmSyncBroadcastReceiver : BroadcastReceiver() { class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
lateinit var vectorPreferences: VectorPreferences
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val appContext = context.applicationContext val appContext = context.applicationContext
if (appContext is HasVectorInjector) { if (appContext is HasVectorInjector) {
@ -40,41 +42,35 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
Timber.v("No active session don't launch sync service.") Timber.v("No active session don't launch sync service.")
return return
} }
} vectorPreferences = appContext.injector().vectorPreferences()
// Acquire a lock to give enough time for the sync :/
context.getSystemService<PowerManager>()!!.run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply {
acquire((10_000).toLong())
}
} }
val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return
// This method is called when the BroadcastReceiver is receiving an Intent broadcast. // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
Timber.d("RestartBroadcastReceiver received intent") Timber.d("RestartBroadcastReceiver received intent")
VectorSyncService.newIntent(context, sessionId).let { VectorSyncService.newPeriodicIntent(context, sessionId, vectorPreferences.backgroundSyncTimeOut(), vectorPreferences.backgroundSyncDelay()).let {
try { try {
ContextCompat.startForegroundService(context, it) ContextCompat.startForegroundService(context, it)
} catch (ex: Throwable) { } catch (ex: Throwable) {
// TODO Timber.i("## Sync: Failed to start service, Alarm scheduled to restart service")
scheduleAlarm(context, sessionId, vectorPreferences.backgroundSyncDelay())
Timber.e(ex) Timber.e(ex)
} }
} }
scheduleAlarm(context, sessionId, 30_000L)
Timber.i("Alarm scheduled to restart service")
} }
companion object { companion object {
private const val REQUEST_CODE = 0 private const val REQUEST_CODE = 0
fun scheduleAlarm(context: Context, sessionId: String, delay: Long) { fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int) {
// Reschedule // Reschedule
Timber.v("## Sync: Scheduling alarm for background sync in $delayInSeconds seconds")
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply { val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply {
putExtra(SyncService.EXTRA_SESSION_ID, sessionId) putExtra(SyncService.EXTRA_SESSION_ID, sessionId)
putExtra(SyncService.EXTRA_PERIODIC, true)
} }
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT) val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val firstMillis = System.currentTimeMillis() + delay val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L
val alarmMgr = context.getSystemService<AlarmManager>()!! val alarmMgr = context.getSystemService<AlarmManager>()!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent) alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
@ -84,11 +80,20 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
} }
fun cancelAlarm(context: Context) { fun cancelAlarm(context: Context) {
Timber.v("Cancel alarm") Timber.v("## Sync: Cancel alarm for background sync")
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java) val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT) val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr = context.getSystemService<AlarmManager>()!! val alarmMgr = context.getSystemService<AlarmManager>()!!
alarmMgr.cancel(pIntent) alarmMgr.cancel(pIntent)
// Stop current service to restart
VectorSyncService.stopIntent(context).let {
try {
ContextCompat.startForegroundService(context, it)
} catch (ex: Throwable) {
Timber.i("## Sync: Cancel sync")
}
}
} }
} }
} }

View File

@ -21,6 +21,8 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.HasVectorInjector
import im.vector.app.core.extensions.vectorComponent
import im.vector.app.fdroid.BackgroundSyncStarter
import timber.log.Timber import timber.log.Timber
class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {
@ -29,10 +31,11 @@ class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {
Timber.v("## onReceive() ${intent.action}") Timber.v("## onReceive() ${intent.action}")
val appContext = context.applicationContext val appContext = context.applicationContext
if (appContext is HasVectorInjector) { if (appContext is HasVectorInjector) {
val activeSession = appContext.injector().activeSessionHolder().getSafeActiveSession() BackgroundSyncStarter.start(
if (activeSession != null) { context,
AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, 10) appContext.vectorComponent().vectorPreferences(),
} appContext.injector().activeSessionHolder()
)
} }
} }
} }

View File

@ -22,9 +22,9 @@ import android.app.Activity
import android.content.Context import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.fdroid.BackgroundSyncStarter
import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import timber.log.Timber
/** /**
* This class has an alter ego in the gplay variant. * This class has an alter ego in the gplay variant.
@ -61,16 +61,13 @@ object FcmHelper {
// No op // No op
} }
fun onEnterForeground(context: Context) { fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) {
// try to stop all regardless of background mode
activeSessionHolder.getSafeActiveSession()?.stopAnyBackgroundSync()
AlarmSyncBroadcastReceiver.cancelAlarm(context) AlarmSyncBroadcastReceiver.cancelAlarm(context)
} }
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
// We need to use alarm in this mode BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder)
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) {
val currentSession = activeSessionHolder.getActiveSession()
AlarmSyncBroadcastReceiver.scheduleAlarm(context, currentSession.sessionId, 4_000L)
Timber.i("Alarm scheduled to restart service")
}
} }
} }

View File

@ -18,6 +18,7 @@ package im.vector.app.push.fcm
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.app.fdroid.features.settings.troubleshoot.TestAutoStartBoot import im.vector.app.fdroid.features.settings.troubleshoot.TestAutoStartBoot
import im.vector.app.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions import im.vector.app.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions
import im.vector.app.fdroid.features.settings.troubleshoot.TestBatteryOptimization
import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.app.features.settings.troubleshoot.TestAccountSettings import im.vector.app.features.settings.troubleshoot.TestAccountSettings
import im.vector.app.features.settings.troubleshoot.TestDeviceSettings import im.vector.app.features.settings.troubleshoot.TestDeviceSettings
@ -30,7 +31,8 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
private val testDeviceSettings: TestDeviceSettings, private val testDeviceSettings: TestDeviceSettings,
private val testPushRulesSettings: TestPushRulesSettings, private val testPushRulesSettings: TestPushRulesSettings,
private val testAutoStartBoot: TestAutoStartBoot, private val testAutoStartBoot: TestAutoStartBoot,
private val testBackgroundRestrictions: TestBackgroundRestrictions) { private val testBackgroundRestrictions: TestBackgroundRestrictions,
private val testBatteryOptimization: TestBatteryOptimization) {
fun create(fragment: Fragment): NotificationTroubleshootTestManager { fun create(fragment: Fragment): NotificationTroubleshootTestManager {
val mgr = NotificationTroubleshootTestManager(fragment) val mgr = NotificationTroubleshootTestManager(fragment)
@ -40,6 +42,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
mgr.addTest(testPushRulesSettings) mgr.addTest(testPushRulesSettings)
mgr.addTest(testAutoStartBoot) mgr.addTest(testAutoStartBoot)
mgr.addTest(testBackgroundRestrictions) mgr.addTest(testBackgroundRestrictions)
mgr.addTest(testBatteryOptimization)
return mgr return mgr
} }
} }

View File

@ -130,14 +130,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceivedInternal() : $data") Timber.i("## onMessageReceivedInternal() : $data")
} }
val eventId = data["event_id"]
val roomId = data["room_id"]
if (eventId == null || roomId == null) {
Timber.e("## onMessageReceivedInternal() missing eventId and/or roomId")
return
}
// update the badge counter // update the badge counter
val unreadCount = data.get("unread")?.let { Integer.parseInt(it) } ?: 0 val unreadCount = data["unread"]?.let { Integer.parseInt(it) } ?: 0
BadgeProxy.updateBadgeCount(applicationContext, unreadCount) BadgeProxy.updateBadgeCount(applicationContext, unreadCount)
val session = activeSessionHolder.getSafeActiveSession() val session = activeSessionHolder.getSafeActiveSession()
@ -145,6 +140,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
if (session == null) { if (session == null) {
Timber.w("## Can't sync from push, no current session") Timber.w("## Can't sync from push, no current session")
} else { } else {
val eventId = data["event_id"]
val roomId = data["room_id"]
if (isEventAlreadyKnown(eventId, roomId)) { if (isEventAlreadyKnown(eventId, roomId)) {
Timber.i("Ignoring push, event already known") Timber.i("Ignoring push, event already known")
} else { } else {

View File

@ -19,7 +19,6 @@ package im.vector.app.push.fcm
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
@ -27,6 +26,7 @@ import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.iid.FirebaseInstanceId import com.google.firebase.iid.FirebaseInstanceId
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import timber.log.Timber import timber.log.Timber
@ -46,7 +46,7 @@ object FcmHelper {
* @return the FCM token or null if not received from FCM * @return the FCM token or null if not received from FCM
*/ */
fun getFcmToken(context: Context): String? { fun getFcmToken(context: Context): String? {
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null) return DefaultSharedPreferences.getInstance(context).getString(PREFS_KEY_FCM_TOKEN, null)
} }
/** /**
@ -58,7 +58,7 @@ object FcmHelper {
*/ */
fun storeFcmToken(context: Context, fun storeFcmToken(context: Context,
token: String?) { token: String?) {
PreferenceManager.getDefaultSharedPreferences(context).edit { DefaultSharedPreferences.getInstance(context).edit {
putString(PREFS_KEY_FCM_TOKEN, token) putString(PREFS_KEY_FCM_TOKEN, token)
} }
} }
@ -102,7 +102,7 @@ object FcmHelper {
} }
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun onEnterForeground(context: Context) { fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) {
// No op // No op
} }

View File

@ -21,6 +21,7 @@ import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.os.StrictMode
import androidx.core.provider.FontRequest import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat import androidx.core.provider.FontsContractCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -92,6 +93,7 @@ class VectorApplication :
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
override fun onCreate() { override fun onCreate() {
enableStrictModeIfNeeded()
super.onCreate() super.onCreate()
appContext = this appContext = this
vectorComponent = DaggerVectorComponent.factory().create(this) vectorComponent = DaggerVectorComponent.factory().create(this)
@ -144,7 +146,7 @@ class VectorApplication :
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() { fun entersForeground() {
Timber.i("App entered foreground") Timber.i("App entered foreground")
FcmHelper.onEnterForeground(appContext) FcmHelper.onEnterForeground(appContext, activeSessionHolder)
activeSessionHolder.getSafeActiveSession()?.also { activeSessionHolder.getSafeActiveSession()?.also {
it.stopAnyBackgroundSync() it.stopAnyBackgroundSync()
} }
@ -163,6 +165,15 @@ class VectorApplication :
// initKnownEmojiHashSet(appContext) // initKnownEmojiHashSet(appContext)
} }
private fun enableStrictModeIfNeeded() {
if (BuildConfig.ENABLE_STRICT_MODE_LOGS) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build())
}
}
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION) override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)
override fun getWorkManagerConfiguration(): WorkConfiguration { override fun getWorkManagerConfiguration(): WorkConfiguration {

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import android.text.format.DateFormat
import im.vector.app.core.resources.LocaleProvider
import org.threeten.bp.format.DateTimeFormatter
import javax.inject.Inject
class AbbrevDateFormatterProvider @Inject constructor(private val localeProvider: LocaleProvider) : DateFormatterProvider {
override val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMM")
DateTimeFormatter.ofPattern(pattern, localeProvider.current())
}
override val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "dd.MM.yyyy")
DateTimeFormatter.ofPattern(pattern, localeProvider.current())
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
/* This will represent all kind of available date formats for the app.
We will use the date Sep 7 2020 at 9:30am as an example.
The formatting is depending of the current date.
*/
enum class DateFormatKind {
// Will show date relative and time (today or yesterday or Sep 7 or 09/07/2020 at 9:30am)
DEFAULT_DATE_AND_TIME,
// Will show hour or date relative (9:30am or yesterday or Sep 7 or 09/07/2020)
ROOM_LIST,
// Will show full date (Sep 7 2020)
TIMELINE_DAY_DIVIDER,
// Will show full date and time (Mon, Sep 7 2020, 9:30am)
MESSAGE_DETAIL,
// Will only show time (9:30am)
MESSAGE_SIMPLE,
// Will only show time (9:30am)
EDIT_HISTORY_ROW,
// Will only show date relative (today or yesterday or Sep 7 or 09/07/2020)
EDIT_HISTORY_HEADER
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import org.threeten.bp.format.DateTimeFormatter
interface DateFormatterProvider {
val dateWithMonthFormatter: DateTimeFormatter
val dateWithYearFormatter: DateTimeFormatter
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import javax.inject.Inject
class DateFormatterProviders @Inject constructor(private val defaultDateFormatterProvider: DefaultDateFormatterProvider,
private val abbrevDateFormatterProvider: AbbrevDateFormatterProvider) {
fun provide(abbrev: Boolean): DateFormatterProvider {
return if (abbrev) {
abbrevDateFormatterProvider
} else {
defaultDateFormatterProvider
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import android.content.Context
import android.text.format.DateFormat
import im.vector.app.core.resources.LocaleProvider
import org.threeten.bp.format.DateTimeFormatter
import javax.inject.Inject
class DefaultDateFormatterProvider @Inject constructor(private val context: Context,
private val localeProvider: LocaleProvider)
: DateFormatterProvider {
override val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMMMM")
DateTimeFormatter.ofPattern(pattern)
}
override val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMM y")
DateTimeFormatter.ofPattern(pattern)
}
}

View File

@ -19,64 +19,147 @@ package im.vector.app.core.date
import android.content.Context import android.content.Context
import android.text.format.DateFormat import android.text.format.DateFormat
import android.text.format.DateUtils import android.text.format.DateUtils
import im.vector.app.core.resources.DateProvider
import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.toTimestamp
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
import org.threeten.bp.Period
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue
/**
* Returns the timestamp for the start of the day of the provided time.
* For example, for the time "Jul 21, 11:11" the start of the day: "Jul 21, 00:00" is returned.
*/
fun startOfDay(time: Long): Long {
val calendar = Calendar.getInstance()
calendar.time = Date(time)
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
return calendar.time.time
}
class VectorDateFormatter @Inject constructor(private val context: Context, class VectorDateFormatter @Inject constructor(private val context: Context,
private val localeProvider: LocaleProvider) { private val localeProvider: LocaleProvider,
private val dateFormatterProviders: DateFormatterProviders) {
private val messageHourFormatter by lazy { private val hourFormatter by lazy {
DateTimeFormatter.ofPattern("H:mm", localeProvider.current()) if (DateFormat.is24HourFormat(context)) {
DateTimeFormatter.ofPattern("HH:mm", localeProvider.current())
} else {
DateTimeFormatter.ofPattern("h:mm a", localeProvider.current())
}
} }
private val messageDayFormatter by lazy { private val fullDateFormatter by lazy {
DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(localeProvider.current(), "EEE d MMM")) val pattern = if (DateFormat.is24HourFormat(context)) {
} DateFormat.getBestDateTimePattern(localeProvider.current(), "EEE, d MMM yyyy HH:mm")
} else {
fun formatMessageHour(localDateTime: LocalDateTime): String { DateFormat.getBestDateTimePattern(localeProvider.current(), "EEE, d MMM yyyy h:mm a")
return messageHourFormatter.format(localDateTime) }
} DateTimeFormatter.ofPattern(pattern, localeProvider.current())
fun formatMessageDay(localDateTime: LocalDateTime): String {
return messageDayFormatter.format(localDateTime)
} }
/** /**
* Formats a localized relative date time for the last 2 days, e.g, "Today, HH:MM", "Yesterday, HH:MM" or * This method is used to format some date in the app.
* "2 days ago, HH:MM". * It will be able to show only time, only date or both with some logic.
* For earlier timestamps the absolute date time is returned, e.g. "Month Day, HH:MM". * @param ts the timestamp to format or null.
* @param dateFormatKind the kind of format to use
* *
* @param time the absolute timestamp [ms] that should be formatted relative to now * @return the formatted date as string.
*/ */
fun formatRelativeDateTime(time: Long?): String { fun format(ts: Long?, dateFormatKind: DateFormatKind): String {
if (time == null) { if (ts == null) return "-"
val localDateTime = DateProvider.toLocalDateTime(ts)
return when (dateFormatKind) {
DateFormatKind.DEFAULT_DATE_AND_TIME -> formatDateAndTime(ts)
DateFormatKind.ROOM_LIST -> formatTimeOrDate(
date = localDateTime,
showTimeIfSameDay = true,
abbrev = true,
useRelative = true
)
DateFormatKind.TIMELINE_DAY_DIVIDER -> formatTimeOrDate(
date = localDateTime,
alwaysShowYear = true
)
DateFormatKind.MESSAGE_DETAIL -> formatFullDate(localDateTime)
DateFormatKind.MESSAGE_SIMPLE -> formatHour(localDateTime)
DateFormatKind.EDIT_HISTORY_ROW -> formatHour(localDateTime)
DateFormatKind.EDIT_HISTORY_HEADER -> formatTimeOrDate(
date = localDateTime,
abbrev = true,
useRelative = true
)
}
}
private fun formatFullDate(localDateTime: LocalDateTime): String {
return fullDateFormatter.format(localDateTime)
}
private fun formatHour(localDateTime: LocalDateTime): String {
return hourFormatter.format(localDateTime)
}
private fun formatDateWithMonth(localDateTime: LocalDateTime, abbrev: Boolean = false): String {
return dateFormatterProviders.provide(abbrev).dateWithMonthFormatter.format(localDateTime)
}
private fun formatDateWithYear(localDateTime: LocalDateTime, abbrev: Boolean = false): String {
return dateFormatterProviders.provide(abbrev).dateWithYearFormatter.format(localDateTime)
}
/**
* This method will only show time or date following the parameters.
*/
private fun formatTimeOrDate(
date: LocalDateTime?,
showTimeIfSameDay: Boolean = false,
useRelative: Boolean = false,
alwaysShowYear: Boolean = false,
abbrev: Boolean = false
): String {
if (date == null) {
return "" return ""
} }
val now = System.currentTimeMillis() val currentDate = DateProvider.currentLocalDateTime()
return DateUtils.getRelativeDateTimeString( val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
context, return if (showTimeIfSameDay && isSameDay) {
time, formatHour(date)
} else {
formatDate(date, currentDate, alwaysShowYear, abbrev, useRelative)
}
}
private fun formatDate(
date: LocalDateTime,
currentDate: LocalDateTime,
alwaysShowYear: Boolean,
abbrev: Boolean,
useRelative: Boolean
): String {
val period = Period.between(date.toLocalDate(), currentDate.toLocalDate())
return if (period.years.absoluteValue >= 1 || alwaysShowYear) {
formatDateWithYear(date, abbrev)
} else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
getRelativeDay(date.toTimestamp())
} else {
formatDateWithMonth(date, abbrev)
}
}
/**
* This method will show date and time with a preposition
*/
private fun formatDateAndTime(ts: Long): String {
val date = DateProvider.toLocalDateTime(ts)
val currentDate = DateProvider.currentLocalDateTime()
// This fake date is created to be able to use getRelativeTimeSpanString so we can get a "at"
// preposition and the right am/pm management.
val fakeDate = LocalDateTime.of(currentDate.toLocalDate(), date.toLocalTime())
val formattedTime = DateUtils.getRelativeTimeSpanString(context, fakeDate.toTimestamp(), true).toString()
val formattedDate = formatDate(date, currentDate, alwaysShowYear = false, abbrev = true, useRelative = true)
return "$formattedDate $formattedTime"
}
/**
* We are using this method for the keywords Today/Yesterday
*/
private fun getRelativeDay(ts: Long): String {
return DateUtils.getRelativeTimeSpanString(
ts,
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS, DateUtils.DAY_IN_MILLIS,
now - startOfDay(now - 2 * DateUtils.DAY_IN_MILLIS), DateUtils.FORMAT_SHOW_WEEKDAY).toString()
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME
).toString()
} }
} }

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.di
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
object DefaultSharedPreferences {
@Volatile private var INSTANCE: SharedPreferences? = null
fun getInstance(context: Context): SharedPreferences =
INSTANCE ?: synchronized(this) {
INSTANCE ?: PreferenceManager.getDefaultSharedPreferences(context.applicationContext).also { INSTANCE = it }
}
}

View File

@ -57,6 +57,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import javax.inject.Singleton import javax.inject.Singleton
@ -118,6 +119,8 @@ interface VectorComponent {
fun authenticationService(): AuthenticationService fun authenticationService(): AuthenticationService
fun rawService(): RawService
fun bugReporter(): BugReporter fun bugReporter(): BugReporter
fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler

View File

@ -34,6 +34,7 @@ import im.vector.app.features.ui.UiStateRepository
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@Module @Module
@ -78,6 +79,12 @@ abstract class VectorModule {
fun providesAuthenticationService(matrix: Matrix): AuthenticationService { fun providesAuthenticationService(matrix: Matrix): AuthenticationService {
return matrix.authenticationService() return matrix.authenticationService()
} }
@Provides
@JvmStatic
fun providesRawService(matrix: Matrix): RawService {
return matrix.rawService()
}
} }
@Binds @Binds

View File

@ -22,36 +22,63 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) { fun VectorBaseActivity.addFragment(
supportFragmentManager.commitTransaction { add(frameId, fragment) } frameId: Int,
fragment: Fragment,
allowStateLoss: Boolean = false
) {
supportFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment) }
} }
fun <T : Fragment> VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseActivity.addFragment(
supportFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
supportFragmentManager.commitTransaction(allowStateLoss) {
add(frameId, fragmentClass, params.toMvRxBundle(), tag) add(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseActivity.replaceFragment(
supportFragmentManager.commitTransaction { replace(frameId, fragment, tag) } frameId: Int,
fragment: Fragment,
tag: String? = null,
allowStateLoss: Boolean = false
) {
supportFragmentManager.commitTransaction(allowStateLoss) { replace(frameId, fragment, tag) }
} }
fun <T : Fragment> VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseActivity.replaceFragment(
supportFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
supportFragmentManager.commitTransaction(allowStateLoss) {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseActivity.addFragmentToBackstack(
supportFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) } frameId: Int,
fragment: Fragment,
tag: String? = null,
allowStateLoss: Boolean = false
) {
supportFragmentManager.commitTransaction(allowStateLoss) { replace(frameId, fragment).addToBackStack(tag) }
} }
fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int, fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int,
fragmentClass: Class<T>, fragmentClass: Class<T>,
params: Parcelable? = null, params: Parcelable? = null,
tag: String? = null, tag: String? = null,
allowStateLoss: Boolean = false,
option: ((FragmentTransaction) -> Unit)? = null) { option: ((FragmentTransaction) -> Unit)? = null) {
supportFragmentManager.commitTransaction { supportFragmentManager.commitTransaction(allowStateLoss) {
option?.invoke(this) option?.invoke(this)
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
} }

View File

@ -26,62 +26,126 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) { fun VectorBaseFragment.addFragment(
parentFragmentManager.commitTransaction { add(frameId, fragment) } frameId: Int,
fragment: Fragment,
allowStateLoss: Boolean = false
) {
parentFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment) }
} }
fun <T : Fragment> VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.addFragment(
parentFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
parentFragmentManager.commitTransaction(allowStateLoss) {
add(frameId, fragmentClass, params.toMvRxBundle(), tag) add(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) { fun VectorBaseFragment.replaceFragment(
parentFragmentManager.commitTransaction { replace(frameId, fragment) } frameId: Int,
fragment: Fragment,
allowStateLoss: Boolean = false
) {
parentFragmentManager.commitTransaction(allowStateLoss) { replace(frameId, fragment) }
} }
fun <T : Fragment> VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.replaceFragment(
parentFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
parentFragmentManager.commitTransaction(allowStateLoss) {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseFragment.addFragmentToBackstack(
parentFragmentManager.commitTransaction { replace(frameId, fragment, tag).addToBackStack(tag) } frameId: Int,
fragment: Fragment,
tag: String? = null,
allowStateLoss: Boolean = false
) {
parentFragmentManager.commitTransaction(allowStateLoss) { replace(frameId, fragment, tag).addToBackStack(tag) }
} }
fun <T : Fragment> VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.addFragmentToBackstack(
parentFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
parentFragmentManager.commitTransaction(allowStateLoss) {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
} }
} }
fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseFragment.addChildFragment(
childFragmentManager.commitTransaction { add(frameId, fragment, tag) } frameId: Int,
fragment: Fragment,
tag: String? = null,
allowStateLoss: Boolean = false
) {
childFragmentManager.commitTransaction(allowStateLoss) { add(frameId, fragment, tag) }
} }
fun <T : Fragment> VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.addChildFragment(
childFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
childFragmentManager.commitTransaction(allowStateLoss) {
add(frameId, fragmentClass, params.toMvRxBundle(), tag) add(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseFragment.replaceChildFragment(
childFragmentManager.commitTransaction { replace(frameId, fragment, tag) } frameId: Int,
fragment: Fragment,
tag: String? = null,
allowStateLoss: Boolean = false
) {
childFragmentManager.commitTransaction(allowStateLoss) { replace(frameId, fragment, tag) }
} }
fun <T : Fragment> VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.replaceChildFragment(
childFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
childFragmentManager.commitTransaction(allowStateLoss) {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseFragment.addChildFragmentToBackstack(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseFragment.addChildFragmentToBackstack(
childFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) } frameId: Int,
fragment: Fragment,
tag: String? = null,
allowStateLoss: Boolean = false
) {
childFragmentManager.commitTransaction(allowStateLoss) { replace(frameId, fragment).addToBackStack(tag) }
} }
fun <T : Fragment> VectorBaseFragment.addChildFragmentToBackstack(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.addChildFragmentToBackstack(
childFragmentManager.commitTransaction { frameId: Int,
fragmentClass: Class<T>,
params: Parcelable? = null,
tag: String? = null,
allowStateLoss: Boolean = false
) {
childFragmentManager.commitTransaction(allowStateLoss) {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag)
} }
} }

View File

@ -27,6 +27,11 @@ inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: Frag
} }
} }
inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) { inline fun androidx.fragment.app.FragmentManager.commitTransaction(allowStateLoss: Boolean = false, func: FragmentTransaction.() -> FragmentTransaction) {
beginTransaction().func().commit() val transaction = beginTransaction().func()
if (allowStateLoss) {
transaction.commitAllowingStateLoss()
} else {
transaction.commit()
}
} }

View File

@ -37,7 +37,8 @@ fun Session.configureAndStart(context: Context) {
fun Session.startSyncing(context: Context) { fun Session.startSyncing(context: Context) {
val applicationContext = context.applicationContext val applicationContext = context.applicationContext
if (!hasAlreadySynced()) { if (!hasAlreadySynced()) {
VectorSyncService.newIntent(applicationContext, sessionId).also { // initial sync is done as a service so it can continue below app lifecycle
VectorSyncService.newOneShotIntent(applicationContext, sessionId, 0).also {
try { try {
ContextCompat.startForegroundService(applicationContext, it) ContextCompat.startForegroundService(applicationContext, it)
} catch (ex: Throwable) { } catch (ex: Throwable) {

View File

@ -100,7 +100,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
Timber.v("Load data: $data") Timber.v("Load data: $data")
if (data.isLocalFile() && data.url != null) { if (data.isLocalFile && data.url != null) {
val initialFile = File(data.url) val initialFile = File(data.url)
callback.onDataReady(initialFile.inputStream()) callback.onDataReady(initialFile.inputStream())
return return

View File

@ -23,6 +23,10 @@ import org.threeten.bp.ZoneId
object DateProvider { object DateProvider {
private val zoneId = ZoneId.systemDefault() private val zoneId = ZoneId.systemDefault()
private val zoneOffset by lazy {
val now = currentLocalDateTime()
zoneId.rules.getOffset(now)
}
fun toLocalDateTime(timestamp: Long?): LocalDateTime { fun toLocalDateTime(timestamp: Long?): LocalDateTime {
val instant = Instant.ofEpochMilli(timestamp ?: 0) val instant = Instant.ofEpochMilli(timestamp ?: 0)
@ -33,4 +37,10 @@ object DateProvider {
val instant = Instant.now() val instant = Instant.now()
return LocalDateTime.ofInstant(instant, zoneId) return LocalDateTime.ofInstant(instant, zoneId)
} }
fun toTimestamp(localDateTime: LocalDateTime): Long {
return localDateTime.toInstant(zoneOffset).toEpochMilli()
}
} }
fun LocalDateTime.toTimestamp(): Long = DateProvider.toTimestamp(this)

View File

@ -44,4 +44,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
fun shouldShowUnimportantCounterBadge(): Boolean { fun shouldShowUnimportantCounterBadge(): Boolean {
return vectorPreferences.shouldShowUnimportantCounterBadge() return vectorPreferences.shouldShowUnimportantCounterBadge()
} }
fun shouldShowRoomMemberStateEvents(): Boolean {
return vectorPreferences.showRoomMemberStateEvents()
}
} }

View File

@ -21,19 +21,56 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkRequest
import androidx.work.Worker
import androidx.work.WorkerParameters
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.vectorComponent import im.vector.app.core.extensions.vectorComponent
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import org.matrix.android.sdk.internal.session.sync.job.SyncService import org.matrix.android.sdk.internal.session.sync.job.SyncService
import timber.log.Timber
class VectorSyncService : SyncService() { class VectorSyncService : SyncService() {
companion object { companion object {
fun newIntent(context: Context, sessionId: String): Intent { fun newOneShotIntent(context: Context, sessionId: String, timeoutSeconds: Int): Intent {
return Intent(context, VectorSyncService::class.java).also { return Intent(context, VectorSyncService::class.java).also {
it.putExtra(EXTRA_SESSION_ID, sessionId) it.putExtra(EXTRA_SESSION_ID, sessionId)
it.putExtra(EXTRA_TIMEOUT_SECONDS, timeoutSeconds)
it.putExtra(EXTRA_PERIODIC, false)
}
}
fun newPeriodicIntent(context: Context, sessionId: String, timeoutSeconds: Int, delayInSeconds: Int): Intent {
return Intent(context, VectorSyncService::class.java).also {
it.putExtra(EXTRA_SESSION_ID, sessionId)
it.putExtra(EXTRA_TIMEOUT_SECONDS, timeoutSeconds)
it.putExtra(EXTRA_PERIODIC, true)
it.putExtra(EXTRA_DELAY_SECONDS, delayInSeconds)
}
}
fun newPeriodicNetworkBackIntent(context: Context, sessionId: String, timeoutSeconds: Int, delayInSeconds: Int): Intent {
return Intent(context, VectorSyncService::class.java).also {
it.putExtra(EXTRA_SESSION_ID, sessionId)
it.putExtra(EXTRA_TIMEOUT_SECONDS, timeoutSeconds)
it.putExtra(EXTRA_PERIODIC, true)
it.putExtra(EXTRA_DELAY_SECONDS, delayInSeconds)
it.putExtra(EXTRA_NETWORK_BACK_RESTART, true)
}
}
fun stopIntent(context: Context): Intent {
return Intent(context, VectorSyncService::class.java).also {
it.action = ACTION_STOP
} }
} }
} }
@ -55,8 +92,30 @@ class VectorSyncService : SyncService() {
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
} }
override fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) { override fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) {
reschedule(sessionId, delay) reschedule(sessionId, timeout, delay)
}
override fun onNetworkError(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) {
Timber.d("## Sync: A network error occured during sync")
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<RestartWhenNetworkOn>()
.setInputData(Data.Builder()
.putString("sessionId", sessionId)
.putInt("timeout", timeout)
.putInt("delay", delay)
.build()
)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
Timber.d("## Sync: Schedule a work to restart service when network will be on")
WorkManager
.getInstance(applicationContext)
.enqueue(uploadWorkRequest)
} }
override fun onDestroy() { override fun onDestroy() {
@ -69,13 +128,13 @@ class VectorSyncService : SyncService() {
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE) notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
} }
private fun reschedule(sessionId: String, delay: Long) { private fun reschedule(sessionId: String, timeout: Int, delay: Int) {
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(this, 0, newIntent(this, sessionId), 0) PendingIntent.getForegroundService(this, 0, newPeriodicIntent(this, sessionId, timeout, delay), 0)
} else { } else {
PendingIntent.getService(this, 0, newIntent(this, sessionId), 0) PendingIntent.getService(this, 0, newPeriodicIntent(this, sessionId, timeout, delay), 0)
} }
val firstMillis = System.currentTimeMillis() + delay val firstMillis = System.currentTimeMillis() + delay * 1000L
val alarmMgr = getSystemService<AlarmManager>()!! val alarmMgr = getSystemService<AlarmManager>()!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
@ -83,4 +142,28 @@ class VectorSyncService : SyncService() {
alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
} }
} }
class RestartWhenNetworkOn(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
val sessionId = inputData.getString("sessionId") ?: return Result.failure()
val timeout = inputData.getInt("timeout", 6)
val delay = inputData.getInt("delay", 60)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(applicationContext, 0, newPeriodicNetworkBackIntent(applicationContext, sessionId, timeout, delay), 0)
} else {
PendingIntent.getService(applicationContext, 0, newPeriodicNetworkBackIntent(applicationContext, sessionId, timeout, delay), 0)
}
val firstMillis = System.currentTimeMillis() + delay * 1000L
val alarmMgr = getSystemService<AlarmManager>(applicationContext, AlarmManager::class.java)!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
} else {
alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
}
// Indicate whether the work finished successfully with the Result
return Result.success()
}
}
} }

View File

@ -23,11 +23,11 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import butterknife.OnClick import butterknife.OnClick
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.DefaultSharedPreferences
import timber.log.Timber import timber.log.Timber
/** /**
@ -57,7 +57,7 @@ class KeysBackupBanner @JvmOverloads constructor(
init { init {
setupView() setupView()
PreferenceManager.getDefaultSharedPreferences(context).edit { DefaultSharedPreferences.getInstance(context).edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
} }
@ -105,17 +105,17 @@ class KeysBackupBanner @JvmOverloads constructor(
state.let { state.let {
when (it) { when (it) {
is State.Setup -> { is State.Setup -> {
PreferenceManager.getDefaultSharedPreferences(context).edit { DefaultSharedPreferences.getInstance(context).edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true) putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true)
} }
} }
is State.Recover -> { is State.Recover -> {
PreferenceManager.getDefaultSharedPreferences(context).edit { DefaultSharedPreferences.getInstance(context).edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version) putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version)
} }
} }
is State.Update -> { is State.Update -> {
PreferenceManager.getDefaultSharedPreferences(context).edit { DefaultSharedPreferences.getInstance(context).edit {
putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version) putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version)
} }
} }
@ -150,7 +150,7 @@ class KeysBackupBanner @JvmOverloads constructor(
private fun renderSetup(nbOfKeys: Int) { private fun renderSetup(nbOfKeys: Int) {
if (nbOfKeys == 0 if (nbOfKeys == 0
|| PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) { || DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) {
// Do not display the setup banner if there is no keys to backup, or if the user has already closed it // Do not display the setup banner if there is no keys to backup, or if the user has already closed it
isVisible = false isVisible = false
} else { } else {
@ -164,7 +164,7 @@ class KeysBackupBanner @JvmOverloads constructor(
} }
private fun renderRecover(version: String) { private fun renderRecover(version: String) {
if (version == PreferenceManager.getDefaultSharedPreferences(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) { if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) {
isVisible = false isVisible = false
} else { } else {
isVisible = true isVisible = true
@ -177,7 +177,7 @@ class KeysBackupBanner @JvmOverloads constructor(
} }
private fun renderUpdate(version: String) { private fun renderUpdate(version: String) {
if (version == PreferenceManager.getDefaultSharedPreferences(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) { if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) {
isVisible = false isVisible = false
} else { } else {
isVisible = true isVisible = true
@ -258,7 +258,7 @@ class KeysBackupBanner @JvmOverloads constructor(
* Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version * Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version
*/ */
fun onRecoverDoneForVersion(context: Context, version: String) { fun onRecoverDoneForVersion(context: Context, version: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit { DefaultSharedPreferences.getInstance(context).edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version) putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version)
} }
} }

View File

@ -21,7 +21,7 @@ import android.media.Ringtone
import android.media.RingtoneManager import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
/** /**
@ -40,7 +40,7 @@ import im.vector.app.features.settings.VectorPreferences
* @see Ringtone * @see Ringtone
*/ */
fun getCallRingtoneUri(context: Context): Uri? { fun getCallRingtoneUri(context: Context): Uri? {
val callRingtone: String? = PreferenceManager.getDefaultSharedPreferences(context) val callRingtone: String? = DefaultSharedPreferences.getInstance(context)
.getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null) .getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null)
callRingtone?.let { callRingtone?.let {
@ -94,7 +94,7 @@ fun getCallRingtoneName(context: Context): String? {
* @see Ringtone * @see Ringtone
*/ */
fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) { fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) {
PreferenceManager.getDefaultSharedPreferences(context) DefaultSharedPreferences.getInstance(context)
.edit { .edit {
putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString()) putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString())
} }
@ -104,14 +104,14 @@ fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) {
* Set using Riot default ringtone * Set using Riot default ringtone
*/ */
fun useRiotDefaultRingtone(context: Context): Boolean { fun useRiotDefaultRingtone(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true) return DefaultSharedPreferences.getInstance(context).getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true)
} }
/** /**
* Ask if default Riot ringtone has to be used * Ask if default Riot ringtone has to be used
*/ */
fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) { fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) {
PreferenceManager.getDefaultSharedPreferences(context) DefaultSharedPreferences.getInstance(context)
.edit { .edit {
putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault) putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault)
} }

View File

@ -24,6 +24,7 @@ import android.content.pm.PackageManager
import android.media.AudioManager import android.media.AudioManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import im.vector.app.core.services.WiredHeadsetStateReceiver import im.vector.app.core.services.WiredHeadsetStateReceiver
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -116,10 +117,19 @@ class CallAudioManager(
// Always disable microphone mute during a WebRTC call. // Always disable microphone mute during a WebRTC call.
setMicrophoneMute(false) setMicrophoneMute(false)
adjustCurrentSoundDevice(mxCall)
}
private fun adjustCurrentSoundDevice(mxCall: MxCall) {
val audioManager = audioManager ?: return
executor.execute { executor.execute {
// If there are no headset, start video output in speaker if (mxCall.state == CallState.LocalRinging && !isHeadsetOn()) {
// (you can't watch the video and have the phone close to your ear) // Always use speaker if incoming call is in ringing state and a headset is not connected
if (mxCall.isVideoCall && !isHeadsetOn()) { Timber.v("##VOIP: AudioManager default to SPEAKER (it is ringing)")
setCurrentSoundDevice(SoundDevice.SPEAKER)
} else if (mxCall.isVideoCall && !isHeadsetOn()) {
// If there are no headset, start video output in speaker
// (you can't watch the video and have the phone close to your ear)
Timber.v("##VOIP: AudioManager default to speaker ") Timber.v("##VOIP: AudioManager default to speaker ")
setCurrentSoundDevice(SoundDevice.SPEAKER) setCurrentSoundDevice(SoundDevice.SPEAKER)
} else { } else {
@ -138,6 +148,11 @@ class CallAudioManager(
} }
} }
fun onCallConnected(mxCall: MxCall) {
Timber.v("##VOIP: AudioManager call answered, adjusting current sound device")
adjustCurrentSoundDevice(mxCall)
}
fun getAvailableSoundDevices(): List<SoundDevice> { fun getAvailableSoundDevices(): List<SoundDevice> {
return ArrayList<SoundDevice>().apply { return ArrayList<SoundDevice>().apply {
if (isBluetoothHeadsetOn()) add(SoundDevice.WIRELESS_HEADSET) if (isBluetoothHeadsetOn()) add(SoundDevice.WIRELESS_HEADSET)

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call
import im.vector.app.core.platform.VectorViewModelAction
sealed class VectorCallViewActions : VectorViewModelAction {
object EndCall : VectorCallViewActions()
object AcceptCall : VectorCallViewActions()
object DeclineCall : VectorCallViewActions()
object ToggleMute : VectorCallViewActions()
object ToggleVideo : VectorCallViewActions()
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : VectorCallViewActions()
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.session.call.TurnServerResponse
sealed class VectorCallViewEvents : VectorViewEvents {
object DismissNoCall : VectorCallViewEvents()
data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents()
data class ShowSoundDeviceChooser(
val available: List<CallAudioManager.SoundDevice>,
val current: CallAudioManager.SoundDevice
) : VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : VectorCallViewEvents()
}

View File

@ -16,10 +16,8 @@
package im.vector.app.features.call package im.vector.app.features.call
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
@ -27,9 +25,7 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
@ -41,48 +37,6 @@ import org.webrtc.PeerConnection
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
data class VectorCallViewState(
val callId: String? = null,
val roomId: String = "",
val isVideoCall: Boolean,
val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true,
val isVideoCaptureInError: Boolean = false,
val isHD: Boolean = false,
val isFrontCamera: Boolean = true,
val canSwitchCamera: Boolean = true,
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
val callState: Async<CallState> = Uninitialized
) : MvRxState
sealed class VectorCallViewActions : VectorViewModelAction {
object EndCall : VectorCallViewActions()
object AcceptCall : VectorCallViewActions()
object DeclineCall : VectorCallViewActions()
object ToggleMute : VectorCallViewActions()
object ToggleVideo : VectorCallViewActions()
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : VectorCallViewActions()
}
sealed class VectorCallViewEvents : VectorViewEvents {
object DismissNoCall : VectorCallViewEvents()
data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents()
data class ShowSoundDeviceChooser(
val available: List<CallAudioManager.SoundDevice>,
val current: CallAudioManager.SoundDevice
) : VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : VectorCallViewEvents()
}
class VectorCallViewModel @AssistedInject constructor( class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState, @Assisted initialState: VectorCallViewState,
@Assisted val args: CallArgs, @Assisted val args: CallArgs,
@ -91,23 +45,23 @@ class VectorCallViewModel @AssistedInject constructor(
val proximityManager: CallProximityManager val proximityManager: CallProximityManager
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) { ) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
var call: MxCall? = null private var call: MxCall? = null
var connectionTimoutTimer: Timer? = null private var connectionTimeoutTimer: Timer? = null
var hasBeenConnectedOnce = false private var hasBeenConnectedOnce = false
private val callStateListener = object : MxCall.StateListener { private val callStateListener = object : MxCall.StateListener {
override fun onStateUpdate(call: MxCall) { override fun onStateUpdate(call: MxCall) {
val callState = call.state val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) { if (callState is CallState.Connected && callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
hasBeenConnectedOnce = true hasBeenConnectedOnce = true
connectionTimoutTimer?.cancel() connectionTimeoutTimer?.cancel()
connectionTimoutTimer = null connectionTimeoutTimer = null
} else { } else {
// do we reset as long as it's moving? // do we reset as long as it's moving?
connectionTimoutTimer?.cancel() connectionTimeoutTimer?.cancel()
if (hasBeenConnectedOnce) { if (hasBeenConnectedOnce) {
connectionTimoutTimer = Timer().apply { connectionTimeoutTimer = Timer().apply {
schedule(object : TimerTask() { schedule(object : TimerTask() {
override fun run() { override fun run() {
session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> { session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> {
@ -136,17 +90,17 @@ class VectorCallViewModel @AssistedInject constructor(
override fun onCurrentCallChange(call: MxCall?) { override fun onCurrentCallChange(call: MxCall?) {
} }
override fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) { override fun onCaptureStateChanged() {
setState { setState {
copy( copy(
isVideoCaptureInError = mgr.capturerIsInError, isVideoCaptureInError = webRtcPeerConnectionManager.capturerIsInError,
isHD = mgr.currentCaptureFormat() is CaptureFormat.HD isHD = webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD
) )
} }
} }
override fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) { override fun onAudioDevicesChange() {
val currentSoundDevice = mgr.audioManager.getCurrentSoundDevice() val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
proximityManager.start() proximityManager.start()
} else { } else {
@ -155,17 +109,17 @@ class VectorCallViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
availableSoundDevices = mgr.audioManager.getAvailableSoundDevices(), availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(),
soundDevice = currentSoundDevice soundDevice = currentSoundDevice
) )
} }
} }
override fun onCameraChange(mgr: WebRtcPeerConnectionManager) { override fun onCameraChange() {
setState { setState {
copy( copy(
canSwitchCamera = mgr.canSwitchCamera(), canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(),
isFrontCamera = mgr.currentCameraType() == CameraType.FRONT isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT
) )
} }
} }
@ -182,7 +136,7 @@ class VectorCallViewModel @AssistedInject constructor(
mxCall.addListener(callStateListener) mxCall.addListener(callStateListener)
val currentSoundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice() val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
proximityManager.start() proximityManager.start()
} }
@ -193,7 +147,7 @@ class VectorCallViewModel @AssistedInject constructor(
callState = Success(mxCall.state), callState = Success(mxCall.state),
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
soundDevice = currentSoundDevice, soundDevice = currentSoundDevice,
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices(), availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(),
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT, isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(), canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(),
isHD = mxCall.isVideoCall && webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD isHD = mxCall.isVideoCall && webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD
@ -250,10 +204,10 @@ class VectorCallViewModel @AssistedInject constructor(
Unit Unit
} }
is VectorCallViewActions.ChangeAudioDevice -> { is VectorCallViewActions.ChangeAudioDevice -> {
webRtcPeerConnectionManager.audioManager.setCurrentSoundDevice(action.device) webRtcPeerConnectionManager.callAudioManager.setCurrentSoundDevice(action.device)
setState { setState {
copy( copy(
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice() soundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
) )
} }
} }

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.util.MatrixItem
data class VectorCallViewState(
val callId: String? = null,
val roomId: String = "",
val isVideoCall: Boolean,
val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true,
val isVideoCaptureInError: Boolean = false,
val isHD: Boolean = false,
val isFrontCamera: Boolean = true,
val canSwitchCamera: Boolean = true,
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
val callState: Async<CallState> = Uninitialized
) : MvRxState

View File

@ -23,6 +23,9 @@ import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.services.BluetoothHeadsetReceiver import im.vector.app.core.services.BluetoothHeadsetReceiver
import im.vector.app.core.services.CallService import im.vector.app.core.services.CallService
import im.vector.app.core.services.WiredHeadsetStateReceiver import im.vector.app.core.services.WiredHeadsetStateReceiver
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -35,9 +38,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject
import org.webrtc.AudioSource import org.webrtc.AudioSource
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator import org.webrtc.Camera1Enumerator
@ -79,9 +79,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
interface CurrentCallListener { interface CurrentCallListener {
fun onCurrentCallChange(call: MxCall?) fun onCurrentCallChange(call: MxCall?)
fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {} fun onCaptureStateChanged() {}
fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {} fun onAudioDevicesChange() {}
fun onCameraChange(mgr: WebRtcPeerConnectionManager) {} fun onCameraChange() {}
} }
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList() private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
@ -93,9 +93,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCallsListeners.remove(listener) currentCallsListeners.remove(listener)
} }
val audioManager = CallAudioManager(context.applicationContext) { val callAudioManager = CallAudioManager(context.applicationContext) {
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryThis { it.onAudioDevicesChange(this) } tryThis { it.onAudioDevicesChange() }
} }
} }
@ -174,7 +174,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
set(value) { set(value) {
field = value field = value
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryThis { it.onCaptureStateChanged(this) } tryThis { it.onCaptureStateChanged() }
} }
} }
@ -503,7 +503,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
// render local video in pip view // render local video in pip view
localSurfaceRenderer.forEach { localSurfaceRenderer.forEach {
it.get()?.let { pipSurface -> it.get()?.let { pipSurface ->
pipSurface.setMirror(true) pipSurface.setMirror(this.cameraInUse?.type == CameraType.FRONT)
// no need to check if already added, addSink is checking that // no need to check if already added, addSink is checking that
currentCall?.localVideoTrack?.addSink(pipSurface) currentCall?.localVideoTrack?.addSink(pipSurface)
} }
@ -577,7 +577,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
fun close() { fun close() {
Timber.v("## VOIP WebRtcPeerConnectionManager close() >") Timber.v("## VOIP WebRtcPeerConnectionManager close() >")
CallService.onNoActiveCall(context) CallService.onNoActiveCall(context)
audioManager.stop() callAudioManager.stop()
val callToEnd = currentCall val callToEnd = currentCall
currentCall = null currentCall = null
// This must be done in this thread // This must be done in this thread
@ -631,7 +631,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
val callContext = CallContext(createdCall) val callContext = CallContext(createdCall)
audioManager.startForCall(createdCall) callAudioManager.startForCall(createdCall)
currentCall = callContext currentCall = callContext
val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName() val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
@ -684,7 +684,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
val callContext = CallContext(mxCall) val callContext = CallContext(mxCall)
currentCall = callContext currentCall = callContext
audioManager.startForCall(mxCall) callAudioManager.startForCall(mxCall)
executor.execute { executor.execute {
callContext.remoteCandidateSource = ReplaySubject.create() callContext.remoteCandidateSource = ReplaySubject.create()
} }
@ -740,8 +740,12 @@ class WebRtcPeerConnectionManager @Inject constructor(
override fun onCameraSwitchDone(isFrontCamera: Boolean) { override fun onCameraSwitchDone(isFrontCamera: Boolean) {
Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera") Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK } cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK }
localSurfaceRenderer.forEach {
it.get()?.setMirror(isFrontCamera)
}
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryThis { it.onCameraChange(this@WebRtcPeerConnectionManager) } tryThis { it.onCameraChange() }
} }
} }
@ -767,7 +771,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
// videoCapturer?.stopCapture() // videoCapturer?.stopCapture()
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
currentCaptureMode = format currentCaptureMode = format
currentCallsListeners.forEach { tryThis { it.onCaptureStateChanged(this) } } currentCallsListeners.forEach { tryThis { it.onCaptureStateChanged() } }
} }
} }
@ -798,12 +802,12 @@ class WebRtcPeerConnectionManager @Inject constructor(
Timber.v("## VOIP onWiredDeviceEvent $event") Timber.v("## VOIP onWiredDeviceEvent $event")
currentCall ?: return currentCall ?: return
// sometimes we received un-wanted unplugged... // sometimes we received un-wanted unplugged...
audioManager.wiredStateChange(event) callAudioManager.wiredStateChange(event)
} }
fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("## VOIP onWirelessDeviceEvent $event") Timber.v("## VOIP onWirelessDeviceEvent $event")
audioManager.bluetoothStateChange(event.plugged) callAudioManager.bluetoothStateChange(event.plugged)
} }
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
@ -858,6 +862,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
*/ */
PeerConnection.PeerConnectionState.CONNECTED -> { PeerConnection.PeerConnectionState.CONNECTED -> {
callContext.mxCall.state = CallState.Connected(newState) callContext.mxCall.state = CallState.Connected(newState)
callAudioManager.onCallConnected(callContext.mxCall)
} }
/** /**
* One or more of the ICE transports on the connection is in the "failed" state. * One or more of the ICE transports on the connection is in the "failed" state.

View File

@ -16,6 +16,7 @@
package im.vector.app.features.createdirect package im.vector.app.features.createdirect
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
@ -23,13 +24,19 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.userdirectory.PendingInvitee import im.vector.app.features.userdirectory.PendingInvitee
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState, initialState: CreateDirectRoomViewState,
private val rawService: RawService,
private val session: Session) private val session: Session)
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) { : VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@ -54,22 +61,28 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
} }
private fun createRoomAndInviteSelectedUsers(invitees: Set<PendingInvitee>) { private fun createRoomAndInviteSelectedUsers(invitees: Set<PendingInvitee>) {
val roomParams = CreateRoomParams() viewModelScope.launch(Dispatchers.IO) {
.apply { val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
invitees.forEach { ?.isE2EByDefault()
when (it) { ?: true
is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId)
is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
}.exhaustive
}
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = session.getHomeServerCapabilities().adminE2EByDefault
}
session.rx() val roomParams = CreateRoomParams()
.createRoom(roomParams) .apply {
.execute { invitees.forEach {
copy(createAndInviteState = it) when (it) {
} is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId)
is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
}.exhaustive
}
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
session.rx()
.createRoom(roomParams)
.execute {
copy(createAndInviteState = it)
}
}
} }
} }

View File

@ -87,13 +87,13 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
viewModel.navigateEvent.observeEvent(this) { uxStateEvent -> viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
when (uxStateEvent) { when (uxStateEvent) {
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_RECOVER_WITH_KEY -> { KeysBackupRestoreSharedViewModel.NAVIGATE_TO_RECOVER_WITH_KEY -> {
addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java) addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java, allowStateLoss = true)
} }
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> { KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
viewModel.keyVersionResult.value?.version?.let { viewModel.keyVersionResult.value?.version?.let {
KeysBackupBanner.onRecoverDoneForVersion(this, it) KeysBackupBanner.onRecoverDoneForVersion(this, it)
} }
replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java) replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java, allowStateLoss = true)
} }
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_4S -> { KeysBackupRestoreSharedViewModel.NAVIGATE_TO_4S -> {
launch4SActivity() launch4SActivity()

View File

@ -21,6 +21,8 @@ package im.vector.app.features.crypto.keysrequest
import android.content.Context import android.content.Context
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.DefaultVectorAlert
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
@ -38,10 +40,6 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import timber.log.Timber import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -54,8 +52,11 @@ import javax.inject.Singleton
*/ */
@Singleton @Singleton
class KeyRequestHandler @Inject constructor(private val context: Context, private val popupAlertManager: PopupAlertManager) class KeyRequestHandler @Inject constructor(
: GossipingRequestListener, private val context: Context,
private val popupAlertManager: PopupAlertManager,
private val dateFormatter: VectorDateFormatter
) : GossipingRequestListener,
VerificationService.Listener { VerificationService.Listener {
private val alertsToRequests = HashMap<String, ArrayList<IncomingRoomKeyRequest>>() private val alertsToRequests = HashMap<String, ArrayList<IncomingRoomKeyRequest>>()
@ -156,16 +157,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
moreInfo.lastSeenIp moreInfo.lastSeenIp
} }
val lastSeenTime = moreInfo.lastSeenTs?.let { ts -> val lastSeenTime = dateFormatter.format(moreInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
val lastSeenInfo = context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) val lastSeenInfo = context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
dialogText = if (wasNewDevice) { dialogText = if (wasNewDevice) {
context.getString(R.string.you_added_a_new_device_with_info, deviceName, lastSeenInfo) context.getString(R.string.you_added_a_new_device_with_info, deviceName, lastSeenInfo)

View File

@ -20,8 +20,8 @@ import android.app.Activity
import android.content.Context import android.content.Context
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.features.settings.VectorSettingsUrls import im.vector.app.features.settings.VectorSettingsUrls
@ -33,7 +33,7 @@ private const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE"
fun showDisclaimerDialog(activity: Activity) { fun showDisclaimerDialog(activity: Activity) {
// This is a RiotX/Element disclaimer // This is a RiotX/Element disclaimer
if (true) return if (true) return
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(activity) val sharedPrefs = DefaultSharedPreferences.getInstance(activity)
if (sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE) { if (sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE) {
sharedPrefs.edit { sharedPrefs.edit {
@ -54,7 +54,7 @@ fun showDisclaimerDialog(activity: Activity) {
} }
fun doNotShowDisclaimerDialog(context: Context) { fun doNotShowDisclaimerDialog(context: Context) {
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) val sharedPrefs = DefaultSharedPreferences.getInstance(context)
sharedPrefs.edit { sharedPrefs.edit {
putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE)

View File

@ -122,7 +122,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START) is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> { is HomeActivitySharedAction.OpenGroup -> {
drawerLayout.closeDrawer(GravityCompat.START) drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
} }
}.exhaustive }.exhaustive
} }

View File

@ -31,7 +31,6 @@ import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.core.utils.subscribeLogError import im.vector.app.core.utils.subscribeLogError
import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.call.WebRtcPeerConnectionManager
import im.vector.app.features.command.CommandParser import im.vector.app.features.command.CommandParser
@ -40,9 +39,10 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import io.reactivex.Observable import io.reactivex.Observable
@ -59,11 +59,12 @@ import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -86,7 +87,6 @@ import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
@ -105,39 +105,24 @@ import java.util.concurrent.atomic.AtomicBoolean
class RoomDetailViewModel @AssistedInject constructor( class RoomDetailViewModel @AssistedInject constructor(
@Assisted private val initialState: RoomDetailViewState, @Assisted private val initialState: RoomDetailViewState,
userPreferencesProvider: UserPreferencesProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val rainbowGenerator: RainbowGenerator, private val rainbowGenerator: RainbowGenerator,
private val session: Session, private val session: Session,
private val rawService: RawService,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stickerPickerActionHandler: StickerPickerActionHandler, private val stickerPickerActionHandler: StickerPickerActionHandler,
private val roomSummaryHolder: RoomSummaryHolder, private val roomSummaryHolder: RoomSummaryHolder,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener { ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>() private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>() private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { private val timelineSettings = timelineSettingsFactory.create()
TimelineSettings(30,
filterEdits = false,
filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(),
filterUseless = false,
filterTypes = false,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
} else {
TimelineSettings(30,
filterEdits = true,
filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(),
filterUseless = true,
filterTypes = true,
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
}
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>() private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
val timeline = room.createTimeline(eventId, timelineSettings) val timeline = room.createTimeline(eventId, timelineSettings)
@ -349,7 +334,12 @@ class RoomDetailViewModel @AssistedInject constructor(
val roomId: String = room.roomId val roomId: String = room.roomId
val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale) val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale)
val jitsiDomain = session.getHomeServerCapabilities().preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) val preferredJitsiDomain = tryThis {
rawService.getElementWellknown(session.myUserId)
?.jitsiServer
?.preferredDomain
}
val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)
// We use the default element wrapper for this widget // We use the default element wrapper for this widget
// https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md // https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md
@ -898,13 +888,15 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {
if (action.event.root.sendState.isSent()) { // ignore pending/local events viewModelScope.launch(Dispatchers.Default) {
visibleEventsObservable.accept(action) if (action.event.root.sendState.isSent()) { // ignore pending/local events
} visibleEventsObservable.accept(action)
// We need to update this with the related m.replace also (to move read receipt) }
action.event.annotations?.editSummary?.sourceEvents?.forEach { // We need to update this with the related m.replace also (to move read receipt)
room.getTimeLineEvent(it)?.let { event -> action.event.annotations?.editSummary?.sourceEvents?.forEach {
visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event)) room.getTimeLineEvent(it)?.let { event ->
visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event))
}
} }
} }
} }

View File

@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.readreceipts package im.vector.app.features.home.room.detail.readreceipts
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
@ -36,7 +37,7 @@ class DisplayReadReceiptsController @Inject constructor(private val dateFormatte
override fun buildModels(readReceipts: List<ReadReceiptData>) { override fun buildModels(readReceipts: List<ReadReceiptData>) {
readReceipts.forEach { readReceipts.forEach {
val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp) val timestamp = dateFormatter.format(it.timestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
DisplayReadReceiptItem_() DisplayReadReceiptItem_()
.id(it.userId) .id(it.userId)
.matrixItem(it.toMatrixItem()) .matrixItem(it.toMatrixItem())

View File

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
@ -53,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoCon
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.threeten.bp.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
@ -333,13 +333,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
) { ) {
requestModelBuild() requestModelBuild()
} }
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
} }
private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
return if (addDaySeparator) { return if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date) val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
} else { } else {
null null

View File

@ -36,7 +36,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
EventSharedAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) EventSharedAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
data class Copy(val content: String) : data class Copy(val content: String) :
EventSharedAction(R.string.copy, R.drawable.ic_copy) EventSharedAction(R.string.action_copy, R.drawable.ic_copy)
data class Edit(val eventId: String) : data class Edit(val eventId: String) :
EventSharedAction(R.string.edit, R.drawable.ic_edit) EventSharedAction(R.string.edit, R.drawable.ic_edit)

View File

@ -22,9 +22,6 @@ import com.airbnb.mvrx.Uninitialized
import im.vector.app.core.extensions.canReact import im.vector.app.core.extensions.canReact
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/** /**
* Quick reactions state * Quick reactions state
@ -56,11 +53,7 @@ data class MessageActionState(
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
fun senderName(): String = informationData.memberName?.toString() ?: "" fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
} }

View File

@ -20,6 +20,8 @@ import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.app.EmojiCompatFontProvider import im.vector.app.EmojiCompatFontProvider
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem import im.vector.app.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem
@ -40,13 +42,16 @@ import javax.inject.Inject
class MessageActionsEpoxyController @Inject constructor( class MessageActionsEpoxyController @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val fontProvider: EmojiCompatFontProvider private val fontProvider: EmojiCompatFontProvider,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<MessageActionState>() { ) : TypedEpoxyController<MessageActionState>() {
var listener: MessageActionsEpoxyControllerListener? = null var listener: MessageActionsEpoxyControllerListener? = null
override fun buildModels(state: MessageActionState) { override fun buildModels(state: MessageActionState) {
// Message preview // Message preview
val date = state.timelineEvent()?.root?.originServerTs
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
bottomSheetMessagePreviewItem { bottomSheetMessagePreviewItem {
id("preview") id("preview")
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
@ -54,7 +59,7 @@ class MessageActionsEpoxyController @Inject constructor(
movementMethod(createLinkMovementMethod(listener)) movementMethod(createLinkMovementMethod(listener))
userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) } userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
body(state.messageBody.linkify(listener)) body(state.messageBody.linkify(listener))
time(state.time()) time(formattedDate)
} }
// Send state // Send state

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