Skip to content
Draft
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())
}
}
}
}
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ androidxCore = "1.17.0"
androidxEspresso = "3.7.0"
androidxHiltNavigationCompose = "1.2.0"
androidxMaterialIcons = "1.7.8"
binaryCompatibilityValidator = "0.17.0"
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 +28,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 +39,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
24 changes: 20 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 @@ -148,13 +162,15 @@ apiValidation {
"com.arcgismaps.toolkit.featureforms.internal.components.material3.ComposableSingletons\$ModalBottomSheetKt",
"com.arcgismaps.toolkit.featureforms.internal.screens.ComposableSingletons\$UNAssociationGroupResultScreenKt"
)

ignoredClasses.addAll(composableSingletons)
}


dependencies {
api(arcgis.mapsSdk)
// mocking jay
implementation(libs.mockingjay)
//
implementation(libs.bundles.commonmark)
implementation(platform(libs.coil.bom))
implementation(libs.coil.compose)
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)

/**
* 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,14 +21,13 @@ 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
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.featureforms.FeatureForm
import com.arcgismaps.mapping.layers.FeatureLayer
import com.esri.mockingjay.MockingJayConfiguration
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -54,8 +53,10 @@ 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 = "",
mockMode : MockingJayConfiguration.Mode = MockingJayConfiguration.Mode.Playback
) : NetworkMockTestCase(mockMode = mockMode) {

/**
* The feature form for the feature with the given [objectId].
*/
Expand Down
Loading