Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,10 @@ gradlePlugin {
id = "artifact-deploy"
implementationClass = "deploy.ArtifactPublisher"
}

create("grantTestPermissions") {
id = "grant-test-permissions"
implementationClass = "GrantTestPermissions"
}
}
}
102 changes: 102 additions & 0 deletions buildSrc/src/main/kotlin/GrantTestPermissions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2025 Esri
*
* 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.
*/

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.kotlin.dsl.create
import java.io.File

/**
* An extension to configure the [GrantTestPermissions] plugin.
*/
abstract class PermissionPluginExtension {

/**
* The package name of the application for which permissions will be granted.
*/
abstract val packageName: Property<String>

/**
* The path to the ADB executable.
*/
abstract val adbPath: Property<File>

/**
* A list of permissions to be granted to the application.
* Each permission should be specified as a string, e.g., "android.permission.CAMERA".
*/
abstract val permissions: ListProperty<String>
}

/**
* A Gradle plugin to grant runtime permissions to a test application. This plugin is useful for
* Android instrumented tests that require specific permissions to be granted before running tests.
* It automates the process of granting permissions using ADB.
*
* @since 300.0.0
*/
class GrantTestPermissions : Plugin<Project> {
override fun apply(project: Project) {
val extension =
project.extensions.create<PermissionPluginExtension>("grantTestPermissionsConfig")
val grantPermissionTask = project.tasks.register("grantTestPermissions") {
doLast {
val packageName = extension.packageName.get()
val adb = extension.adbPath.get()
val permissions = extension.permissions.get()
println("Granting permissions for package: $packageName")
permissions.forEach { permission ->
if (permission.contains("MANAGE_EXTERNAL_STORAGE")) {
// Special handling for MANAGE_EXTERNAL_STORAGE permission
project.providers.exec {
commandLine(
adb,
"shell",
"appops",
"set",
"--uid",
packageName,
"MANAGE_EXTERNAL_STORAGE",
"allow"
)
}.result.get().assertNormalExitValue()
} else {
// General permission granting
project.providers.exec {
commandLine(adb, "shell", "pm", "grant", packageName, permission)
}.result.get().assertNormalExitValue()
}
println("Granted permission: $permission")
}
}
}

project.afterEvaluate {
// Configure the task runs right after the install task
project.tasks.matching { it.name.startsWith("install") && it.name.endsWith("AndroidTest") }
.forEach { installTask ->
grantPermissionTask.get().dependsOn(installTask)
}
// Configure the task runs before the connectedAndroidTest tasks
project.tasks.matching { it.name.startsWith("connected") && it.name.endsWith("AndroidTest") }
.forEach { connectedTask ->
connectedTask.dependsOn(grantPermissionTask.get())
}
}
}
}
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ androidxTestExt = "1.3.0"
androidXTestRunner = "1.7.0"
androidXTestRules = "1.7.0"
androidxWindow = "1.4.0"
mockingjay = "2.0.0"
workVersion = "2.10.3"
binaryCompatibilityValidator = "0.18.1"
compileSdk = "36"
compose-navigation = "2.9.3"
Expand All @@ -29,6 +27,7 @@ ksp = "2.2.10-2.0.2"
media3Exoplayer = "1.8.0"
minSdk = "28"
mlkitBarcodeScanning = "17.3.0"
mockingjay = "2.0.0"
kotlinxCoroutinesTest = "1.10.2"
kotlinxSerializationJson = "1.9.0"
mockkAndroid = "1.14.5"
Expand All @@ -39,6 +38,7 @@ arcore = "1.50.0"
playServicesLocation = "21.3.0"
gmazzo = "2.4.6"
gradleSecrets = "2.0.1"
workVersion = "2.10.3"

[libraries]
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose"}
Expand Down
21 changes: 17 additions & 4 deletions toolkit/featureforms/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ plugins {
id("artifact-deploy")
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
id("kotlin-parcelize")
id("grant-test-permissions")
alias(libs.plugins.binary.compatibility.validator) apply true
alias(libs.plugins.kotlin.serialization) apply true
}
Expand Down Expand Up @@ -51,14 +52,14 @@ android {
disable += "MissingTranslation"
disable += "MissingQuantity"
}

defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()

testApplicationId = "com.arcgismaps.toolkit.featureforms.test"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}

