Browse Source

Add a flag to enable or disable incoming share

pull/2984/head
Benoit Marty 4 months ago committed by Benoit Marty
parent
commit
e619fefb7f
  1. 12
      app/src/main/AndroidManifest.xml
  2. 1
      appnav/build.gradle.kts
  3. 6
      appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt
  4. 24
      appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt
  5. 23
      features/share/api/src/main/kotlin/io/element/android/features/share/api/ShareService.kt
  6. 1
      features/share/impl/build.gradle.kts
  7. 76
      features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareService.kt
  8. 28
      features/share/test/build.gradle.kts
  9. 29
      features/share/test/src/main/kotlin/io/element/android/features/share/test/FakeShareService.kt
  10. 7
      libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
  11. 1
      libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

12
app/src/main/AndroidManifest.xml

@ -120,10 +120,18 @@
<data android:host="user" /> <data android:host="user" />
<data android:host="room" /> <data android:host="room" />
</intent-filter> </intent-filter>
</activity>
<!-- Using an activity-alias for incoming share intent, in order
to be able to disable the feature programmatically -->
<activity-alias
android:name=".ShareActivity"
android:exported="true"
android:targetActivity=".MainActivity">
<!-- Incoming share simple --> <!-- Incoming share simple -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*"/> <data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.OPENABLE" />
@ -136,7 +144,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.OPENABLE" />
</intent-filter> </intent-filter>
</activity> </activity-alias>
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"

1
appnav/build.gradle.kts

