diff --git a/.gitignore b/.gitignore index b67000e7b..127173826 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ proguard/ .classpath bin/ gen/ -/.settings +.settings/ .vscode/ # OS generated files # diff --git a/README.md b/README.md index 86ea230e3..2b2b85e1e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ The ArcGIS Maps SDK for Kotlin Toolkit contains components that will simplify yo * **[GeoView-Compose](toolkit/geoview-compose)** - Compose wrappers for the MapView and SceneView. * **[Callout](toolkit/geoview-compose#display-a-callout)** - Draws a callout on the GeoView to display Composable content. * **[Popup](toolkit/popup)** - View field values of features in a layer using the Popup API. +* **[UtilityNetworkTrace](toolkit/utilitynetworks)** - Configure, run, and visualize UtilityNetworkTraces on a composable MapView. +* **[Augmented Reality](toolkit/ar)** - Provides components to "augment" the physical world with virtual content. ## API Reference @@ -43,12 +45,15 @@ repositories { The *ArcGIS Maps SDK for Kotlin Toolkit* is released with a "bill of materials" (`BOM`). The releasable BOM is versioned and represents a set of versions of the toolkit components which are compatible with one another. You may specify dependencies as follows ``` -implementation(platform("com.esri:arcgis-maps-kotlin-toolkit-bom:200.5.0")) +implementation(platform("com.esri:arcgis-maps-kotlin-toolkit-bom:200.6.0")) implementation("com.esri:arcgis-maps-kotlin-toolkit-authentication") implementation("com.esri:arcgis-maps-kotlin-toolkit-compass") +implementation("com.esri:arcgis-maps-kotlin-toolkit-featureforms") implementation("com.esri:arcgis-maps-kotlin-toolkit-geoview-compose") implementation("com.esri:arcgis-maps-kotlin-toolkit-indoors") implementation("com.esri:arcgis-maps-kotlin-toolkit-popup") +implementation("com.esri:arcgis-maps-kotlin-toolkit-utilitynetworks") +implementation("com.esri:arcgis-maps-kotlin-toolkit-ar") ``` The template and TemplateApp modules are for bootstrapping new modules. diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index e96a5f922..941902952 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -31,8 +31,9 @@ val artifactoryUsername: String by project val artifactoryPassword: String by project val versionNumber: String by project val buildNumber: String by project -val ignoreBuildNumber: String by project -val artifactVersion: String = if (ignoreBuildNumber == "true") { +val finalBuild: Boolean = (project.properties["finalBuild"] ?: "false") + .run { this == "true" } +val artifactVersion: String = if (finalBuild) { versionNumber } else { "$versionNumber-$buildNumber" diff --git a/build.gradle.kts b/build.gradle.kts index adc31058d..d678be7e9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,7 @@ plugins { alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.dokka) apply false alias(libs.plugins.gmazzo.test.aggregation) + alias(libs.plugins.compose.compiler) apply false } buildscript { @@ -38,6 +39,38 @@ buildscript { // before any dependent subproject uses its symbols to configure a dokka task. classpath(libs.dokka.versioning) } + val finalBuild: Boolean = (project.properties["finalBuild"] ?: "false") + .run { this == "true" } + + if (finalBuild) { + check(project.hasProperty("versionNumber")) + project.logger.info("release candidate build requested version ${project.properties["versionNumber"]}") + } else if (!project.hasProperty("versionNumber") && !project.hasProperty("buildNum")) { + // both version number and build number must be set + java.util.Properties().run { + try { + File(project.projectDir, "../buildnum/buildnum.txt") + .reader() + .use { + load(it) + } + this["BUILDVER"]?.let { + project.extra.set("versionNumber", it) + } + this["BUILDNUM"]?.let { + project.extra.set("buildNumber", it) + } + check(project.hasProperty("versionNumber")) + check(project.hasProperty("buildNumber")) + project.logger.info("version and build number set from buildnum.txt to ${project.properties["versionNumber"]}-${project.properties["buildNumber"]}") + } catch (t: Throwable) { + // The buildnum file is not there. ignore it and log a warning. + project.logger.warn("the buildnum.txt file is missing or not readable") + project.extra.set("versionNumber", "0.0.0") + project.extra.set("buildNumber", "SNAPSHOT") + } + } + } } // Path to the centralized folder in root directory where test reports for connected tests end up @@ -60,6 +93,7 @@ testAggregation { "microapps-lib", "template", "template-app", + "utility-network-trace-app", "composable-map").forEach { this.modules.include(project(":$it")) } @@ -77,3 +111,4 @@ fun getModulesExcept(vararg modulesToExclude: String): List = } .filter { !modulesToExclude.contains(it) } // exclude specified modules } + diff --git a/buildSrc/src/main/kotlin/deploy/ArtifactPublisher.kt b/buildSrc/src/main/kotlin/deploy/ArtifactPublisher.kt index 5d81a334b..0f1cb0aaf 100644 --- a/buildSrc/src/main/kotlin/deploy/ArtifactPublisher.kt +++ b/buildSrc/src/main/kotlin/deploy/ArtifactPublisher.kt @@ -21,10 +21,12 @@ package deploy import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin -import org.gradle.kotlin.dsl.* import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.plugins.MavenPublishPlugin import org.gradle.configurationcache.extensions.capitalized +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.provideDelegate import java.net.URI /** @@ -45,9 +47,10 @@ class ArtifactPublisher : Plugin { val artifactoryUsername: String by project val artifactoryPassword: String by project val versionNumber: String by project - val ignoreBuildNumber: String by project + val finalBuild: Boolean = (project.properties["finalBuild"] ?: "false") + .run { this == "true" } val buildNumber: String by project - val artifactVersion: String = if (ignoreBuildNumber == "true") { + val artifactVersion: String = if (finalBuild) { versionNumber } else { "$versionNumber-$buildNumber" diff --git a/ci/publish.sh b/ci/publish.sh index cd4a69f19..b35a8f118 100755 --- a/ci/publish.sh +++ b/ci/publish.sh @@ -69,7 +69,8 @@ function _check_options_and_set_variables() { function _publish() { _log "Publish the release build to artifactory" - if ! ${apps_path}/arcgis-maps-sdk-kotlin-toolkit/ci/run_gradle_task.sh -v -t publish -x "-PartifactoryUsername=${ARTIFACTORY_USR} -PartifactoryPassword=${ARTIFACTORY_PSW} -PversionNumber=${BUILDVER} -PbuildNumber=${BUILDNUM}" ; then + + if ! ${apps_path}/arcgis-maps-sdk-kotlin-toolkit/ci/run_gradle_task.sh -t publish -x "-PartifactoryUsername=${ARTIFACTORY_USR} -PartifactoryPassword=${ARTIFACTORY_PSW} -PversionNumber=${BUILDVER} -PbuildNumber=${BUILDNUM} -PfinalBuild=${FINAL_BUILD}" ; then echo "error: Running the publish gradle task failed" exit 1 fi diff --git a/ci/run_gradle_task.sh b/ci/run_gradle_task.sh index 741e31158..3900f4c43 100755 --- a/ci/run_gradle_task.sh +++ b/ci/run_gradle_task.sh @@ -77,7 +77,7 @@ function _run_gradle_task() { if [ "${verbose}" == "true" ]; then gradle_flags+="--info " fi - + if ! ./gradlew ${gradle_flags} ${task} ${extra_gradle_flags}; then echo echo "error: Something went wrong when running gradle" diff --git a/gradle.properties b/gradle.properties index 7561d7b9c..717ed2948 100644 --- a/gradle.properties +++ b/gradle.properties @@ -44,14 +44,10 @@ artifactoryGroupId=com.esri artifactoryArtifactBaseId=arcgis-maps-kotlin-toolkit artifactoryUsername="" artifactoryPassword="" -# these numbers will define the artifact version on artifactory -# and are overridden by the jenkins command line in the daily build -versionNumber=200.5.0 -buildNumber=0000-snapshot -#set this flag to `true` to ignore the build number when publishing. This -# will publish an artifact with a build number like "..:200.2.0" as opposed to "...:200.2.0-3963 -ignoreBuildNumber=true -# these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency -# and are generally not overridden at the command line unless a special build is requested. -sdkVersionNumber=200.5.0 + +# These versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency +# when building the toolkit locally, typically from Android Studio. When building the toolkit +# with CI, these versions are obtained from command line provided properties, see sdkVersionNumber +# in settings.gradle.kts. +sdkVersionNumber=200.6.0 sdkBuildNumber= diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5c383274..15b113bb8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,8 @@ activityCompose = "1.8.2" androidGradlePlugin = "8.3.2" androidXBrowser = "1.8.0" coilBOM = "2.5.0" -composeBOM = "2024.04.01" +composeBOM = "2024.10.00" +androidxCamera = "1.4.0-rc02" androidxComposeCompiler = "1.5.12" androidxCore = "1.13.0" androidxEspresso = "3.5.1" @@ -11,33 +12,41 @@ androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.7.0" androidxMaterialIcons = "1.6.6" androidxTestExt = "1.1.5" +androidXTestRunner = "1.6.2" +androidXTestRules = "1.6.1" androidxWindow = "1.2.0" binaryCompatibilityValidator = "0.14.0" compileSdk = "34" compose-navigation = "2.7.7" +commonMark = "0.22.0" dokka = "1.9.20" hilt = "2.49" hiltExt = "1.2.0" junit = "4.13.2" -kotlin = "1.9.23" -ksp = "1.9.23-1.0.20" +kotlin = "2.0.20" +ksp = "2.0.20-1.0.24" media3Exoplayer = "1.3.1" minSdk = "26" +mlkitBarcodeScanning = "17.3.0" kotlinxCoroutinesTest = "1.8.0" kotlinxSerializationJson = "1.6.3" mockkAndroid = "1.13.10" room = "2.6.1" truth = "1.4.2" uiautomator = "2.3.0" +arcore = "1.45.0" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose"} androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidXBrowser"} +androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "androidxCamera" } +androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "androidxCamera" } +androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "androidxCamera" } +androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "androidxCamera" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBOM" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui"} -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime"} androidx-compose-material3 = { module = "androidx.compose.material3:material3"} androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "compose-navigation" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics"} @@ -56,13 +65,18 @@ androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "medi androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3Exoplayer" } androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" } androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } +androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidXTestRunner" } +androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidXTestRules" } androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } androidx-window = { group = "androidx.window", name = "window", version.ref = "androidxWindow" } androidx-window-core = { group = "androidx.window", name = "window-core", version.ref = "androidxWindow" } +mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkitBarcodeScanning" } coil-bom = { group = "io.coil-kt", name = "coil-bom", version.ref = "coilBOM" } coil = { group = "io.coil-kt", name = "coil" } coil-base = { group = "io.coil-kt", name = "coil-base" } coil-compose = { group = "io.coil-kt", name = "coil-compose" } +commonmark = { group = "org.commonmark", name = "commonmark", version.ref="commonMark" } +commonmark-strikethrough = { group = "org.commonmark", name = "commonmark-ext-gfm-strikethrough", version.ref="commonMark" } hilt-android-core = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" } @@ -78,6 +92,7 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = room-ext = { group = "androidx.room", name = "room-ktx", version.ref = "room" } truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } dokka-versioning = { group = "org.jetbrains.dokka", name = "versioning-plugin", version.ref = "dokka" } +arcore = { group = "com.google.ar", name = "core", version.ref = "arcore" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -89,13 +104,26 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } gradle-secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version = "2.0.1"} kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } gmazzo-test-aggregation = { id = "io.github.gmazzo.test.aggregation.results", version = "2.2.0" } [bundles] +camerax = [ + "androidx-camera-core", + "androidx-camera-camera2", + "androidx-camera-lifecycle", + "androidx-camera-view" +] + core = [ "androidx-core-ktx" ] +commonmark = [ + "commonmark", + "commonmark-strikethrough" +] + composeCore = [ "androidx-compose-material3", "androidx-compose-ui", @@ -107,7 +135,8 @@ composeCore = [ composeTest = [ "androidx-test-ext", "androidx-test-espresso-core", - "androidx-compose-ui-test" + "androidx-compose-ui-test", + "truth" ] debug = [ @@ -115,14 +144,13 @@ debug = [ "androidx-compose-ui-test-manifest" ] -serialization = [ - "kotlinx-serialization-core", - "kotlinx-serialization-json", - "kotlin-reflect" -] - unitTest = [ "junit", "kotlinx-coroutines-test", "truth" ] + +androidXTest = [ + "androidx-test-runner", + "androidx-test-rules" +] diff --git a/kdoc/build.gradle.kts b/kdoc/build.gradle.kts index 64c554d13..13f34fc19 100644 --- a/kdoc/build.gradle.kts +++ b/kdoc/build.gradle.kts @@ -23,13 +23,6 @@ plugins { } val versionNumber: String by project -val buildNumber: String by project -val ignoreBuildNumber: String by project -val artifactVersion: String = if (ignoreBuildNumber == "true") { - versionNumber -} else { - "$versionNumber-$buildNumber" -} // make this project get evaluated after all the other projects // so that we can be sure the logic to determine released components diff --git a/microapps/ArTabletopApp/.gitignore b/microapps/ArTabletopApp/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/microapps/ArTabletopApp/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/microapps/ArTabletopApp/README.md b/microapps/ArTabletopApp/README.md new file mode 100644 index 000000000..4740f9e9b --- /dev/null +++ b/microapps/ArTabletopApp/README.md @@ -0,0 +1,11 @@ +# AR TableTop Micro-app + +This micro-app demonstrates the use of the `TableTopSceneView` toolkit component which renders an `ArcGISSceneLayer` with buildings onto a physical surface detected in the device's camera feed. + +![Screenshot](../../toolkit/ar/screenshot.png) + +## Usage + +Launch the app and follow on-screen instructions to point the camera towards a surface while moving the device slightly. When a surface is detected it will appear as a grid of white lines in the camera feed. Tap on the grid to choose an anchor location to place the 3D buildings. Shortly after tapping on the screen, the buildings will appear as shown in the screenshot above. + +For more information on the `TableTopSceneView` component and how it works, see it's [Readme](../../toolkit/ar/README.md). diff --git a/microapps/ArTabletopApp/app/.gitignore b/microapps/ArTabletopApp/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/microapps/ArTabletopApp/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/microapps/ArTabletopApp/app/build.gradle.kts b/microapps/ArTabletopApp/app/build.gradle.kts new file mode 100644 index 000000000..592e88f3a --- /dev/null +++ b/microapps/ArTabletopApp/app/build.gradle.kts @@ -0,0 +1,98 @@ +/* + * + * Copyright 2024 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. + * + */ + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") +} + +secrets { + // this file doesn't contain secrets, it just provides defaults which can be committed into git. + defaultPropertiesFileName = "secrets.defaults.properties" +} + +android { + namespace = "com.arcgismaps.toolkit.artabletopapp" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId ="com.arcgismaps.toolkit.artabletopapp" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.compileSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner ="androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + //proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),("proguard-rules.pro" + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + buildConfig = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + /** + * Configures the test report for connected (instrumented) tests to be copied to a central + * folder in the project's root directory. + */ + testOptions { + val connectedTestReportsPath: String by project + reportDir = "$connectedTestReportsPath/${project.name}" + } +} + +dependencies { + implementation(project(":geoview-compose")) + implementation(project(":microapps-lib")) + implementation(project(":ar")) + implementation(libs.arcore) + implementation(arcgis.mapsSdk) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.composeCore) + implementation(libs.bundles.core) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + testImplementation(libs.bundles.unitTest) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.bundles.composeTest) + debugImplementation(libs.bundles.debug) +} diff --git a/microapps/ArTabletopApp/app/proguard-rules.pro b/microapps/ArTabletopApp/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/microapps/ArTabletopApp/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/microapps/ArTabletopApp/app/src/main/AndroidManifest.xml b/microapps/ArTabletopApp/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..26791142d --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/MainActivity.kt b/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/MainActivity.kt new file mode 100644 index 000000000..f9cea6477 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/MainActivity.kt @@ -0,0 +1,103 @@ +/* + * + * Copyright 2024 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. + * + */ + +package com.arcgismaps.toolkit.artabletopapp + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arcgismaps.ApiKey +import com.arcgismaps.ArcGISEnvironment +import com.arcgismaps.toolkit.artabletopapp.screens.MainScreen +import com.esri.microappslib.theme.MicroAppTheme +import com.google.ar.core.ArCoreApk +import kotlinx.coroutines.flow.MutableStateFlow + +class MainActivity : ComponentActivity() { + + private var userRequestedInstall: Boolean = true + + // Flow to track if Google Play Services for AR is installed on the device + // By using `collectAsStateWithLifecycle()` in the composable, the UI will recompose when the + // value changes + private val isGooglePlayServicesArInstalled = MutableStateFlow(false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY) + setContent { + MicroAppTheme { + if (isGooglePlayServicesArInstalled.collectAsStateWithLifecycle().value) { + ArTabletopApp() + } else { + Text(text = stringResource(R.string.arcore_not_installed_screen_message)) + } + } + } + } + + override fun onResume() { + super.onResume() + checkGooglePlayServicesArInstalled() + } + + private fun checkGooglePlayServicesArInstalled() { + // Check if Google Play Services for AR is installed on the device + // If it's not installed, this method should get called twice: once to request the installation + // and once to ensure it was installed when the activity resumes + try { + when (ArCoreApk.getInstance().requestInstall(this, userRequestedInstall)) { + ArCoreApk.InstallStatus.INSTALL_REQUESTED -> { + userRequestedInstall = false + return + } + + ArCoreApk.InstallStatus.INSTALLED -> { + isGooglePlayServicesArInstalled.value = true + return + } + } + } catch (e: Exception) { + Log.e("ArTabletopApp", "Error checking Google Play Services for AR: ${e.message}") + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ArTabletopApp() { + Scaffold( + topBar = { TopAppBar(title = { Text("ArTabletopApp") }) } + ) { + Box(Modifier.padding(it)) { + MainScreen() + } + } +} diff --git a/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/screens/MainScreen.kt b/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/screens/MainScreen.kt new file mode 100644 index 000000000..074be6b78 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/screens/MainScreen.kt @@ -0,0 +1,204 @@ +/* + * + * Copyright 2024 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. + * + */ + +package com.arcgismaps.toolkit.artabletopapp.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arcgismaps.LoadStatus +import com.arcgismaps.data.ArcGISFeature +import com.arcgismaps.geometry.Point +import com.arcgismaps.mapping.ArcGISScene +import com.arcgismaps.mapping.ElevationSource +import com.arcgismaps.mapping.Surface +import com.arcgismaps.mapping.layers.ArcGISSceneLayer +import com.arcgismaps.mapping.view.ScreenCoordinate +import com.arcgismaps.toolkit.ar.TableTopSceneView +import com.arcgismaps.toolkit.ar.TableTopSceneViewProxy +import com.arcgismaps.toolkit.ar.TableTopSceneViewStatus +import com.arcgismaps.toolkit.ar.rememberTableTopSceneViewStatus +import com.arcgismaps.toolkit.artabletopapp.R +import kotlinx.coroutines.launch + +@Composable +fun MainScreen() { + val arcGISSceneLayer = remember { + ArcGISSceneLayer("https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/DevA_BuildingShells/SceneServer") + } + val arcGISScene = remember { + ArcGISScene().apply { + operationalLayers.add(arcGISSceneLayer) + baseSurface = Surface().apply { + elevationSources.add( + ElevationSource.fromTerrain3dService() + ) + opacity = 0f + } + } + } + val arcGISSceneAnchor = remember { + Point(-122.68350326165559, 45.53257485106716, 0.0, arcGISScene.spatialReference) + } + + // Tracks the currently selected building + var identifiedBuilding by remember { mutableStateOf(null) } + + var initializationStatus: TableTopSceneViewStatus by rememberTableTopSceneViewStatus() + val tableTopSceneViewProxy = remember { TableTopSceneViewProxy() } + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize()) { + TableTopSceneView( + arcGISScene = arcGISScene, + arcGISSceneAnchor = arcGISSceneAnchor, + translationFactor = 400.0, + modifier = Modifier.fillMaxSize(), + clippingDistance = 400.0, + tableTopSceneViewProxy = tableTopSceneViewProxy, + onInitializationStatusChanged = { + initializationStatus = it + }, + onSingleTapConfirmed = { tap -> + arcGISSceneLayer.clearSelection() + coroutineScope.launch { + identifiedBuilding = arcGISSceneLayer.identifyBuilding( + tap.screenCoordinate, + tableTopSceneViewProxy + ) + identifiedBuilding?.let { identifiedBuilding -> + arcGISSceneLayer.selectFeature(identifiedBuilding.feature) + } + } + } + ) { + identifiedBuilding?.let { + Callout(it.location) { + Text("Building ID: ${it.feature.attributes["OBJECTID"]}") + } + } + } + } + + // Show an overlay with instructions or progress indicator based on the initialization status + when (val status = initializationStatus) { + is TableTopSceneViewStatus.Initializing -> TextWithScrim(text = stringResource(R.string.initializing_overlay)) + is TableTopSceneViewStatus.DetectingPlanes -> TextWithScrim(text = stringResource(R.string.detect_planes_overlay)) + is TableTopSceneViewStatus.Initialized -> { + val sceneLoadStatus = arcGISScene.loadStatus.collectAsStateWithLifecycle().value + when (sceneLoadStatus) { + is LoadStatus.NotLoaded -> { + // Tell the user to tap the screen if the scene has not started loading + TextWithScrim(text = stringResource(R.string.tap_scene_overlay)) + } + + is LoadStatus.Loading -> { + // The scene may take a while to load, so show a progress indicator + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } + + is LoadStatus.FailedToLoad -> { + TextWithScrim( + text = stringResource( + R.string.failed_to_load_scene, + sceneLoadStatus.error + ) + ) + } + + LoadStatus.Loaded -> {} // Do nothing + } + } + + is TableTopSceneViewStatus.FailedToInitialize -> { + TextWithScrim( + text = stringResource( + R.string.failed_to_initialize_overlay, + status.error.message ?: status.error + ) + ) + } + } +} + +/** + * Displays the provided [text] on top of a half-transparent gray background. + * + * @since 200.6.0 + */ +@Composable +fun TextWithScrim(text: String) { + Column( + modifier = Modifier + .background(Color.Gray.copy(alpha = 0.5f)) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = text) + } +} + +/** + * Identifies the building at the given [screenCoordinate] and returns the identified building. + * If no feature is identified, or if no location can be found for the given [screenCoordinate], + * this function returns `null`. + * + * @since 200.6.0 + */ +private suspend fun ArcGISSceneLayer.identifyBuilding( + screenCoordinate: ScreenCoordinate, + proxy: TableTopSceneViewProxy +): IdentifiedBuilding? { + val identifyLayerResult = proxy.identify(this, screenCoordinate, 50.dp).getOrElse { + return null + } + val identifiedFeature = + identifyLayerResult.geoElements.firstOrNull() as? ArcGISFeature ?: return null + val identifiedPoint = proxy.screenToLocation(screenCoordinate).getOrNull() ?: return null + return IdentifiedBuilding(identifiedFeature, identifiedPoint) +} + +/** + * Represents a building feature along with the location in the scene where it was identified. + * + * @since 200.6.0 + */ +private data class IdentifiedBuilding(val feature: ArcGISFeature, val location: Point) diff --git a/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/ui/theme/Color.kt b/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/ui/theme/Color.kt new file mode 100644 index 000000000..a972158de --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/ui/theme/Color.kt @@ -0,0 +1,29 @@ +/* + * + * Copyright 2024 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. + * + */ + +package com.arcgismaps.toolkit.artabletopapp.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) diff --git a/microapps/ArTabletopApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/microapps/ArTabletopApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..92971e871 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + diff --git a/microapps/ArTabletopApp/app/src/main/res/drawable/ic_launcher_background.xml b/microapps/ArTabletopApp/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..b51b347d8 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/microapps/ArTabletopApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6b4a339aa --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/microapps/ArTabletopApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6b4a339aa --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/microapps/ArTabletopApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/microapps/ArTabletopApp/app/src/main/res/values/colors.xml b/microapps/ArTabletopApp/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..6c58071d0 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/values/colors.xml @@ -0,0 +1,28 @@ + + + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/microapps/ArTabletopApp/app/src/main/res/values/strings.xml b/microapps/ArTabletopApp/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..c3e773f70 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/values/strings.xml @@ -0,0 +1,29 @@ + + + + ArTabletopApp + Google Play Services for AR must be installed to run this app. + Initialization status: %1$s + Move your phone around to detect planes... + Lat: %1$s, Lon: %2$s + Tap on a plane to place the scene + Setting up AR... + Failed to initialize: %1$s + Failed to load scene: %1$s + diff --git a/microapps/ArTabletopApp/app/src/main/res/values/themes.xml b/microapps/ArTabletopApp/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..697911fcf --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/values/themes.xml @@ -0,0 +1,23 @@ + + + + + +