Merge pull request #6238 from element-hq/feature/bma/importFromClassic
Ensure that Element X can use the service from Element Classic.
This commit is contained in:
commit
c91c78171e
4 changed files with 69 additions and 31 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import extension.buildConfigFieldStr
|
||||||
import extension.setupDependencyInjection
|
import extension.setupDependencyInjection
|
||||||
import extension.testCommonDependencies
|
import extension.testCommonDependencies
|
||||||
|
|
||||||
|
|
@ -23,6 +24,30 @@ android {
|
||||||
isIncludeAndroidResources = true
|
isIncludeAndroidResources = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
val elementClassicPackageKey = "elementClassicPackage"
|
||||||
|
val elementClassicPackage = "im.vector.app"
|
||||||
|
val elementClassicPackageDebug = "$elementClassicPackage.debug"
|
||||||
|
val elementClassicPackageNightly = "$elementClassicPackage.nightly"
|
||||||
|
getByName("release") {
|
||||||
|
manifestPlaceholders[elementClassicPackageKey] = elementClassicPackage
|
||||||
|
buildConfigFieldStr(elementClassicPackageKey, elementClassicPackage)
|
||||||
|
}
|
||||||
|
getByName("debug") {
|
||||||
|
manifestPlaceholders[elementClassicPackageKey] = elementClassicPackageDebug
|
||||||
|
buildConfigFieldStr(elementClassicPackageKey, elementClassicPackageDebug)
|
||||||
|
}
|
||||||
|
register("nightly") {
|
||||||
|
matchingFallbacks += listOf("release")
|
||||||
|
manifestPlaceholders[elementClassicPackageKey] = elementClassicPackageNightly
|
||||||
|
buildConfigFieldStr(elementClassicPackageKey, elementClassicPackageNightly)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependencyInjection()
|
setupDependencyInjection()
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||||
</intent>
|
</intent>
|
||||||
|
|
||||||
|
<!-- To be able to start the service exported by Element Classic -->
|
||||||
|
<package android:name="${elementClassicPackage}" />
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<!-- Permission to read data from Element classic -->
|
|
||||||
<uses-permission android:name="im.vector.app.READ_DATA" />
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@ import android.os.Messenger
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.features.login.impl.BuildConfig
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -44,7 +43,11 @@ sealed interface ElementClassicConnectionState {
|
||||||
object Idle : ElementClassicConnectionState
|
object Idle : ElementClassicConnectionState
|
||||||
object ElementClassicNotFound : ElementClassicConnectionState
|
object ElementClassicNotFound : ElementClassicConnectionState
|
||||||
object ElementClassicReadyNoSession : ElementClassicConnectionState
|
object ElementClassicReadyNoSession : ElementClassicConnectionState
|
||||||
data class ElementClassicReady(val userId: UserId) : ElementClassicConnectionState
|
data class ElementClassicReady(
|
||||||
|
val userId: UserId,
|
||||||
|
val secrets: String,
|
||||||
|
) : ElementClassicConnectionState
|
||||||
|
|
||||||
data class Error(val error: String) : ElementClassicConnectionState
|
data class Error(val error: String) : ElementClassicConnectionState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +59,6 @@ class DefaultElementClassicConnection(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@AppCoroutineScope
|
@AppCoroutineScope
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val buildMeta: BuildMeta,
|
|
||||||
) : ElementClassicConnection {
|
) : ElementClassicConnection {
|
||||||
// Messenger for communicating with the service.
|
// Messenger for communicating with the service.
|
||||||
private var messenger: Messenger? = null
|
private var messenger: Messenger? = null
|
||||||
|
|
@ -101,7 +103,7 @@ class DefaultElementClassicConnection(
|
||||||
// applications replace our component.
|
// applications replace our component.
|
||||||
try {
|
try {
|
||||||
val intentService = Intent()
|
val intentService = Intent()
|
||||||
intentService.setComponent(getElementClassicComponent(buildMeta))
|
intentService.setComponent(getElementClassicComponent())
|
||||||
if (context.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) {
|
if (context.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) {
|
||||||
Timber.tag(loggerTag.value).d("Binding returned true")
|
Timber.tag(loggerTag.value).d("Binding returned true")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -198,17 +200,8 @@ class DefaultElementClassicConnection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getElementClassicComponent(buildMeta: BuildMeta) = ComponentName(
|
private fun getElementClassicComponent() = ComponentName(
|
||||||
buildString {
|
BuildConfig.elementClassicPackage,
|
||||||
append(ELEMENT_CLASSIC_APP_ID)
|
|
||||||
append(
|
|
||||||
when (buildMeta.buildType) {
|
|
||||||
BuildType.DEBUG -> ELEMENT_CLASSIC_APP_ID_DEBUG_SUFFIX
|
|
||||||
BuildType.NIGHTLY -> ELEMENT_CLASSIC_APP_ID_NIGHTLY_SUFFIX
|
|
||||||
BuildType.RELEASE -> ELEMENT_CLASSIC_APP_ID_RELEASE_SUFFIX
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME,
|
ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -220,9 +213,14 @@ class DefaultElementClassicConnection(
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
ElementClassicConnectionState.Error(error)
|
ElementClassicConnectionState.Error(error)
|
||||||
} else {
|
} else {
|
||||||
val userId = getString(KEY_USER_ID_STR)?.let(::UserId)
|
val userId = getString(KEY_USER_ID_STR)?.takeIf { it.isNotEmpty() }?.let(::UserId)
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
ElementClassicConnectionState.ElementClassicReady(userId)
|
val secrets = getString(KEY_SECRETS_STR)?.takeIf { it.isNotEmpty() }
|
||||||
|
if (secrets == null) {
|
||||||
|
ElementClassicConnectionState.Error("No secrets received from Element Classic")
|
||||||
|
} else {
|
||||||
|
ElementClassicConnectionState.ElementClassicReady(userId, secrets)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ElementClassicConnectionState.ElementClassicReadyNoSession
|
ElementClassicConnectionState.ElementClassicReadyNoSession
|
||||||
}
|
}
|
||||||
|
|
@ -232,18 +230,31 @@ class DefaultElementClassicConnection(
|
||||||
|
|
||||||
// Everything in this companion object must match what is defined in Element Classic
|
// Everything in this companion object must match what is defined in Element Classic
|
||||||
private companion object {
|
private companion object {
|
||||||
|
const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService"
|
||||||
|
|
||||||
// Command to the service to get the data.
|
// Command to the service to get the data.
|
||||||
const val MSG_GET_DATA = 1
|
const val MSG_GET_DATA = 1
|
||||||
|
|
||||||
const val ELEMENT_CLASSIC_APP_ID = "im.vector.app"
|
|
||||||
const val ELEMENT_CLASSIC_APP_ID_DEBUG_SUFFIX = ".debug"
|
|
||||||
const val ELEMENT_CLASSIC_APP_ID_NIGHTLY_SUFFIX = ".nightly"
|
|
||||||
const val ELEMENT_CLASSIC_APP_ID_RELEASE_SUFFIX = ""
|
|
||||||
|
|
||||||
const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService"
|
|
||||||
|
|
||||||
// Keys for the bundle returned from the service
|
// Keys for the bundle returned from the service
|
||||||
const val KEY_ERROR_STR = "error"
|
const val KEY_ERROR_STR = "error"
|
||||||
const val KEY_USER_ID_STR = "userId"
|
const val KEY_USER_ID_STR = "userId"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key to extract the secrets from the bundle, as a Json string.
|
||||||
|
* Json will have this format:
|
||||||
|
* {
|
||||||
|
* "cross_signing" : {
|
||||||
|
* "master_key" : "z8RUxnaAGu___REDACTED___k+BQL9o",
|
||||||
|
* "user_signing_key" : "baJHzA___REDACTED___xMLbSUAXw9QUzqms",
|
||||||
|
* "self_signing_key" : "DU0CvLtR2G/___REDACTED___dV/MONNq4nsQhM"
|
||||||
|
* },
|
||||||
|
* "backup" : {
|
||||||
|
* "algorithm" : "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||||
|
* "key" : "VzncmQ+UOV___REDACTED___patxDz7m0Nc",
|
||||||
|
* "backup_version" : "1"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const val KEY_SECRETS_STR = "secrets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.test.A_SECRET
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
|
@ -114,7 +115,7 @@ class LoginWithClassicPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
elementClassicConnection.emitState(
|
elementClassicConnection.emitState(
|
||||||
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
|
||||||
)
|
)
|
||||||
val readyState = awaitItem()
|
val readyState = awaitItem()
|
||||||
assertThat(readyState.canLoginWithClassic).isTrue()
|
assertThat(readyState.canLoginWithClassic).isTrue()
|
||||||
|
|
@ -140,7 +141,7 @@ class LoginWithClassicPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
elementClassicConnection.emitState(
|
elementClassicConnection.emitState(
|
||||||
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
|
||||||
)
|
)
|
||||||
val readyState = awaitItem()
|
val readyState = awaitItem()
|
||||||
assertThat(readyState.canLoginWithClassic).isTrue()
|
assertThat(readyState.canLoginWithClassic).isTrue()
|
||||||
|
|
@ -175,7 +176,7 @@ class LoginWithClassicPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
elementClassicConnection.emitState(
|
elementClassicConnection.emitState(
|
||||||
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
|
||||||
)
|
)
|
||||||
// No new item, because canLoginWithClassic is still false
|
// No new item, because canLoginWithClassic is still false
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +193,7 @@ class LoginWithClassicPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
// Note: it should not happen IRL
|
// Note: it should not happen IRL
|
||||||
elementClassicConnection.emitState(
|
elementClassicConnection.emitState(
|
||||||
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID)
|
ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET)
|
||||||
)
|
)
|
||||||
// No new item, because canLoginWithClassic is still false
|
// No new item, because canLoginWithClassic is still false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue