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" }