diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 860c5e628..4e7dab8d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,8 @@ mockkAndroid = "1.13.10" room = "2.6.1" truth = "1.4.2" uiautomator = "2.3.0" +arcore = "1.45.0" +obj = "0.4.0" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose"} @@ -76,6 +78,8 @@ 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" } +obj = { module = "de.javagl:obj", version.ref = "obj" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } 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/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..6883fb2cf --- /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" + } + @Suppress("UnstableApiUsage") + 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) + 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..d130d71e4 --- /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..f690bba47 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/java/com/arcgismaps/toolkit/artabletopapp/MainActivity.kt @@ -0,0 +1,217 @@ +/* + * + * 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 android.view.View +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Text +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.arcgismaps.ApiKey +import com.arcgismaps.ArcGISEnvironment +import com.arcgismaps.geometry.Point +import com.arcgismaps.mapping.ArcGISScene +import com.arcgismaps.mapping.BasemapStyle +import com.arcgismaps.toolkit.ar.TableTopSceneView +import com.arcgismaps.toolkit.ar.TableTopSceneViewProxy +import com.esri.microappslib.theme.MicroAppTheme +import com.google.ar.core.ArCoreApk +import com.google.ar.core.Session +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +class MainActivity : ComponentActivity() { + + private var isARCoreSupported = false + + private val message = MutableStateFlow("Setting up ARCore...") + private var isGooglePlayServicesArInstalled = false + private var userRequestedInstall = true + + private val session = MutableStateFlow(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY) + enableEdgeToEdge() + checkARCoreSupport() + checkGooglePlayServicesArInstalled() + createSession() + requestCameraPermission() + configureSession() + + // required for displayRotationHelper + // if this is not called, the session will be paused when the screen is turned off and the app crashes + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + lifecycleScope.launch { + message.collect { + Log.d("MainActivity", it) + } + } + + setContent { + MicroAppTheme { + val arcGISScene = remember { + ArcGISScene(BasemapStyle.OsmStandard) + } + val tableTopSceneViewProxy = remember { TableTopSceneViewProxy() } + var tappedLocation by remember { mutableStateOf(null) } + // Using this to ensure we have a non=null session before even thinking about rendering + session.collectAsState().value?.let { session -> + TableTopSceneView( + arcGISScene = arcGISScene, + session = session, + modifier = Modifier.fillMaxSize(), + tableTopSceneViewProxy = tableTopSceneViewProxy, + onSingleTapConfirmed = { + val location = tableTopSceneViewProxy.screenToBaseSurface(it.screenCoordinate) + location?.let { point -> + tappedLocation = point + } + } + ) { + tappedLocation?.let { + Callout(location = it, modifier = Modifier.wrapContentSize()) { + Text("Lat: ${it.y.roundToInt()}, Lon: ${it.x.roundToInt()}") + } + } + } + } + } + } + } + + private fun checkARCoreSupport() { + // Check if ARCore is supported on the device + message.value = "Checking ARCore support..." + val availability = ArCoreApk.getInstance().checkAvailability(this) + isARCoreSupported = availability.isSupported + message.value = if (isARCoreSupported) { + "ARCore is supported on this device." + } else { + "ARCore is not supported on this device." + } + } + + private fun checkGooglePlayServicesArInstalled() { + message.value = "Checking Google Play Services for AR..." + // Check if Google Play Services for AR is installed on the device + try { + if (session.value == null) { + //Note: we should use the async method here but possibly this is causing some timing issues + // so we are using the sync method for now + when (ArCoreApk.getInstance().requestInstall(this, userRequestedInstall)) { + ArCoreApk.InstallStatus.INSTALL_REQUESTED -> { + message.value = "Google Play Services for AR installation requested." + userRequestedInstall = false + return + } + + ArCoreApk.InstallStatus.INSTALLED -> { + isGooglePlayServicesArInstalled = true + message.value = "Google Play Services for AR is installed." + return + } + + } + } + } catch (e: Exception) { + message.value = "Error checking Google Play Services for AR: ${e.message}" + } + } + + private fun createSession() { + if (isARCoreSupported && isGooglePlayServicesArInstalled) { + session.value = Session(this) + message.value = "ARCore session created." + } + } + + private fun requestCameraPermission() { + val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + message.value = "Camera permission granted." + } else { + message.value = "Camera permission denied." + } + } + requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) + } + + private fun configureSession() { + session.value?.let { session -> + message.value = "Configuring ARCore session..." + // TODO: Configure the ARCore session + session.configure(session.config/*.apply{}*/) + message.value = "ARCore session configured." + } + } + + // ensure full screen and immersive mode + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + // https://developer.android.com/training/system-ui/immersive.html#sticky + this + .getWindow() + .getDecorView() + .setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_FULLSCREEN + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) + } + } + + override fun onResume() { + super.onResume() + session.value?.resume() + } + + override fun onPause() { + super.onPause() + session.value?.pause() + } + + override fun onDestroy() { + session.value?.close() + super.onDestroy() + } +} 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..ecdd09577 --- /dev/null +++ b/microapps/ArTabletopApp/app/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + ArTabletopApp + 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 @@ + + + + + +