buildTypes {
release {
isMinifyEnabled = false
Expand Down Expand Up @@ -111,6 +112,19 @@ android {
}
}

grantTestPermissionsConfig {
packageName.set(android.defaultConfig.testApplicationId)
adbPath.set(android.adbExecutable.absoluteFile)
permissions.set(
listOf(
"android.permission.CAMERA",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.MANAGE_EXTERNAL_STORAGE"
)
)
}

apiValidation {
ignoredClasses.add("com.arcgismaps.toolkit.featureforms.BuildConfig")
// todo: remove when this is resolved https://github.com/Kotlin/binary-compatibility-validator/issues/74
Expand Down Expand Up @@ -153,7 +167,6 @@ apiValidation {
"com.arcgismaps.toolkit.featureforms.internal.screens.ComposableSingletons\$SelectNetworkSourceScreenKt",
"com.arcgismaps.toolkit.featureforms.internal.utils.ComposableSingletons\$SearchBarKt"
)

ignoredClasses.addAll(composableSingletons)
}

Expand Down
11 changes: 11 additions & 0 deletions toolkit/featureforms/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- Note: this manifest is only applied to instrumented tests -->

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA"/>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.arcgismaps.toolkit.featureforms

import android.Manifest
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createComposeRule
Expand All @@ -25,7 +24,6 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.requestFocus
import androidx.test.rule.GrantPermissionRule
import com.arcgismaps.mapping.featureforms.BarcodeScannerFormInput
import com.arcgismaps.mapping.featureforms.FieldFormElement
import com.google.common.truth.Truth.assertThat
Expand All @@ -41,11 +39,6 @@ class BarcodeTests : FeatureFormTestRunner(
@get:Rule
val composeTestRule = createComposeRule()

// Grant camera permission for barcode scanning
@get:Rule
val runtimePermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(Manifest.permission.CAMERA)

Comment on lines 43 to -48
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am understanding this correctly, then the GrantTestPermissions plugin doesn't actually need to grant the camera permission before running the tests, because the test can request it. I have some questions about this though:

  1. Could we grant the storage permissions using GrantPermissionRule? If so, could we add that as a BeforeAfterAction once we have that available to the toolkit (soon)? I investigated this and it's not possible
  2. If we can't do the above, could we keep the Camera permission granted at this level? This would be helpful because it would mean we could share the grant permissions task from the SDK plugin without modifications
  3. When we use GrantPermissionRule, we don't need to worry about the manifest? I'm guessing that is the case because I don't see it in the docs and you are just now adding the permissions to the manifest

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't do the above, could we keep the Camera permission granted at this level? This would be helpful because it would mean we could share the grant permissions task from the SDK plugin without modifications.

Yeah, the camera permission can stay within the test. I just moved it out because the having all the permissions granted by the plugin made sense.

When we use GrantPermissionRule, we don't need to worry about the manifest? I'm guessing that is the case because I don't see it in the docs and you are just now adding the permissions to the manifest

I believe so according to the doc. But the manage storage permission definitely need the manifest.

/**
* Test case 11.1:
* Given a `FeatureForm` with a `BarcodeScannerFormInput`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import com.arcgismaps.LoadStatus
import com.arcgismaps.Loadable
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.data.QueryParameters
import com.arcgismaps.httpcore.authentication.ArcGISAuthenticationChallengeResponse
import com.arcgismaps.httpcore.authentication.NetworkAuthenticationChallenge
import com.arcgismaps.httpcore.authentication.NetworkAuthenticationChallengeHandler
import com.arcgismaps.httpcore.authentication.NetworkAuthenticationChallengeResponse
import com.arcgismaps.httpcore.authentication.ServerTrust
Expand Down Expand Up @@ -54,8 +52,9 @@ open class FeatureFormTestRunner(
private val objectId: Long,
private val user: String = BuildConfig.webMapUser,
private val password: String = BuildConfig.webMapPassword,
private val layerName: String = ""
private val layerName: String = "",
) {

/**
* The feature form for the feature with the given [objectId].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,15 @@ class GroupElementTests : FeatureFormTestRunner(
groupElement1.assertIsDisplayed()
// assert description is displayed
groupElement1.assertTextContains(groupFormElement1.description)
assert(groupElement1.isToggled())
// assert that the group is expanded
assert(groupElement1.onParent().isToggled())
// assert this group has children including the header
assert(groupElement1.onParent().onChildren().fetchSemanticsNodes().count() > 1)

val groupElement2 = composeTestRule.onNodeWithText("Group with Multiple Form Elements 2")
groupElement2.assertIsDisplayed()
assert(!groupElement2.isToggled())
// assert the group is expanded
assert(!groupElement2.onParent().isToggled())
// assert that only the header is displayed
assert(groupElement2.onParent().onChildren().fetchSemanticsNodes().count() == 1)
}
Expand Down
Loading