diff --git a/ORLib/build.gradle b/ORLib/build.gradle index 91bf604..226c01f 100644 --- a/ORLib/build.gradle +++ b/ORLib/build.gradle @@ -82,6 +82,11 @@ dependencies { implementation libs.password.generator api project(':orlib-protobuf') + + androidTestImplementation libs.junit + androidTestImplementation libs.androidx.test.junit + androidTestImplementation libs.androidx.test.runner + androidTestImplementation libs.espresso.core } diff --git a/ORLib/src/androidTest/AndroidManifest.xml b/ORLib/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..d71a54e --- /dev/null +++ b/ORLib/src/androidTest/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/ORLib/src/androidTest/java/io/openremote/orlib/ui/PermissionDisclosuresTest.kt b/ORLib/src/androidTest/java/io/openremote/orlib/ui/PermissionDisclosuresTest.kt new file mode 100644 index 0000000..a620053 --- /dev/null +++ b/ORLib/src/androidTest/java/io/openremote/orlib/ui/PermissionDisclosuresTest.kt @@ -0,0 +1,54 @@ +package io.openremote.orlib.ui + +import android.app.Activity +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.openremote.orlib.R +import org.junit.Test +import org.junit.runner.RunWith + +class PlatformThemeActivity : Activity() +class AppCompatThemeActivity : Activity() + +/** + * Regression test for https://github.com/openremote/console-android/issues/71: + * the disclosure dialog must not crash when the host activity does not use a + * Theme.AppCompat descendant, which is the case for consoles that supply their + * own theme. + */ +@RunWith(AndroidJUnit4::class) +class PermissionDisclosuresTest { + + private fun showDisclosureAndAssertVisible(scenario: ActivityScenario) { + scenario.onActivity { activity -> + PermissionDisclosures.show( + activity, + R.string.location_disclosure_title, + R.string.location_disclosure_body, + onAccept = {} + ) + } + onView(withText(R.string.disclosure_continue)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + } + + @Test + fun showsDisclosureOnNonAppCompatThemedActivity() { + ActivityScenario.launch(PlatformThemeActivity::class.java).use { + showDisclosureAndAssertVisible(it) + } + } + + @Test + fun showsDisclosureOnAppCompatThemedActivity() { + ActivityScenario.launch(AppCompatThemeActivity::class.java).use { + showDisclosureAndAssertVisible(it) + } + } +} diff --git a/ORLib/src/main/java/io/openremote/orlib/ui/PermissionDisclosures.kt b/ORLib/src/main/java/io/openremote/orlib/ui/PermissionDisclosures.kt index 91545fa..c170ba7 100644 --- a/ORLib/src/main/java/io/openremote/orlib/ui/PermissionDisclosures.kt +++ b/ORLib/src/main/java/io/openremote/orlib/ui/PermissionDisclosures.kt @@ -1,8 +1,10 @@ package io.openremote.orlib.ui import android.app.Activity +import android.util.TypedValue import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog +import androidx.appcompat.view.ContextThemeWrapper import io.openremote.orlib.R /** @@ -23,7 +25,20 @@ object PermissionDisclosures { if (activity.isFinishing || activity.isDestroyed) { return@runOnUiThread } - AlertDialog.Builder(activity) + // The AppCompat AlertDialog requires a theme that resolves alertDialogTheme; + // consuming apps may run this activity with a non-AppCompat theme. + val themedContext = if (activity.theme.resolveAttribute( + androidx.appcompat.R.attr.alertDialogTheme, TypedValue(), true + ) + ) { + activity + } else { + ContextThemeWrapper( + activity, + androidx.appcompat.R.style.Theme_AppCompat_DayNight_Dialog_Alert + ) + } + AlertDialog.Builder(themedContext) .setTitle(title) .setMessage(message) .setCancelable(false) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 586f9d6..6c972f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,9 +4,12 @@ androidx-appcompat = "1.7.1" androidx-constraintlayout = "2.2.1" androidx-core-ktx = "1.18.0" androidx-preference = "1.2.1" +androidx-test-junit = "1.3.0" +androidx-test-runner = "1.7.0" axion-release-plugin = "1.21.2" coroutines = "1.11.0" esp-idf-provisioning = "lib-2.4.4" +espresso = "3.7.0" eventbus = "3.3.1" firebase-bom = "34.16.0" google-services-plugin = "4.5.0" @@ -28,9 +31,12 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" } +androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } esp-idf-provisioning = { module = "com.github.espressif:esp-idf-provisioning-android", version.ref = "esp-idf-provisioning" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" }