@ -72,6 +72,7 @@ dependencies {
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.rageshake.impl)
testImplementation(projects.features.share.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(libs.test.appyx.junit) testImplementation(libs.test.appyx.junit)

6
appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt

@ -23,6 +23,7 @@ import androidx.compose.runtime.getValue
import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.SuperProperties
import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter
import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter
import io.element.android.features.share.api.ShareService
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.SdkMetadata import io.element.android.libraries.matrix.api.SdkMetadata
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -34,6 +35,7 @@ class RootPresenter @Inject constructor(
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
private val appErrorStateService: AppErrorStateService, private val appErrorStateService: AppErrorStateService,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val shareService: ShareService,
private val sdkMetadata: SdkMetadata, private val sdkMetadata: SdkMetadata,
) : Presenter<RootState> { ) : Presenter<RootState> {
@Composable @Composable
@ -52,6 +54,10 @@ class RootPresenter @Inject constructor(
) )
} }
LaunchedEffect(Unit) {
shareService.observeFeatureFlag(this)
}
return RootState( return RootState(
rageshakeDetectionState = rageshakeDetectionState, rageshakeDetectionState = rageshakeDetectionState,
crashDetectionState = crashDetectionState, crashDetectionState = crashDetectionState,

24
appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt

@ -28,6 +28,8 @@ import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import io.element.android.features.share.api.ShareService
import io.element.android.features.share.test.FakeShareService
import io.element.android.libraries.matrix.test.FakeSdkMetadata import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
@ -35,6 +37,8 @@ import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService import io.element.android.services.apperror.impl.DefaultAppErrorStateService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -55,6 +59,22 @@ class RootPresenterTest {
} }
} }
@Test
fun `present - check that share service is invoked`() = runTest {
val lambda = lambdaRecorder<CoroutineScope, Unit> { _ -> }
val presenter = createRootPresenter(
shareService = FakeShareService {
lambda(it)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(2)
lambda.assertions().isCalledOnce()
}
}
@Test @Test
fun `present - passes app error state`() = runTest { fun `present - passes app error state`() = runTest {
val presenter = createRootPresenter( val presenter = createRootPresenter(
@ -79,7 +99,8 @@ class RootPresenterTest {
} }
private fun createRootPresenter( private fun createRootPresenter(
appErrorService: AppErrorStateService = DefaultAppErrorStateService() appErrorService: AppErrorStateService = DefaultAppErrorStateService(),
shareService: ShareService = FakeShareService {},
): RootPresenter { ): RootPresenter {
val crashDataStore = FakeCrashDataStore() val crashDataStore = FakeCrashDataStore()
val rageshakeDataStore = FakeRageshakeDataStore() val rageshakeDataStore = FakeRageshakeDataStore()
@ -102,6 +123,7 @@ class RootPresenterTest {
rageshakeDetectionPresenter = rageshakeDetectionPresenter, rageshakeDetectionPresenter = rageshakeDetectionPresenter,
appErrorStateService = appErrorService, appErrorStateService = appErrorService,
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
shareService = shareService,
sdkMetadata = FakeSdkMetadata("sha") sdkMetadata = FakeSdkMetadata("sha")
) )
} }

23
features/share/api/src/main/kotlin/io/element/android/features/share/api/ShareService.kt

@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 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 io.element.android.features.share.api
import kotlinx.coroutines.CoroutineScope
interface ShareService {
fun observeFeatureFlag(coroutineScope: CoroutineScope)
}

1
features/share/impl/build.gradle.kts

@ -43,6 +43,7 @@ dependencies {
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)

76
features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareService.kt

@ -0,0 +1,76 @@
/*
* Copyright (c) 2024 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 io.element.android.features.share.impl
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.share.api.ShareService
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultShareService @Inject constructor(
private val featureFlagService: FeatureFlagService,
@ApplicationContext private val context: Context,
) : ShareService {
override fun observeFeatureFlag(coroutineScope: CoroutineScope) {
val shareActivityComponent = context
.packageManager
.getPackageInfo(
context.packageName,
PackageManager.GET_ACTIVITIES or PackageManager.MATCH_DISABLED_COMPONENTS
)
.activities
.firstOrNull { it.name.endsWith(".ShareActivity") }
?.let { shareActivityInfo ->
ComponentName(
shareActivityInfo.packageName,
shareActivityInfo.name,
)
}
?: return Unit.also {
Timber.w("ShareActivity not found")
}
featureFlagService.isFeatureEnabledFlow(FeatureFlags.IncomingShare)
.onEach { enabled ->
val state = if (enabled) {
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
try {
context.packageManager.setComponentEnabledSetting(
shareActivityComponent,
state,
PackageManager.DONT_KILL_APP,
)
} catch (e: Exception) {
Timber.e(e, "Failed to enable or disable the component")
}
}
.launchIn(coroutineScope)
}
}

28
features/share/test/build.gradle.kts

@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.share.test"
}
dependencies {
implementation(projects.features.share.api)
implementation(libs.coroutines.core)
implementation(projects.tests.testutils)
}

29
features/share/test/src/main/kotlin/io/element/android/features/share/test/FakeShareService.kt

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 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 io.element.android.features.share.test
import io.element.android.features.share.api.ShareService
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.CoroutineScope
class FakeShareService(
private val observeFeatureFlagLambda: (CoroutineScope) -> Unit = { lambdaError() }
) : ShareService {
override fun observeFeatureFlag(coroutineScope: CoroutineScope) {
observeFeatureFlagLambda(coroutineScope)
}
}

7
libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt

@ -96,4 +96,11 @@ enum class FeatureFlags(
defaultValue = true, defaultValue = true,
isFinished = false, isFinished = false,
), ),
IncomingShare(
key = "feature.incomingShare",
title = "Incoming Share support",
description = "Allow the application to receive data from other applications",
defaultValue = true,
isFinished = false,
),
} }

1
libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

@ -44,6 +44,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.RoomDirectorySearch -> false FeatureFlags.RoomDirectorySearch -> false
FeatureFlags.ShowBlockedUsersDetails -> false FeatureFlags.ShowBlockedUsersDetails -> false
FeatureFlags.QrCodeLogin -> OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE FeatureFlags.QrCodeLogin -> OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE
FeatureFlags.IncomingShare -> true
} }
} else { } else {
false false

Loading…
Cancel
Save