diff --git a/CHANGES.md b/CHANGES.md index 207d04d6d0..b56cbef37a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,37 @@ -Changes in RiotX 0.23.0 (2020-XX-XX) +Changes in Riot.imX 0.91.4 (2020-XX-XX) =================================================== +Features ✨: + - + +Improvements 🙌: + - + +Bugfix 🐛: + - Fix crash when coming from a notification (#1601) + - Fix Exception when importing keys (#1576) + +Translations 🗣: + - + +SDK API changes ⚠️: + - + +Build 🧱: + - + +Other changes: + - + +Changes in Riot.imX 0.91.3 (2020-07-01) +=================================================== + +Notes: + - This version is the third beta version of RiotX codebase published as Riot-Android on the PlayStore. + - Changelog below includes changes of v0.91.0, v0.91.1, and v0.91.2, because the first beta versions have been tagged and + published from the branch feature/migration_from_legacy. + - This version uses temporary name `Riot.imX`, to distinguish the app with RiotX app. + Features ✨: - Call with WebRTC support (##611) - Add capability to change the display name (#1529) @@ -13,6 +44,8 @@ Improvements 🙌: - Update user avatar (#1054) - Allow self-signed certificate (#1564) - Improve file download and open in timeline + - Catchup tab is removed temporarily (#1565) + - Render room avatar change (#1319) Bugfix 🐛: - Fix dark theme issue on login screen (#1097) @@ -21,12 +54,7 @@ Bugfix 🐛: - Use vendor prefix for non merged MSC (#1537) - Compress images before sending (#1333) - Searching by displayname is case sensitive (#1468) - -Translations 🗣: - - - -SDK API changes ⚠️: - - + - Fix layout overlap issue (#1407) Build 🧱: - Enable code optimization (Proguard) diff --git a/matrix-sdk-android/proguard-rules.pro b/matrix-sdk-android/proguard-rules.pro index 08a20cbf0a..fa860d8049 100644 --- a/matrix-sdk-android/proguard-rules.pro +++ b/matrix-sdk-android/proguard-rules.pro @@ -64,3 +64,19 @@ ### Webrtc -keep class org.webrtc.** { *; } + +### Serializable persisted classes +# https://www.guardsquare.com/en/products/proguard/manual/examples#serializable +-keepnames class * implements java.io.Serializable + +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index b4f9afdbd4..5b0f24aed7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -127,6 +127,12 @@ interface Session : */ fun getSyncStateLive(): LiveData + /** + * This method returns the current sync state. + * @return the current [SyncState]. + */ + fun getSyncState(): SyncState + /** * This methods return true if an initial sync has been processed */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index eb2fc9ebad..7bf8e2478f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -176,13 +176,14 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } } - // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper2 + // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper realm.schema.get("OlmInboundGroupSessionEntity") ?.transform { obj -> try { val oldSerializedData = obj.getString("olmInboundGroupSessionData") deserializeFromRealm(oldSerializedData)?.let { mxOlmInboundGroupSession2 -> - val newOlmInboundGroupSessionWrapper2 = OlmInboundGroupSessionWrapper2() + val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier() + val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false) .apply { olmInboundGroupSession = mxOlmInboundGroupSession2.mSession roomId = mxOlmInboundGroupSession2.mRoomId @@ -191,7 +192,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain } - obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper2)) + obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper)) } } catch (e: Exception) { Timber.e(e, "Error") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt index 2346ff8877..e375c3d364 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt @@ -175,7 +175,7 @@ internal object CertUtil { } } - val trustPinned = arrayOf(PinnedTrustManager(hsConfig.allowedFingerprints, defaultTrustManager)) + val trustPinned = arrayOf(PinnedTrustManagerProvider.provide(hsConfig.allowedFingerprints, defaultTrustManager)) val sslSocketFactory: SSLSocketFactory @@ -239,12 +239,12 @@ internal object CertUtil { fun newConnectionSpecs(hsConfig: HomeServerConnectionConfig): List { val builder = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) val tlsVersions = hsConfig.tlsVersions - if (null != tlsVersions) { + if (null != tlsVersions && tlsVersions.isNotEmpty()) { builder.tlsVersions(*tlsVersions.toTypedArray()) } val tlsCipherSuites = hsConfig.tlsCipherSuites - if (null != tlsCipherSuites) { + if (null != tlsCipherSuites && tlsCipherSuites.isNotEmpty()) { builder.cipherSuites(*tlsCipherSuites.toTypedArray()) } @@ -252,7 +252,8 @@ internal object CertUtil { builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions) val list = ArrayList() list.add(builder.build()) - if (hsConfig.allowHttpExtension) { + // TODO: we should display a warning if user enter an http url + if (hsConfig.allowHttpExtension || hsConfig.homeServerUri.toString().startsWith("http://")) { list.add(ConnectionSpec.CLEARTEXT) } return list diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt index de41d168fd..615358310f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt @@ -27,26 +27,23 @@ import javax.net.ssl.X509TrustManager */ /** - * @param fingerprints An array of SHA256 cert fingerprints + * @param fingerprints Not empty array of SHA256 cert fingerprints * @param defaultTrustManager Optional trust manager to fall back on if cert does not match * any of the fingerprints. Can be null. */ -internal class PinnedTrustManager(private val fingerprints: List?, +internal class PinnedTrustManager(private val fingerprints: List, private val defaultTrustManager: X509TrustManager?) : X509TrustManager { - // Set to false to perform some test - private val USE_DEFAULT_TRUST_MANAGER = true - @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array, s: String) { try { - if (defaultTrustManager != null && USE_DEFAULT_TRUST_MANAGER) { + if (defaultTrustManager != null) { defaultTrustManager.checkClientTrusted(chain, s) return } } catch (e: CertificateException) { // If there is an exception we fall back to checking fingerprints - if (fingerprints.isNullOrEmpty()) { + if (fingerprints.isEmpty()) { throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) } } @@ -57,13 +54,13 @@ internal class PinnedTrustManager(private val fingerprints: List?, @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array, s: String) { try { - if (defaultTrustManager != null && USE_DEFAULT_TRUST_MANAGER) { + if (defaultTrustManager != null) { defaultTrustManager.checkServerTrusted(chain, s) return } } catch (e: CertificateException) { // If there is an exception we fall back to checking fingerprints - if (fingerprints == null || fingerprints.isEmpty()) { + if (fingerprints.isEmpty()) { throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) } } @@ -75,22 +72,12 @@ internal class PinnedTrustManager(private val fingerprints: List?, private fun checkTrusted(chain: Array) { val cert = chain[0] - var found = false - if (fingerprints != null) { - for (allowedFingerprint in fingerprints) { - if (allowedFingerprint.matchesCert(cert)) { - found = true - break - } - } - } - - if (!found) { + if (!fingerprints.any { it.matchesCert(cert) }) { throw UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null) } } override fun getAcceptedIssuers(): Array { - return emptyArray() + return defaultTrustManager?.acceptedIssuers ?: emptyArray() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManagerApi24.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManagerApi24.kt new file mode 100644 index 0000000000..98257caefc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManagerApi24.kt @@ -0,0 +1,155 @@ +/* + * 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.matrix.android.internal.network.ssl + +import android.os.Build +import androidx.annotation.RequiresApi +import java.net.Socket +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedTrustManager + +/** + * Implements a TrustManager that checks Certificates against an explicit list of known + * fingerprints. + */ + +/** + * @param fingerprints An array of SHA256 cert fingerprints + * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * any of the fingerprints. Can be null. + */ +@RequiresApi(Build.VERSION_CODES.N) +internal class PinnedTrustManagerApi24(private val fingerprints: List, + private val defaultTrustManager: X509ExtendedTrustManager?) : X509ExtendedTrustManager() { + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String, engine: SSLEngine?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType, engine) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String, socket: Socket?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType, socket) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String, socket: Socket?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, authType, socket) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String, engine: SSLEngine?) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, authType, engine) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, s: String) { + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, s) + return + } + } catch (e: CertificateException) { + // If there is an exception we fall back to checking fingerprints + if (fingerprints.isEmpty()) { + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) + } + } + + checkTrusted(chain) + } + + @Throws(CertificateException::class) + private fun checkTrusted(chain: Array) { + val cert = chain[0] + + if (!fingerprints.any { it.matchesCert(cert) }) { + throw UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null) + } + } + + override fun getAcceptedIssuers(): Array { + return defaultTrustManager?.acceptedIssuers ?: emptyArray() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManagerProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManagerProvider.kt new file mode 100644 index 0000000000..d47adbfd07 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManagerProvider.kt @@ -0,0 +1,41 @@ +/* + * 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.matrix.android.internal.network.ssl + +import android.os.Build +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager + +internal object PinnedTrustManagerProvider { + // Set to false to perform some tests + private const val USE_DEFAULT_TRUST_MANAGER = true + + fun provide(fingerprints: List?, + defaultTrustManager: X509TrustManager?): X509TrustManager { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && defaultTrustManager is X509ExtendedTrustManager) { + PinnedTrustManagerApi24( + fingerprints.orEmpty(), + defaultTrustManager.takeIf { USE_DEFAULT_TRUST_MANAGER } + ) + } else { + PinnedTrustManager( + fingerprints.orEmpty(), + defaultTrustManager.takeIf { USE_DEFAULT_TRUST_MANAGER } + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 72d6f95852..466ad6fc7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session import androidx.annotation.MainThread -import androidx.lifecycle.LiveData import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.SessionParams @@ -45,7 +44,6 @@ import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService -import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.typing.TypingUsersTracker import im.vector.matrix.android.api.session.user.UserService @@ -254,9 +252,9 @@ internal class DefaultSession @Inject constructor( eventBus.unregister(this) } - override fun getSyncStateLive(): LiveData { - return getSyncThread().liveState() - } + override fun getSyncStateLive() = getSyncThread().liveState() + + override fun getSyncState() = getSyncThread().currentState() override fun hasAlreadySynced(): Boolean { return syncTokenStore.getLastToken() != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt index fad9938387..ea2948c11f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt @@ -65,6 +65,7 @@ internal class RoomSummaryUpdater @Inject constructor( 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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index 3643d65505..49adb1b507 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -104,7 +104,7 @@ abstract class SyncService : Service() { try { syncTask.execute(params) // Start sync if we were doing an initial sync and the syncThread is not launched yet - if (isInitialSync && session.getSyncStateLive().value == SyncState.Idle) { + if (isInitialSync && session.getSyncState() == SyncState.Idle) { val isForeground = !backgroundDetectionObserver.isInBackground session.startSync(isForeground) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index d17294a967..45a478c749 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -51,7 +51,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle - private var liveState = MutableLiveData() + private var liveState = MutableLiveData(state) private val lock = Object() private val syncScope = CoroutineScope(SupervisorJob()) private val debouncer = Debouncer(createUIHandler()) @@ -98,6 +98,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, lock.notify() } + fun currentState() = state + fun liveState(): LiveData { return liveState } diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 4e8e5abc96..9b7fa01eaf 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -37,6 +37,8 @@ You removed your display name (it was %1$s) %1$s changed the topic to: %2$s You changed the topic to: %1$s + %1$s changed the room avatar + You changed the room avatar %1$s changed the room name to: %2$s You changed the room name to: %1$s %s placed a video call. @@ -71,6 +73,8 @@ You removed the room name %1$s removed the room topic You removed the room topic + %1$s removed the room avatar + You removed the room avatar Message removed Message removed by %1$s Message removed [reason: %1$s] diff --git a/vector/build.gradle b/vector/build.gradle index c80b716008..e497b156ae 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -17,7 +17,7 @@ androidExtensions { // Note: 2 digits max for each value ext.versionMajor = 0 ext.versionMinor = 91 -ext.versionPatch = 2 +ext.versionPatch = 4 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector/proguard-rules.pro b/vector/proguard-rules.pro index 56d3b95510..bc27767d8a 100644 --- a/vector/proguard-rules.pro +++ b/vector/proguard-rules.pro @@ -20,4 +20,7 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class im.vector.riotx.features.** { *; } \ No newline at end of file +-keep class im.vector.riotx.features.** { *; } + +## print all the rules in a file +# -printconfiguration ../proguard_files/full-r8-config.txt diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index a9a0cee0d6..a5fa8fc4e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -32,6 +32,7 @@ import im.vector.riotx.core.extensions.startSyncing import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.deleteAllFiles import im.vector.riotx.features.home.HomeActivity +import im.vector.riotx.features.home.ShortcutsHandler import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.settings.VectorPreferences @@ -82,6 +83,7 @@ class MainActivity : VectorBaseActivity() { @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var uiStateRepository: UiStateRepository + @Inject lateinit var shortcutsHandler: ShortcutsHandler override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -105,6 +107,9 @@ class MainActivity : VectorBaseActivity() { // Dismiss all notifications notificationDrawerManager.clearAllEvents() notificationDrawerManager.persistInfo() + + // Also clear the dynamic shortcuts + shortcutsHandler.clearShortcuts() } private fun parseArgs(): MainActivityArgs { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 3824ba7922..98bd3a76ab 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -117,8 +117,12 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho .observeOn(Schedulers.computation()) .map { it.asSequence() } .subscribe { summaries -> - val invites = summaries - .filter { it.membership == Membership.INVITE } + val invitesDm = summaries + .filter { it.membership == Membership.INVITE && it.isDirect } + .count() + + val invitesRoom = summaries + .filter { it.membership == Membership.INVITE && it.isDirect.not() } .count() val peopleNotifications = summaries @@ -139,12 +143,12 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho setState { copy( - notificationCountCatchup = peopleNotifications + roomsNotifications + invites, + notificationCountCatchup = peopleNotifications + roomsNotifications + invitesDm + invitesRoom, notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, - notificationCountPeople = peopleNotifications, - notificationHighlightPeople = peopleHasHighlight, - notificationCountRooms = roomsNotifications, - notificationHighlightRooms = roomsHasHighlight + notificationCountPeople = peopleNotifications + invitesDm, + notificationHighlightPeople = peopleHasHighlight || invitesDm > 0, + notificationCountRooms = roomsNotifications + invitesRoom, + notificationHighlightRooms = roomsHasHighlight || invitesRoom > 0 ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt index 1777fa03c1..a9a838195b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt @@ -27,7 +27,7 @@ import im.vector.matrix.android.api.session.sync.SyncState data class HomeDetailViewState( val groupSummary: Option = Option.empty(), val asyncRooms: Async> = Uninitialized, - val displayMode: RoomListDisplayMode = RoomListDisplayMode.HOME, + val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE, val notificationCountCatchup: Int = 0, val notificationHighlightCatchup: Boolean = false, val notificationCountPeople: Int = 0, diff --git a/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt index 34f2b7bd76..805014a81e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home import android.content.Context +import android.content.pm.ShortcutManager import android.graphics.Bitmap import android.os.Build import androidx.core.content.pm.ShortcutInfoCompat @@ -26,6 +27,7 @@ import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.room.detail.RoomDetailActivity +import io.reactivex.Observable import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import javax.inject.Inject @@ -51,6 +53,11 @@ class ShortcutsHandler @Inject constructor( } fun observeRoomsAndBuildShortcuts(): Disposable { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + // No op + return Observable.empty().subscribe() + } + return homeRoomListStore .observe() .distinct() @@ -78,6 +85,25 @@ class ShortcutsHandler @Inject constructor( } } + fun clearShortcuts() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + // No op + return + } + + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + + // We can only disabled pinned shortcuts with the API, but at least it will prevent the crash + if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + context.getSystemService(ShortcutManager::class.java) + ?.let { + it.disableShortcuts(it.pinnedShortcuts.map { pinnedShortcut -> pinnedShortcut.id }) + } + } + } + } + // PRIVATE API ********************************************************************************* private fun Bitmap.toProfileImageIcon(): IconCompat { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 6c192105d7..a50d748f93 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -179,6 +179,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_AVATAR, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 462caf8e97..22fd4eb5ec 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -48,6 +48,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_AVATAR, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 89e170e25e..c1f4187e0b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.GuestAccess import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.RoomAliasesContent +import im.vector.matrix.android.api.session.room.model.RoomAvatarContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomGuestAccessContent import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent @@ -57,6 +58,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) @@ -149,6 +151,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) + EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName) EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName) EventType.CALL_INVITE, @@ -220,6 +223,23 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active } } + private fun formatRoomAvatarEvent(event: Event, senderName: String?): CharSequence? { + val content = event.getClearContent().toModel() ?: return null + return if (content.avatarUrl.isNullOrEmpty()) { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_avatar_removed_by_you) + } else { + sp.getString(R.string.notice_room_avatar_removed, senderName) + } + } else { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_avatar_changed_by_you) + } else { + sp.getString(R.string.notice_room_avatar_changed, senderName) + } + } + } + private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 882d8e8869..62a835ee8e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -28,6 +28,7 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_AVATAR, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt index 9b5f74c9e6..3045987d01 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt @@ -30,8 +30,8 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) : return when (displayMode) { RoomListDisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE || roomSummary.userDrafts.isNotEmpty() - RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN - RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN + RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership.isActive() + RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership.isActive() RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index d7dabd0778..36874d5782 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -205,7 +205,7 @@ class NotificationUtils @Inject constructor(private val context: Context, @SuppressLint("NewApi") fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { // build the pending intent go to the home screen if this is clicked. - val i = Intent(context, HomeActivity::class.java) + val i = HomeActivity.newIntent(context) i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP val pi = PendingIntent.getActivity(context, 0, i, 0) @@ -307,7 +307,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) val answerCallPendingIntent = TaskStackBuilder.create(context) - .addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) + .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(VectorCallActivity.newIntent( context = context, callId = callId, @@ -459,7 +459,7 @@ class NotificationUtils @Inject constructor(private val context: Context, ) val contentPendingIntent = TaskStackBuilder.create(context) - .addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) + .addNextIntentWithParentStack(HomeActivity.newIntent(context)) // TODO other userId .addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, null)) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) @@ -651,7 +651,7 @@ class NotificationUtils @Inject constructor(private val context: Context, stringProvider.getString(R.string.join), joinIntentPendingIntent) - val contentIntent = Intent(context, HomeActivity::class.java) + val contentIntent = HomeActivity.newIntent(context) contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that contentIntent.data = Uri.parse("foobar://" + inviteNotifiableEvent.eventId) @@ -689,7 +689,7 @@ class NotificationUtils @Inject constructor(private val context: Context, .setColor(accentColor) .setAutoCancel(true) .apply { - val contentIntent = Intent(context, HomeActivity::class.java) + val contentIntent = HomeActivity.newIntent(context) contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId) @@ -718,7 +718,7 @@ class NotificationUtils @Inject constructor(private val context: Context, // Recreate the back stack return TaskStackBuilder.create(context) - .addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) + .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(roomIntentTap) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) } diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt index 4878134375..35f7e37f41 100644 --- a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt @@ -171,6 +171,7 @@ object ThemeUtils { * @param resourceId the resource id in the light theme * @return the resource Id for the current theme */ + // TODO Now that we are API 21, this is not necessary anymore fun getResourceId(c: Context, resourceId: Int): Int { val theme = getApplicationTheme(c) diff --git a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt index 43761ee214..d1a4315cc9 100644 --- a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt @@ -36,7 +36,7 @@ class SharedPreferencesUiStateRepository @Inject constructor(private val sharedP return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE VALUE_DISPLAY_MODE_ROOMS -> RoomListDisplayMode.ROOMS - else -> RoomListDisplayMode.HOME + else -> RoomListDisplayMode.PEOPLE // RoomListDisplayMode.HOME } } diff --git a/vector/src/main/res/layout/item_form_switch.xml b/vector/src/main/res/layout/item_form_switch.xml index 63004365dc..d4fdaae35c 100644 --- a/vector/src/main/res/layout/item_form_switch.xml +++ b/vector/src/main/res/layout/item_form_switch.xml @@ -30,13 +30,13 @@ @@ -46,7 +46,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="19dp" - android:layout_marginRight="19dp" app:layout_constraintBottom_toTopOf="@+id/formSwitchDivider" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/vector/src/main/res/menu/home_bottom_navigation.xml b/vector/src/main/res/menu/home_bottom_navigation.xml index f93a018436..aaf3203fe9 100644 --- a/vector/src/main/res/menu/home_bottom_navigation.xml +++ b/vector/src/main/res/menu/home_bottom_navigation.xml @@ -5,7 +5,8 @@ android:id="@+id/bottom_action_home" android:enabled="true" android:icon="@drawable/ic_home_bottom_catchup" - android:title="@string/bottom_action_home" /> + android:title="@string/bottom_action_home" + android:visible="false" />