Add full screen intent permissions banner (#3024)
* Add full screen intent permissions banner, creating `:libraries:fullscreenintent` modules.
* Add it to notification settings too:
- Create `libraries:fullscreenintent` modules for the permission presenter and associated data.
- Add the presenter and states to `NotificationSettingsPresenter` and `NotificationSettingsView`.
* Use the right API to check for full screen intent permissions.
- Use the right package name for `:libraries:permission` contents.
* Fix broken tests (flaky?)
* Ignore coverage verification for fake and small presenters
---------
Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
committed by
GitHub
parent
7322c75a67
commit
feef0f6976
29
libraries/fullscreenintent/api/build.gradle.kts
Normal file
29
libraries/fullscreenintent/api/build.gradle.kts
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.fullscreenintent.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.libraries.fullscreenintent.api
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
interface FullScreenIntentPermissionsPresenter : Presenter<FullScreenIntentPermissionsState>
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.libraries.fullscreenintent.api
|
||||
|
||||
data class FullScreenIntentPermissionsState(
|
||||
val permissionGranted: Boolean,
|
||||
val shouldDisplayBanner: Boolean,
|
||||
val dismissFullScreenIntentBanner: () -> Unit,
|
||||
val openFullScreenIntentSettings: () -> Unit,
|
||||
)
|
||||
58
libraries/fullscreenintent/impl/build.gradle.kts
Normal file
58
libraries/fullscreenintent/impl/build.gradle.kts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.fullscreenintent.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.fullscreenintent.api)
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.permissions.noop)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
testImplementation(projects.libraries.fullscreenintent.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.testtags)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.libraries.fullscreenintent.impl
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||
import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFullScreenIntentPermissionsPresenter @Inject constructor(
|
||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||
private val externalIntentLauncher: ExternalIntentLauncher,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val notificationManagerCompat: NotificationManagerCompat,
|
||||
preferencesDataStoreFactory: PreferenceDataStoreFactory,
|
||||
) : FullScreenIntentPermissionsPresenter {
|
||||
companion object {
|
||||
private const val PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED = "PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED"
|
||||
}
|
||||
|
||||
private val dataStore = preferencesDataStoreFactory.create("full_screen_intent_permissions")
|
||||
|
||||
private val isFullScreenIntentBannerDismissed = dataStore.data.map { prefs ->
|
||||
prefs[booleanPreferencesKey(PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED)] ?: false
|
||||
}
|
||||
|
||||
private suspend fun dismissFullScreenIntentBanner() {
|
||||
dataStore.edit { prefs ->
|
||||
prefs[booleanPreferencesKey(PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED)] = true
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): FullScreenIntentPermissionsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val isGranted = notificationManagerCompat.canUseFullScreenIntent()
|
||||
val isBannerDismissed by isFullScreenIntentBannerDismissed.collectAsState(initial = true)
|
||||
return FullScreenIntentPermissionsState(
|
||||
permissionGranted = isGranted,
|
||||
shouldDisplayBanner = !isBannerDismissed && !isGranted,
|
||||
dismissFullScreenIntentBanner = {
|
||||
coroutineScope.launch {
|
||||
dismissFullScreenIntentBanner()
|
||||
}
|
||||
},
|
||||
openFullScreenIntentSettings = ::openFullScreenIntentSettings,
|
||||
)
|
||||
}
|
||||
|
||||
private fun openFullScreenIntentSettings() {
|
||||
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
try {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT,
|
||||
Uri.parse("package:${buildMeta.applicationId}")
|
||||
)
|
||||
externalIntentLauncher.launch(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, buildMeta.applicationId)
|
||||
externalIntentLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.libraries.fullscreenintent.test
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.fullscreenintent.impl.DefaultFullScreenIntentPermissionsPresenter
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
|
||||
import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher
|
||||
import io.element.android.services.toolbox.test.intent.FakeExternalIntentLauncher
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DefaultFullScreenIntentPermissionsPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `shouldDisplay - is true when permission is not granted and banner is not dismissed`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
notificationManagerCompat = mockk {
|
||||
every { canUseFullScreenIntent() } returns false
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialItem = awaitItem()
|
||||
assertThat(initialItem.shouldDisplayBanner).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldDisplay - is false if permission is granted`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
notificationManagerCompat = mockk {
|
||||
every { canUseFullScreenIntent() } returns true
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialItem = awaitItem()
|
||||
assertThat(initialItem.shouldDisplayBanner).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dismissFullScreenIntentBanner - makes shouldDisplay false`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val loadedItem = awaitItem()
|
||||
loadedItem.dismissFullScreenIntentBanner()
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(awaitItem().shouldDisplayBanner).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openFullScreenIntentSettings - opens external screen using intent`() = runTest {
|
||||
val launchLambda = lambdaRecorder<Intent, Unit> { _ -> }
|
||||
val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda)
|
||||
val presenter = createPresenter(externalIntentLauncher = externalIntentLauncher)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val loadedItem = awaitItem()
|
||||
loadedItem.openFullScreenIntentSettings()
|
||||
|
||||
launchLambda.assertions().isCalledOnce()
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openFullScreenIntentSettings - does nothing in old APIs`() = runTest {
|
||||
val launchLambda = lambdaRecorder<Intent, Unit> { _ -> }
|
||||
val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda)
|
||||
val presenter = createPresenter(
|
||||
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.Q),
|
||||
externalIntentLauncher = externalIntentLauncher,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val loadedItem = awaitItem()
|
||||
loadedItem.openFullScreenIntentSettings()
|
||||
|
||||
launchLambda.assertions().isNeverCalled()
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
buildVersionSdkIntProvider: FakeBuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.UPSIDE_DOWN_CAKE),
|
||||
dataStoreFactory: FakePreferenceDataStoreFactory = FakePreferenceDataStoreFactory(),
|
||||
externalIntentLauncher: ExternalIntentLauncher = FakeExternalIntentLauncher(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true)
|
||||
) = DefaultFullScreenIntentPermissionsPresenter(
|
||||
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
|
||||
externalIntentLauncher = externalIntentLauncher,
|
||||
buildMeta = buildMeta,
|
||||
preferencesDataStoreFactory = dataStoreFactory,
|
||||
notificationManagerCompat = notificationManagerCompat,
|
||||
)
|
||||
}
|
||||
28
libraries/fullscreenintent/test/build.gradle.kts
Normal file
28
libraries/fullscreenintent/test/build.gradle.kts
Normal file
@@ -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-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.fullscreenintent.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.fullscreenintent.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.libraries.fullscreenintent.test
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||
|
||||
class FakeFullScreenIntentPermissionsPresenter : FullScreenIntentPermissionsPresenter {
|
||||
var state = FullScreenIntentPermissionsState(
|
||||
permissionGranted = true,
|
||||
shouldDisplayBanner = false,
|
||||
dismissFullScreenIntentBanner = {},
|
||||
openFullScreenIntentSettings = {},
|
||||
)
|
||||
@Composable
|
||||
override fun present(): FullScreenIntentPermissionsState {
|
||||
return state
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user