diff --git a/CHANGES.md b/CHANGES.md index fee60cfab7..eada6082b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,7 +20,7 @@ Build 🧱: - Other changes: - - + - Performance: share Realm instance used on UI thread and improve SharedPreferences reading time. Changes in Element 1.0.6 (2020-09-08) =================================================== diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmInstanceWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmInstanceWrapper.kt new file mode 100644 index 0000000000..e2ddbcbca8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmInstanceWrapper.kt @@ -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 withRealm(block: (Realm) -> R): R { + return use { + block(it.realm) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt new file mode 100644 index 0000000000..a7f934ffc0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt @@ -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() + + /** + * Allow you to execute a block with an opened realm. It automatically closes it if necessary (ie. when not in main thread) + */ + fun 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() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt index 188ca4937c..6b9c0e7a45 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -18,26 +18,24 @@ package org.matrix.android.sdk.internal.database.mapper 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.UserEntity 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 -internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { +internal class ReadReceiptsSummaryMapper @Inject constructor(private val realmSessionProvider: RealmSessionProvider) { fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List { if (readReceiptsSummaryEntity == null) { return emptyList() } - return Realm.getInstance(realmConfiguration).use { realm -> + return realmSessionProvider.withRealm { realm -> val readReceipts = readReceiptsSummaryEntity.readReceipts readReceipts .mapNotNull { val user = UserEntity.where(realm, it.userId).findFirst() - ?: return@mapNotNull null + ?: return@mapNotNull null ReadReceipt(user.asDomain(), it.originServerTs.toLong()) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index d404cecc51..5397b8d9bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -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.database.DatabaseCleaner 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.di.Authenticated import org.matrix.android.sdk.internal.di.DeviceId @@ -325,23 +326,27 @@ internal abstract class SessionModule { @Binds @IntoSet - abstract fun bindIntegrationManager(observer: IntegrationManager): SessionLifecycleObserver + abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver @Binds @IntoSet - abstract fun bindWidgetUrlFormatter(observer: DefaultWidgetURLFormatter): SessionLifecycleObserver + abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver @Binds @IntoSet - abstract fun bindShieldTrustUpdated(observer: ShieldTrustUpdater): SessionLifecycleObserver + abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver @Binds @IntoSet - abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver + abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver @Binds @IntoSet - abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver + abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver + + @Binds + @IntoSet + abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver @Binds abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt index 38dcad2311..985cf80e97 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt @@ -17,17 +17,16 @@ 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.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.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields 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.room.membership.RoomMemberHelper -import io.realm.Realm import javax.inject.Inject internal interface RoomGetter { @@ -38,18 +37,18 @@ internal interface RoomGetter { @SessionScope internal class DefaultRoomGetter @Inject constructor( - @SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, private val roomFactory: RoomFactory ) : RoomGetter { override fun getRoom(roomId: String): Room? { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + return realmSessionProvider.withRealm { realm -> createRoom(realm, roomId) } } override fun getDirectRoomWith(otherUserId: String): Room? { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + return realmSessionProvider.withRealm { realm -> RoomSummaryEntity.where(realm) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index b3188883c0..00c624a20d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.send 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.Event 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.timeline.TimelineEvent 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.mapper.ContentMapper 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.timeline.DefaultTimeline import org.matrix.android.sdk.internal.util.awaitTransaction -import io.realm.Realm -import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, private val roomSummaryUpdater: RoomSummaryUpdater, private val eventBus: EventBus, private val timelineEventMapper: TimelineEventMapper) { @@ -59,7 +60,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private if (event.eventId == null) { 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 roomMemberHelper = RoomMemberHelper(realm, roomId) val myUser = roomMemberHelper.getLastRoomMember(senderId) @@ -150,7 +151,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } fun getAllEventsWithStates(roomId: String, states : List): List { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + return realmSessionProvider.withRealm { realm -> TimelineEventEntity .findAllInRoomWithSendStates(realm, roomId, states) .sortedByDescending { it.displayIndex } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt index e8dc2ddf40..65d30868d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt @@ -20,24 +20,26 @@ package org.matrix.android.sdk.internal.session.room.state import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations 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.session.events.model.Event import org.matrix.android.sdk.api.util.Optional 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.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase 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 -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? { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + return realmSessionProvider.withRealm { realm -> 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, stateKey: QueryStringValue): List { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + return realmSessionProvider.withRealm { realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) .findAll() .mapNotNull { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 421cd1b063..52651af881 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -39,6 +39,7 @@ 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.TimelineSettings 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.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields @@ -76,7 +77,8 @@ internal class DefaultTimeline( private val settings: TimelineSettings, private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val eventBus: EventBus, - private val eventDecryptor: TimelineEventDecryptor + private val eventDecryptor: TimelineEventDecryptor, + private val realmSessionProvider: RealmSessionProvider ) : Timeline, TimelineHiddenReadReceipts.Delegate { data class OnNewTimelineEvents(val roomId: String, val eventIds: List) @@ -136,13 +138,13 @@ internal class DefaultTimeline( } override fun pendingEventCount(): Int { - return Realm.getInstance(realmConfiguration).use { + return realmSessionProvider.withRealm { RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 } } override fun failedToDeliverEventCount(): Int { - return Realm.getInstance(realmConfiguration).use { + return realmSessionProvider.withRealm { TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() } } @@ -239,7 +241,7 @@ internal class DefaultTimeline( return eventId } // 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) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .findAll() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index db675f69f5..c60a944409 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -22,6 +22,9 @@ import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject 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.isVideoMessage 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.util.Optional 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.TimelineEventMapper 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.di.SessionDatabase 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, @SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, private val eventBus: EventBus, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, @@ -73,17 +73,17 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), eventBus = eventBus, eventDecryptor = eventDecryptor, - fetchTokenAndPaginateTask = fetchTokenAndPaginateTask + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + realmSessionProvider = realmSessionProvider ) } override fun getTimeLineEvent(eventId: String): TimelineEvent? { - return monarchy - .fetchCopyMap({ - TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() - }, { entity, _ -> - timelineEventMapper.map(entity) - }) + return realmSessionProvider.withRealm { realm -> + TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let { + timelineEventMapper.map(it) + } + } } override fun getTimeLineEventLive(eventId: String): LiveData> { @@ -98,7 +98,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv override fun getAttachmentMessages(): List { // TODO pretty bad query.. maybe we should denormalize clear type in base? - return doWithRealm(monarchy.realmConfiguration) { realm -> + return realmSessionProvider.withRealm { realm -> realm.where() .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt index dd3c6856c0..f6cb86c0ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt @@ -23,9 +23,11 @@ import androidx.paging.DataSource import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList 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.util.Optional 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.model.IgnoredUserEntity 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.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.util.fetchCopied -import io.realm.Case 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 by lazy { monarchy.createDataSourceFactory { realm -> @@ -58,10 +59,10 @@ internal class UserDataSource @Inject constructor(@SessionDatabase private val m } fun getUser(userId: String): User? { - val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null - - return userEntity.asDomain() + return realmSessionProvider.withRealm { + val userEntity = UserEntity.where(it, userId).findFirst() + userEntity?.asDomain() + } } fun getUserLive(userId: String): LiveData> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt index d54bfdd63d..a9261eddab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt @@ -20,18 +20,20 @@ package org.matrix.android.sdk.internal.session.user.accountdata import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations 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.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.model.UserAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields 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 internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, private val accountDataMapper: AccountDataMapper) { fun getAccountDataEvent(type: String): UserAccountDataEvent? { @@ -45,10 +47,9 @@ internal class AccountDataDataSource @Inject constructor(@SessionDatabase privat } fun getAccountDataEvents(types: Set): List { - return monarchy.fetchAllMappedSync( - { accountDataEventsQuery(it, types) }, - accountDataMapper::map - ) + return realmSessionProvider.withRealm { + accountDataEventsQuery(it, types).findAll().map(accountDataMapper::map) + } } fun getLiveAccountDataEvents(types: Set): LiveData> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt index 2cbc9b23dc..992dbf16df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -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.WidgetContent 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.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.user.UserDataSource -import io.realm.Realm -import io.realm.RealmConfiguration import java.net.URLEncoder import javax.inject.Inject -internal class WidgetFactory @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, - private val userDataSource: UserDataSource, +internal class WidgetFactory @Inject constructor(private val userDataSource: UserDataSource, + private val realmSessionProvider: RealmSessionProvider, @UserId private val userId: String) { 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) { null } else { - Realm.getInstance(realmConfiguration).use { + realmSessionProvider.withRealm { val roomMemberHelper = RoomMemberHelper(it, widgetEvent.roomId) val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(widgetEvent.senderId) SenderInfo( diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 0a58175731..ebe877b301 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -172,3 +172,6 @@ import org.matrix.androidsdk.crypto.data===2 ### Use `Context#getSystemService` extension function provided by `core-ktx` getSystemService\(Context + +### Use DefaultSharedPreferences.getInstance() instead for better performance +PreferenceManager\.getDefaultSharedPreferences==2 diff --git a/vector/build.gradle b/vector/build.gradle index 6b251d5329..e0f401aaf6 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -190,6 +190,8 @@ android { resValue "bool", "debug_mode", "true" 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 } @@ -199,6 +201,7 @@ android { resValue "bool", "debug_mode", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" + buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" postprocessing { removeUnusedCode true diff --git a/vector/src/debug/java/im/vector/app/receivers/DebugReceiver.kt b/vector/src/debug/java/im/vector/app/receivers/DebugReceiver.kt index 7b64bed08e..0cea9e3d8e 100644 --- a/vector/src/debug/java/im/vector/app/receivers/DebugReceiver.kt +++ b/vector/src/debug/java/im/vector/app/receivers/DebugReceiver.kt @@ -22,7 +22,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.SharedPreferences import androidx.core.content.edit -import androidx.preference.PreferenceManager +import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.utils.lsFiles import timber.log.Timber @@ -44,7 +44,7 @@ class DebugReceiver : BroadcastReceiver() { } private fun dumpPreferences(context: Context) { - logPrefs("DefaultSharedPreferences", PreferenceManager.getDefaultSharedPreferences(context)) + logPrefs("DefaultSharedPreferences", DefaultSharedPreferences.getInstance(context)) } private fun logPrefs(name: String, sharedPreferences: SharedPreferences?) { @@ -58,7 +58,7 @@ class DebugReceiver : BroadcastReceiver() { } 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") } } diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 6c68d6c3b5..84ca392c45 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -19,7 +19,6 @@ package im.vector.app.push.fcm import android.app.Activity import android.content.Context -import androidx.preference.PreferenceManager import android.widget.Toast import androidx.core.content.edit 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 im.vector.app.R 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.features.settings.VectorPreferences import timber.log.Timber @@ -46,7 +46,7 @@ object FcmHelper { * @return the FCM token or null if not received from FCM */ 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, token: String?) { - PreferenceManager.getDefaultSharedPreferences(context).edit { + DefaultSharedPreferences.getInstance(context).edit { putString(PREFS_KEY_FCM_TOKEN, token) } } diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index f64ebf9245..c9d2c96223 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.res.Configuration import android.os.Handler import android.os.HandlerThread +import android.os.StrictMode import androidx.core.provider.FontRequest import androidx.core.provider.FontsContractCompat import androidx.lifecycle.Lifecycle @@ -92,6 +93,7 @@ class VectorApplication : private var fontThreadHandler: Handler? = null override fun onCreate() { + enableStrictModeIfNeeded() super.onCreate() appContext = this vectorComponent = DaggerVectorComponent.factory().create(this) @@ -163,6 +165,15 @@ class VectorApplication : // 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 getWorkManagerConfiguration(): WorkConfiguration { diff --git a/vector/src/main/java/im/vector/app/core/di/DefaultSharedPreferences.kt b/vector/src/main/java/im/vector/app/core/di/DefaultSharedPreferences.kt new file mode 100644 index 0000000000..abee0cb2e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/DefaultSharedPreferences.kt @@ -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 } + } +} diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index 2a17c2ca1b..0eba78dbf8 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -100,7 +100,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { Timber.v("Load data: $data") - if (data.isLocalFile() && data.url != null) { + if (data.isLocalFile && data.url != null) { val initialFile = File(data.url) callback.onDataReady(initialFile.inputStream()) return diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt index 14d9261204..f4890780b5 100755 --- a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt @@ -23,11 +23,11 @@ import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.edit import androidx.core.view.isVisible -import androidx.preference.PreferenceManager import butterknife.BindView import butterknife.ButterKnife import butterknife.OnClick import im.vector.app.R +import im.vector.app.core.di.DefaultSharedPreferences import timber.log.Timber /** @@ -57,7 +57,7 @@ class KeysBackupBanner @JvmOverloads constructor( init { setupView() - PreferenceManager.getDefaultSharedPreferences(context).edit { + DefaultSharedPreferences.getInstance(context).edit { putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") } @@ -105,17 +105,17 @@ class KeysBackupBanner @JvmOverloads constructor( state.let { when (it) { is State.Setup -> { - PreferenceManager.getDefaultSharedPreferences(context).edit { + DefaultSharedPreferences.getInstance(context).edit { putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true) } } is State.Recover -> { - PreferenceManager.getDefaultSharedPreferences(context).edit { + DefaultSharedPreferences.getInstance(context).edit { putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version) } } is State.Update -> { - PreferenceManager.getDefaultSharedPreferences(context).edit { + DefaultSharedPreferences.getInstance(context).edit { putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version) } } @@ -150,7 +150,7 @@ class KeysBackupBanner @JvmOverloads constructor( private fun renderSetup(nbOfKeys: Int) { 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 isVisible = false } else { @@ -164,7 +164,7 @@ class KeysBackupBanner @JvmOverloads constructor( } 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 } else { isVisible = true @@ -177,7 +177,7 @@ class KeysBackupBanner @JvmOverloads constructor( } 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 } else { 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 */ fun onRecoverDoneForVersion(context: Context, version: String) { - PreferenceManager.getDefaultSharedPreferences(context).edit { + DefaultSharedPreferences.getInstance(context).edit { putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version) } } diff --git a/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt b/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt index 82b8c3a51a..9b84ea7b2f 100644 --- a/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/RingtoneUtils.kt @@ -21,7 +21,7 @@ import android.media.Ringtone import android.media.RingtoneManager import android.net.Uri import androidx.core.content.edit -import androidx.preference.PreferenceManager +import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.features.settings.VectorPreferences /** @@ -40,7 +40,7 @@ import im.vector.app.features.settings.VectorPreferences * @see Ringtone */ 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) callRingtone?.let { @@ -94,7 +94,7 @@ fun getCallRingtoneName(context: Context): String? { * @see Ringtone */ fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) { - PreferenceManager.getDefaultSharedPreferences(context) + DefaultSharedPreferences.getInstance(context) .edit { 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 */ 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 */ fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) { - PreferenceManager.getDefaultSharedPreferences(context) + DefaultSharedPreferences.getInstance(context) .edit { putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault) } diff --git a/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt b/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt index b9568e0b62..c2cd2e11e3 100644 --- a/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt +++ b/vector/src/main/java/im/vector/app/features/disclaimer/DisclaimerDialog.kt @@ -20,8 +20,8 @@ import android.app.Activity import android.content.Context import androidx.appcompat.app.AlertDialog import androidx.core.content.edit -import androidx.preference.PreferenceManager import im.vector.app.R +import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.settings.VectorSettingsUrls @@ -31,7 +31,7 @@ private const val CURRENT_DISCLAIMER_VALUE = 2 private const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE" fun showDisclaimerDialog(activity: Activity) { - val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(activity) + val sharedPrefs = DefaultSharedPreferences.getInstance(activity) if (sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE) { sharedPrefs.edit { @@ -52,7 +52,7 @@ fun showDisclaimerDialog(activity: Activity) { } fun doNotShowDisclaimerDialog(context: Context) { - val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) + val sharedPrefs = DefaultSharedPreferences.getInstance(context) sharedPrefs.edit { putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index eab0007080..a77d50d767 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -896,13 +896,15 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { - 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 { - room.getTimeLineEvent(it)?.let { event -> - visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event)) + viewModelScope.launch(Dispatchers.Default) { + 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 { + room.getTimeLineEvent(it)?.let { event -> + visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event)) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 9ada087207..ffc51f671f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -50,7 +50,7 @@ abstract class MessageImageVideoItem : AbsMessageItem(null) + private val mColorByAttr = HashMap() // init the theme @@ -68,8 +71,15 @@ object ThemeUtils { * @return the selected application theme */ fun getApplicationTheme(context: Context): String { - return PreferenceManager.getDefaultSharedPreferences(context) - .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE + val currentTheme = this.currentTheme.get() + return if (currentTheme == null) { + val themeFromPref = DefaultSharedPreferences.getInstance(context) + .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE + this.currentTheme.set(themeFromPref) + themeFromPref + } else { + currentTheme + } } /** @@ -78,6 +88,7 @@ object ThemeUtils { * @param aTheme the new theme */ fun setApplicationTheme(context: Context, aTheme: String) { + currentTheme.set(aTheme) when (aTheme) { THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black)