diff --git a/README.md b/README.md
index 37b58dc2b..2894b1d3d 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
## Overview
-ArcGIS Maps SDK for Kotlin v200.2.0 samples. The `main` branch of this repository contains sample app modules for the latest available version of the [ArcGIS Maps SDK Android Kotlin](https://developers.arcgis.com/kotlin/). Samples released under older versions can be found through the [git tags](https://github.com/Esri/arcgis-maps-sdk-kotlin-samples/tags). Please read our [wiki](https://github.com/Esri/arcgis-maps-sdk-kotlin-samples/wiki) for help with working with this repository.
+ArcGIS Maps SDK for Kotlin v200.3.0 samples. The `main` branch of this repository contains sample app modules for the latest available version of the [ArcGIS Maps SDK Android Kotlin](https://developers.arcgis.com/kotlin/). Samples released under older versions can be found through the [git tags](https://github.com/Esri/arcgis-maps-sdk-kotlin-samples/tags). Please read our [wiki](https://github.com/Esri/arcgis-maps-sdk-kotlin-samples/wiki) for help with working with this repository.
## Prerequisites
diff --git a/add-dynamic-entity-layer/README.md b/add-dynamic-entity-layer/README.md
new file mode 100644
index 000000000..d2ae6259a
--- /dev/null
+++ b/add-dynamic-entity-layer/README.md
@@ -0,0 +1,45 @@
+# Add dynamic entity layer
+
+Display data from an ArcGIS stream service using a dynamic entity layer.
+
+![Image of add dynamic entity layer](add-dynamic-entity-layer.png)
+
+## Use case
+
+A stream service is a type of service provided by ArcGIS Velocity and GeoEvent Server that allows clients to receive a stream of data observations via a web socket. ArcGIS Maps SDK for Kotlin allows you to connect to a stream service and manage the information as dynamic entities and display them in a dynamic entity layer. Displaying information from feeds such as a stream service is important in applications like dashboards where users need to visualize and track updates of real-world objects in real-time.
+
+Use `ArcGISStreamService` to manage the connection to the stream service and purge options to manage how much data is stored and maintained by the application. The dynamic entity layer will display the latest received observation, and you can set track display properties to determine how to display historical information for each dynamic entity. This includes the number of previous observations to show, whether to display track lines in-between previous observations, and setting renderers.
+
+## How to use the sample
+
+Use the controls to connect to or disconnect from the stream service, modify display properties in the dynamic entity layer, and purge all observations from the application.
+
+## How it works
+
+1. Create an `ArcGIStreamService` using a `Url`.
+2. Set a `DynamicEntityFilter` on the stream service to limit the amount of data coming from the server.
+3. Set the `MaximumDuration` property of the stream service `PurgeOptions` to limit the amount of data managed by the application.
+4. Create a `DynamicEntityLayer` using the stream service.
+5. Update values in the layer's `TrackDisplayProperties` to customize the layer's appearance.
+6. Add the `DynamicEntityLayer` to the map.
+
+## Relevant API
+
+* ArcGISStreamService
+* DynamicEntity
+* DynamicEntityFilter
+* DynamicEntityLayer
+* DynamicEntityPurgeOptions
+* TrackDisplayProperties
+
+## About the data
+
+This sample uses a [stream service](https://realtimegis2016.esri.com:6443/arcgis/rest/services/SandyVehicles/StreamServer) that simulates live data coming from snowplows near Sandy, Utah. There are multiple vehicle types and multiple agencies operating the snowplows.
+
+## Additional information
+
+More information about dynamic entities can be found in the [guide documentation](https://developers.arcgis.com/kotlin/real-time/work-with-dynamic-entities/).
+
+## Tags
+
+data, dynamic, entity, live, purge, real-time, service, stream, track
diff --git a/add-dynamic-entity-layer/README.metadata.json b/add-dynamic-entity-layer/README.metadata.json
new file mode 100644
index 000000000..fc7c41a10
--- /dev/null
+++ b/add-dynamic-entity-layer/README.metadata.json
@@ -0,0 +1,44 @@
+{
+ "category": "Layers",
+ "description": "Display data from an ArcGIS stream service using a dynamic entity layer.",
+ "formal_name": "AddDynamicEntityLayer",
+ "ignore": false,
+ "images": [
+ "add-dynamic-entity-layer.png"
+ ],
+ "keywords": [
+ "data",
+ "dynamic",
+ "entity",
+ "live",
+ "purge",
+ "real-time",
+ "service",
+ "stream",
+ "track",
+ "ArcGISStreamService",
+ "DynamicEntity",
+ "DynamicEntityFilter",
+ "DynamicEntityLayer",
+ "DynamicEntityPurgeOptions",
+ "TrackDisplayProperties"
+ ],
+ "language": "kotlin",
+ "redirect_from": "",
+ "relevant_apis": [
+ "ArcGISStreamService",
+ "DynamicEntity",
+ "DynamicEntityFilter",
+ "DynamicEntityLayer",
+ "DynamicEntityPurgeOptions",
+ "TrackDisplayProperties"
+ ],
+ "snippets": [
+ "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt",
+ "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt",
+ "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt"
+ ],
+ "title": "Add dynamic entity layer"
+}
diff --git a/add-dynamic-entity-layer/add-dynamic-entity-layer.png b/add-dynamic-entity-layer/add-dynamic-entity-layer.png
new file mode 100644
index 000000000..18353c6ab
Binary files /dev/null and b/add-dynamic-entity-layer/add-dynamic-entity-layer.png differ
diff --git a/add-dynamic-entity-layer/build.gradle b/add-dynamic-entity-layer/build.gradle
new file mode 100644
index 000000000..254bf5853
--- /dev/null
+++ b/add-dynamic-entity-layer/build.gradle
@@ -0,0 +1,48 @@
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ applicationId "com.esri.arcgismaps.sample.adddynamicentitylayer"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ buildConfigField("String", "API_KEY", API_KEY)
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$kotlinCompilerExt"
+ }
+
+ namespace 'com.esri.arcgismaps.sample.adddynamicentitylayer'
+}
+
+dependencies {
+ // lib dependencies from rootProject build.gradle
+ implementation "androidx.core:core-ktx:$ktxAndroidCore"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ktxLifecycle"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$ktxLifecycle"
+ implementation "androidx.activity:activity-compose:$composeActivityVersion"
+ // Jetpack Compose Bill of Materials
+ implementation platform("androidx.compose:compose-bom:$composeBOM")
+ // Jetpack Compose dependencies
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.material3:material3"
+ implementation "androidx.compose.ui:ui-tooling"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation project(path: ':samples-lib')
+}
diff --git a/add-dynamic-entity-layer/proguard-rules.pro b/add-dynamic-entity-layer/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/add-dynamic-entity-layer/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/add-dynamic-entity-layer/src/main/AndroidManifest.xml b/add-dynamic-entity-layer/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c6647b46d
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt
new file mode 100644
index 000000000..b8444a445
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/MainActivity.kt
@@ -0,0 +1,55 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.adddynamicentitylayer
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.adddynamicentitylayer.screens.MainScreen
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // authentication with an API key or named user is
+ // required to access basemaps and other location services
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)
+ setContent {
+ SampleAppTheme {
+ AddDynamicEntityLayerApp()
+ }
+ }
+ }
+
+ @Composable
+ private fun AddDynamicEntityLayerApp() {
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainScreen(
+ sampleName = getString(R.string.app_name),
+ application = application
+ )
+ }
+ }
+}
diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt
new file mode 100644
index 000000000..b5c32ac01
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/BottomSheetContent.kt
@@ -0,0 +1,176 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.adddynamicentitylayer.components
+
+import android.content.res.Configuration
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleTypography
+import kotlin.math.roundToInt
+
+/**
+ * Composable component to display Dynamic Entity Layer Settings
+ */
+@Composable
+fun DynamicEntityLayerProperties(
+ onTrackLineVisibilityChanged: (Boolean) -> Unit = { },
+ onPrevObservationsVisibilityChanged: (Boolean) -> Unit = { },
+ onObservationsChanged: (Float) -> Unit = { },
+ onPurgeAllObservations: () -> Unit = { },
+ isTrackLineVisible: Boolean,
+ isPrevObservationsVisible: Boolean,
+ observationsPerTrack: Float,
+ onDismiss: () -> Unit = { }
+) {
+ Column(Modifier.background(MaterialTheme.colorScheme.background)) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(20.dp, 20.dp, 20.dp, 0.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ style = SampleTypography.titleMedium,
+ text = "Dynamic Entity Settings",
+ color = MaterialTheme.colorScheme.primary
+ )
+ TextButton(
+ onClick = onDismiss
+ ) {
+ Text(text = "Done")
+ }
+ }
+
+ Surface(
+ modifier = Modifier.padding(20.dp),
+ tonalElevation = 1.dp,
+ shape = RoundedCornerShape(20.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
+ ) {
+ Column(
+ modifier = Modifier.padding(14.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = "Track Lines",
+ style = SampleTypography.bodyLarge
+ )
+ Switch(
+ checked = isTrackLineVisible,
+ onCheckedChange = {
+ onTrackLineVisibilityChanged(it)
+ }
+ )
+ }
+ Divider(thickness = 0.5.dp)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+
+ Text(
+ text = "Previous Observations",
+ style = SampleTypography.bodyLarge
+ )
+ Switch(
+ checked = isPrevObservationsVisible,
+ onCheckedChange = {
+ onPrevObservationsVisibilityChanged(it)
+ }
+ )
+ }
+ }
+ }
+
+ Surface(
+ modifier = Modifier.padding(20.dp),
+ tonalElevation = 1.dp,
+ shape = RoundedCornerShape(20.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
+ ) {
+ Column(
+ modifier = Modifier.padding(14.dp)
+ ) {
+ Column {
+ Row {
+ Text(
+ modifier = Modifier.weight(12f),
+ text = "Observations per track",
+ )
+ Text(
+ modifier = Modifier.weight(1f),
+ text = observationsPerTrack.roundToInt().toString()
+ )
+ }
+ Slider(
+ value = observationsPerTrack,
+ onValueChange = {
+ onObservationsChanged(it)
+ },
+ valueRange = 1f..16f
+ )
+ }
+ Divider(thickness = 0.5.dp)
+ TextButton(
+ modifier = Modifier.align(CenterHorizontally),
+ onClick = onPurgeAllObservations
+ )
+ {
+ Text(text = "Purge All Observations")
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
+@Composable
+fun DynamicEntityLayerPropertiesPreview() {
+ SampleAppTheme {
+ DynamicEntityLayerProperties(
+ isTrackLineVisible = true,
+ isPrevObservationsVisible = true,
+ observationsPerTrack = 5f
+ )
+ }
+}
diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt
new file mode 100644
index 000000000..73efcabbd
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/ComposeMapView.kt
@@ -0,0 +1,90 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.adddynamicentitylayer.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.LifecycleOwner
+import com.arcgismaps.mapping.view.MapView
+import kotlinx.coroutines.launch
+
+/**
+ * Wraps the MapView in a Composable function.
+ */
+@Composable
+fun ComposeMapView(
+ modifier: Modifier = Modifier,
+ mapViewModel: MapViewModel
+) {
+ // get an instance of the current lifecycle owner
+ val lifecycleOwner = LocalLifecycleOwner.current
+ // collect the latest state of the MapViewState
+ val mapViewState by mapViewModel.mapViewState.collectAsState()
+ // create and add MapView to the activity lifecycle
+ val mapView = createMapViewInstance(lifecycleOwner)
+
+ // wrap the MapView as an AndroidView
+ AndroidView(
+ modifier = modifier,
+ factory = { mapView },
+ // recomposes the MapView on changes in the MapViewState
+ update = { mapView ->
+ mapView.apply {
+ map = mapViewState.arcGISMap
+ setViewpoint(mapViewState.viewpoint)
+ }
+ }
+ )
+
+ // launch coroutine functions in the composition's CoroutineContext
+ LaunchedEffect(Unit) {
+ launch {
+ mapView.onSingleTapConfirmed.collect {
+ mapViewModel.dismissBottomSheet()
+ }
+ }
+ launch {
+ mapView.onPan.collect{
+ mapViewModel.dismissBottomSheet()
+ }
+ }
+ }
+}
+
+/**
+ * Create the MapView instance and add it to the Activity lifecycle
+ */
+@Composable
+fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView {
+ // create the MapView
+ val mapView = MapView(LocalContext.current)
+ // add the side effects for MapView composition
+ DisposableEffect(lifecycleOwner) {
+ lifecycleOwner.lifecycle.addObserver(mapView)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(mapView)
+ }
+ }
+ return mapView
+}
diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt
new file mode 100644
index 000000000..f26268939
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/components/MapViewModel.kt
@@ -0,0 +1,134 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.adddynamicentitylayer.components
+
+import android.app.Application
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.AndroidViewModel
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.Viewpoint
+import com.arcgismaps.mapping.layers.DynamicEntityLayer
+import com.arcgismaps.realtime.ArcGISStreamService
+import com.arcgismaps.realtime.ArcGISStreamServiceFilter
+import com.esri.arcgismaps.sample.adddynamicentitylayer.R
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+class MapViewModel(
+ application: Application,
+ private val sampleCoroutineScope: CoroutineScope,
+) : AndroidViewModel(application) {
+
+ // set the state of the switches and slider
+ val trackLineCheckedState = mutableStateOf(false)
+ val prevObservationCheckedState = mutableStateOf(false)
+ val trackSliderValue = mutableStateOf(5f)
+
+ // flag to show or dismiss the bottom sheet
+ val isBottomSheetVisible = mutableStateOf(false)
+
+ // set the MapView mutable stateflow
+ val mapViewState = MutableStateFlow(MapViewState())
+
+ // create ArcGIS Stream Service
+ private val streamService =
+ ArcGISStreamService(application.getString(R.string.stream_service_url))
+
+ // create ArcGISStreamServiceFilter
+ private val streamServiceFilter = ArcGISStreamServiceFilter()
+
+ // layer displaying the dynamic entities on the map
+ private val dynamicEntityLayer: DynamicEntityLayer
+
+ /**
+ * set the data source for the dynamic entity layer.
+ */
+ init {
+ // set condition on the ArcGISStreamServiceFilter to limit the amount of data coming from the server
+ streamServiceFilter.whereClause = "speed > 0"
+ streamService.apply {
+ filter = streamServiceFilter
+ // sets the maximum time (in seconds) an observation remains in the application.
+ purgeOptions.maximumDuration = 300.0
+ }
+ dynamicEntityLayer = DynamicEntityLayer(streamService)
+
+ // add the dynamic entity layer to the map's operational layers
+ mapViewState.value.arcGISMap.operationalLayers.add(dynamicEntityLayer)
+ }
+
+ // disconnects the stream service
+ fun disconnectStreamService() {
+ sampleCoroutineScope.launch {
+ streamService.disconnect()
+ }
+ }
+
+ // connects the stream service
+ fun connectStreamService() {
+ sampleCoroutineScope.launch {
+ streamService.connect()
+ }
+ }
+
+ // to dismiss the bottom sheet
+ fun dismissBottomSheet() {
+ isBottomSheetVisible.value = false
+ }
+
+ // to manage bottomSheet visibility
+ fun showBottomSheet() {
+ isBottomSheetVisible.value = true
+ }
+
+ // to manage track lines visibility
+ fun trackLineVisibility(checkedValue: Boolean) {
+ trackLineCheckedState.value = checkedValue
+ dynamicEntityLayer.trackDisplayProperties.showTrackLine = trackLineCheckedState.value
+ }
+
+ // to manage previous observations visibility
+ fun prevObservationsVisibility(checkedValue: Boolean) {
+ prevObservationCheckedState.value = checkedValue
+ dynamicEntityLayer.trackDisplayProperties.showPreviousObservations =
+ prevObservationCheckedState.value
+ }
+
+ // to set the maximum number of observations displayed per track
+ fun setObservations(sliderValue: Float) {
+ trackSliderValue.value = sliderValue
+ dynamicEntityLayer.trackDisplayProperties.maximumObservations =
+ trackSliderValue.value.toInt()
+ }
+
+ // remove all dynamic entity observations from the in-memory data cache as well as from the map
+ fun purgeAllObservations() {
+ sampleCoroutineScope.launch {
+ streamService.purgeAll()
+ }
+ }
+}
+
+/**
+ * Data class that represents the MapView state
+ */
+data class MapViewState(
+ var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISStreets),
+ var viewpoint: Viewpoint = Viewpoint(40.559691, -111.869001, 150000.0)
+)
diff --git a/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt
new file mode 100644
index 000000000..307933ae3
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/java/com/esri/arcgismaps/sample/adddynamicentitylayer/screens/MainScreen.kt
@@ -0,0 +1,112 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.adddynamicentitylayer.screens
+
+import android.app.Application
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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.unit.dp
+import com.esri.arcgismaps.sample.adddynamicentitylayer.components.ComposeMapView
+import com.esri.arcgismaps.sample.adddynamicentitylayer.components.DynamicEntityLayerProperties
+import com.esri.arcgismaps.sample.adddynamicentitylayer.components.MapViewModel
+import com.esri.arcgismaps.sample.sampleslib.components.BottomSheet
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+
+/**
+ * Main screen layout for the sample app
+ */
+@Composable
+fun MainScreen(sampleName: String, application: Application) {
+ /// coroutineScope that will be cancelled when this call leaves the composition
+ val sampleCoroutineScope = rememberCoroutineScope()
+
+ // create a ViewModel to handle MapView interactions
+ val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) }
+
+ // display connect/disconnect based on the boolean state
+ var isDisconnected by remember { mutableStateOf(false) }
+
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) },
+ content = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ ) {
+ // composable function that wraps the MapView
+ ComposeMapView(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f),
+ mapViewModel = mapViewModel
+ )
+ Row(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ TextButton(onClick = {
+ if (!isDisconnected)
+ mapViewModel.disconnectStreamService()
+ else
+ mapViewModel.connectStreamService()
+ isDisconnected = !isDisconnected
+ }) {
+ Text(text = if (!isDisconnected) "Disconnect" else "Connect")
+ }
+ TextButton(onClick = {
+ mapViewModel.showBottomSheet()
+ }) {
+ Text(text = "Dynamic Entity Settings")
+ }
+ }
+
+ }
+ // display a bottom sheet to set dynamic entity layer properties
+ BottomSheet(isVisible = mapViewModel.isBottomSheetVisible.value) {
+ DynamicEntityLayerProperties(
+ onTrackLineVisibilityChanged = mapViewModel::trackLineVisibility,
+ onPrevObservationsVisibilityChanged = mapViewModel::prevObservationsVisibility,
+ onObservationsChanged = mapViewModel::setObservations,
+ onPurgeAllObservations = mapViewModel::purgeAllObservations,
+ isTrackLineVisible = mapViewModel.trackLineCheckedState.value,
+ isPrevObservationsVisible = mapViewModel.prevObservationCheckedState.value,
+ observationsPerTrack = mapViewModel.trackSliderValue.value,
+ onDismiss = { mapViewModel.dismissBottomSheet() }
+ )
+ }
+ }
+ )
+}
diff --git a/add-dynamic-entity-layer/src/main/res/drawable-v24/ic_launcher_foreground.xml b/add-dynamic-entity-layer/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..c7bd21dbd
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/add-dynamic-entity-layer/src/main/res/drawable/ic_launcher_background.xml b/add-dynamic-entity-layer/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..6d8cae103
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/add-dynamic-entity-layer/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/add-dynamic-entity-layer/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-hdpi/ic_launcher.png b/add-dynamic-entity-layer/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a2f590828
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-hdpi/ic_launcher_round.png b/add-dynamic-entity-layer/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1b5239980
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-mdpi/ic_launcher.png b/add-dynamic-entity-layer/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ff10afd6e
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-mdpi/ic_launcher_round.png b/add-dynamic-entity-layer/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..115a4c768
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-xhdpi/ic_launcher.png b/add-dynamic-entity-layer/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dcd3cd808
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/add-dynamic-entity-layer/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..459ca609d
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-xxhdpi/ic_launcher.png b/add-dynamic-entity-layer/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8ca12fe02
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/add-dynamic-entity-layer/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8e19b410a
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/add-dynamic-entity-layer/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b824ebdd4
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/add-dynamic-entity-layer/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4c19a13c2
Binary files /dev/null and b/add-dynamic-entity-layer/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/add-dynamic-entity-layer/src/main/res/values/strings.xml b/add-dynamic-entity-layer/src/main/res/values/strings.xml
new file mode 100644
index 000000000..d5d718e78
--- /dev/null
+++ b/add-dynamic-entity-layer/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Add dynamic entity layer
+ https://realtimegis2016.esri.com:6443/arcgis/rest/services/SandyVehicles/StreamServer
+
diff --git a/add-web-tiled-layer/README.md b/add-web-tiled-layer/README.md
index 18bbe3bc9..e19de72b7 100644
--- a/add-web-tiled-layer/README.md
+++ b/add-web-tiled-layer/README.md
@@ -14,7 +14,7 @@ Run the sample and a map will appear. As you navigate the map, map tiles will be
## How it works
-1. Create a `WebTiledLayer` from a URL and a list of subdomains using `WebTiledLayer.create(urlTemplate, subDomains)`.
+1. Create a `WebTiledLayer` from a URL.
2. Set the `attribution` property on the web tiled layer. Note: this is a necessary step because web tiled services don't have associated service metadata.
3. Create a new `Basemap` from the layer and apply it to the mapView for display.
@@ -25,7 +25,7 @@ Run the sample and a map will appear. As you navigate the map, map tiles will be
## About the data
-The basemap in this sample is provided by [Stamen Design](http://maps.stamen.com). Stamen publishes tiled services based on OpenStreetMap data with several unique styles applied.
+The basemap in this sample is provided by [ArcGIS Living Atlas of the World](https://www.arcgis.com/home/item.html?id=1e126e7520f9466c9ca28b8f28b5e500). ArcGIS Living Atlas of the World provides tiled services with several unique styles.
## Additional information
@@ -41,4 +41,4 @@ For more information about web tiled layers, see the following resources:
## Tags
-layer, OGC, Open Street Map, OpenStreetMap, stamen.com, tiled, tiles
+ArcGIS Living Atlas, layer, OGC, tiled, tiles
diff --git a/add-web-tiled-layer/README.metadata.json b/add-web-tiled-layer/README.metadata.json
index 09484b65e..71a802765 100644
--- a/add-web-tiled-layer/README.metadata.json
+++ b/add-web-tiled-layer/README.metadata.json
@@ -7,11 +7,9 @@
"add-web-tiled-layer.png"
],
"keywords": [
+ "ArcGIS Living Atlas",
"OGC",
- "Open Street Map",
- "OpenStreetMap",
"layer",
- "stamen.com",
"tiled",
"tiles",
"Basemap",
diff --git a/add-web-tiled-layer/add-web-tiled-layer.png b/add-web-tiled-layer/add-web-tiled-layer.png
index 3e33986ed..d51ec065e 100644
Binary files a/add-web-tiled-layer/add-web-tiled-layer.png and b/add-web-tiled-layer/add-web-tiled-layer.png differ
diff --git a/add-web-tiled-layer/src/main/java/com/esri/arcgismaps/sample/addwebtiledlayer/MainActivity.kt b/add-web-tiled-layer/src/main/java/com/esri/arcgismaps/sample/addwebtiledlayer/MainActivity.kt
index 0a71779d8..4775c3335 100644
--- a/add-web-tiled-layer/src/main/java/com/esri/arcgismaps/sample/addwebtiledlayer/MainActivity.kt
+++ b/add-web-tiled-layer/src/main/java/com/esri/arcgismaps/sample/addwebtiledlayer/MainActivity.kt
@@ -39,18 +39,13 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
lifecycle.addObserver(mapView)
- // list of sub-domains
- val subDomains = listOf("a", "b", "c", "d")
-
- // build the web tiled layer from stamen url and subDomains
- val webTiledLayer =
- WebTiledLayer.create(
- getString(R.string.template_uri_stamen),
- subDomains
- ).apply {
- // set the attribution on the layer
- attribution = getString(R.string.stamen_attribution)
- }
+ // build the web tiled layer from ArcGIS Living Atlas of the World tile service url
+ val webTiledLayer = WebTiledLayer.create(
+ urlTemplate = getString(R.string.template_uri_living_atlas)
+ ).apply {
+ // set the attribution on the layer
+ attribution = getString(R.string.living_atlas_attribution)
+ }
// use web tiled layer as Basemap
val map = ArcGISMap(Basemap(webTiledLayer))
diff --git a/add-web-tiled-layer/src/main/res/values/strings.xml b/add-web-tiled-layer/src/main/res/values/strings.xml
index b1a4aae2f..5fa9c7d7e 100644
--- a/add-web-tiled-layer/src/main/res/values/strings.xml
+++ b/add-web-tiled-layer/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
Add web tiled layer
- https://stamen-tiles-{subdomain}.a.ssl.fastly.net/terrain/{level}/{col}/{row}.png
- Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.
+ https://server.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{level}/{row}/{col}.jpg
+ Map tiles by ArcGIS Living Atlas of the World, under Esri Master License Agreement. Data by Esri, Garmin, GEBCO, NOAA NGDC, and other contributors.
diff --git a/authenticate-with-oauth/src/main/AndroidManifest.xml b/authenticate-with-oauth/src/main/AndroidManifest.xml
index 67a55000e..9c302b693 100644
--- a/authenticate-with-oauth/src/main/AndroidManifest.xml
+++ b/authenticate-with-oauth/src/main/AndroidManifest.xml
@@ -32,7 +32,7 @@
+ android:scheme="authenticate-with-oauth" />
diff --git a/authenticate-with-oauth/src/main/java/com/esri/arcgismaps/sample/authenticatewithoauth/MainActivity.kt b/authenticate-with-oauth/src/main/java/com/esri/arcgismaps/sample/authenticatewithoauth/MainActivity.kt
index bbdaec534..e4f2e9a47 100644
--- a/authenticate-with-oauth/src/main/java/com/esri/arcgismaps/sample/authenticatewithoauth/MainActivity.kt
+++ b/authenticate-with-oauth/src/main/java/com/esri/arcgismaps/sample/authenticatewithoauth/MainActivity.kt
@@ -47,14 +47,14 @@ class MainActivity : AppCompatActivity() {
// to view the traffic layer in the portal, you must enter valid ArcGIS Online credentials.
private val portal by lazy {
- Portal(getString(R.string.auth_portal_url), Portal.Connection.Authenticated)
+ Portal(getString(R.string.oauth_sample_portal_url), Portal.Connection.Authenticated)
}
private val oAuthConfiguration by lazy {
OAuthUserConfiguration(
portalUrl = portal.url,
- clientId = getString(R.string.oauth_client_id),
- redirectUrl = getString(R.string.oauth_redirect_uri)
+ clientId = getString(R.string.oauth_sample_client_id),
+ redirectUrl = getString(R.string.oauth_sample_redirect_uri)
)
}
diff --git a/authenticate-with-oauth/src/main/res/values/strings.xml b/authenticate-with-oauth/src/main/res/values/strings.xml
index 31d2b074a..fc4979194 100644
--- a/authenticate-with-oauth/src/main/res/values/strings.xml
+++ b/authenticate-with-oauth/src/main/res/values/strings.xml
@@ -1,6 +1,6 @@
Authenticate with OAuth
- https://www.arcgis.com/home/item.html?id=e5039444ef3c48b8a8fdc9227f9be7c1
- lgAdHkYZYlwwfAhC
- my-ags-app://auth
+ https://www.arcgis.com/home/item.html?id=e5039444ef3c48b8a8fdc9227f9be7c1
+ lgAdHkYZYlwwfAhC
+ authenticate-with-oauth://auth
diff --git a/display-points-using-clustering-feature-reduction/.gitignore b/display-points-using-clustering-feature-reduction/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/display-points-using-clustering-feature-reduction/README.md b/display-points-using-clustering-feature-reduction/README.md
new file mode 100644
index 000000000..e36314279
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/README.md
@@ -0,0 +1,45 @@
+# Display points using clustering feature reduction
+
+Display a web map with a point feature layer that has feature reduction enabled to aggregate points into clusters.
+
+Map displaying the feature layer with feature reduction property enabled by default:
+![Feature reduction map](display-points-using-clustering-feature-reduction.png)
+
+Popup message displaying the cluster details:
+![Cluster details popup](display-points-using-clustering-feature-reduction-popup.png)
+
+## Use case
+
+Feature clustering can be used to dynamically aggregate groups of points that are within proximity of each other in order to represent each group with a single symbol. Such grouping allows you to see patterns in the data that are difficult to visualize when a layer contains hundreds or thousands of points that overlap and cover each other.
+
+## How to use the sample
+
+Pan and zoom the map to view how clustering is dynamically updated. Disable clustering to view the original point features that make up the clustered elements. When clustering is On, you can click on a clustered geoelement to view aggregated information and summary statistics for that cluster. When clustering is toggled off and you click on the original feature you get access to information about individual power plant features.
+
+## How it works
+
+1. Create a map from a web map `PortalItem`.
+2. Get the cluster enabled layer from the map's operational layers.
+3. Get the `FeatureReduction` from the feature layer and set `isEnabled` value to enable or disable clustering on the feature layer.
+4. When the user clicks on the map, call `identifyLayers` and pass in the map's screen coordinates.
+5. Get the `Popup` and the corresponding `PopupElement` from the resulting `IdentifyLayerResult` and use it to construct a popup output string.
+6. Use `Html.fromHtml` to convert the html tags in the output string to a styled text and display it to the user.
+
+## Relevant API
+
+* FeatureLayer
+* FeatureReduction
+* IdentifyLayerResult
+* Popup
+* PopupElement
+* PopupField
+* Portal
+* PortalItem
+
+## About the data
+
+This sample uses a [web map](https://www.arcgis.com/home/item.html?id=8916d50c44c746c1aafae001552bad23) that displays the Esri [Global Power Plants](https://www.arcgis.com/home/item.html?id=eb54b44c65b846cca12914b87b315169) feature layer with feature reduction enabled. When enabled, the aggregate features symbology shows the color of the most common power plant type, and a size relative to the average plant capacity of the cluster.
+
+## Tags
+
+aggregate, bin, cluster, group, merge, normalize, reduce, summarize
diff --git a/display-points-using-clustering-feature-reduction/README.metadata.json b/display-points-using-clustering-feature-reduction/README.metadata.json
new file mode 100644
index 000000000..3a9cdd383
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/README.metadata.json
@@ -0,0 +1,48 @@
+{
+ "category": "Visualization",
+ "description": "Display a web map with a point feature layer that has feature reduction enabled to aggregate points into clusters.",
+ "formal_name": "DisplayPointsUsingClusteringFeatureReduction",
+ "ignore": false,
+ "images": [
+ "display-points-using-clustering-feature-reduction.png",
+ "display-points-using-clustering-feature-reduction-popup.png"
+ ],
+ "keywords": [
+ "aggregate",
+ "bin",
+ "cluster",
+ "group",
+ "merge",
+ "normalize",
+ "reduce",
+ "summarize",
+ "FeatureLayer",
+ "FeatureReduction",
+ "IdentifyLayerResult",
+ "Popup",
+ "PopupElement",
+ "PopupField",
+ "Portal",
+ "PortalItem"
+ ],
+ "language": "kotlin",
+ "redirect_from": "",
+ "relevant_apis": [
+ "FeatureLayer",
+ "FeatureReduction",
+ "IdentifyLayerResult",
+ "Popup",
+ "PopupElement",
+ "PopupField",
+ "Portal",
+ "PortalItem"
+ ],
+ "snippets": [
+ "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ClusterInfoContent.kt",
+ "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt",
+ "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt"
+ ],
+ "title": "Display points using clustering feature reduction"
+}
diff --git a/display-points-using-clustering-feature-reduction/build.gradle b/display-points-using-clustering-feature-reduction/build.gradle
new file mode 100644
index 000000000..1e324c88a
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/build.gradle
@@ -0,0 +1,48 @@
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ applicationId "com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ buildConfigField("String", "API_KEY", API_KEY)
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$kotlinCompilerExt"
+ }
+
+ namespace 'com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction'
+}
+
+dependencies {
+ // lib dependencies from rootProject build.gradle
+ implementation "androidx.core:core-ktx:$ktxAndroidCore"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ktxLifecycle"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$ktxLifecycle"
+ implementation "androidx.activity:activity-compose:$composeActivityVersion"
+ // Jetpack Compose Bill of Materials
+ implementation platform("androidx.compose:compose-bom:$composeBOM")
+ // Jetpack Compose dependencies
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.material3:material3"
+ implementation "androidx.compose.ui:ui-tooling"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation project(path: ':samples-lib')
+}
diff --git a/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction-popup.png b/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction-popup.png
new file mode 100644
index 000000000..835469f58
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction-popup.png differ
diff --git a/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction.png b/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction.png
new file mode 100644
index 000000000..5ce450116
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/display-points-using-clustering-feature-reduction.png differ
diff --git a/display-points-using-clustering-feature-reduction/proguard-rules.pro b/display-points-using-clustering-feature-reduction/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/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/display-points-using-clustering-feature-reduction/src/main/AndroidManifest.xml b/display-points-using-clustering-feature-reduction/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c6647b46d
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt
new file mode 100644
index 000000000..5e1d71dca
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/MainActivity.kt
@@ -0,0 +1,56 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.screens.MainScreen
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // authentication with an API key or named user is
+ // required to access basemaps and other location services
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)
+
+ setContent {
+ SampleAppTheme {
+ FeatureReductionApp()
+ }
+ }
+ }
+
+ @Composable
+ private fun FeatureReductionApp() {
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainScreen(
+ sampleName = getString(R.string.app_name),
+ application = application
+ )
+ }
+ }
+}
diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ClusterInfoContent.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ClusterInfoContent.kt
new file mode 100644
index 000000000..b021b2bed
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ClusterInfoContent.kt
@@ -0,0 +1,103 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleTypography
+
+@Composable
+fun ClusterInfoContent(
+ popupTitle: String,
+ clusterInfoList: MutableList,
+ onDismiss: () -> Unit = { },
+) {
+ Column(Modifier.background(MaterialTheme.colorScheme.background)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(horizontal = 30.dp, vertical = 12.dp)
+ .weight(6f),
+ text = popupTitle,
+ style = SampleTypography.displaySmall
+ )
+
+ IconButton(
+ modifier = Modifier.weight(1f),
+ onClick = onDismiss
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Close,
+ contentDescription = "Close button"
+ )
+ }
+ }
+
+ LazyColumn() {
+ items(clusterInfoList.size) { index ->
+ Divider(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ color = Color.LightGray
+ )
+ Text(
+ modifier = Modifier.padding(horizontal = 30.dp, vertical = 16.dp),
+ text = clusterInfoList[index]
+ )
+ }
+ }
+ Spacer(modifier = Modifier.size(24.dp))
+ }
+}
+
+@Preview(showBackground = true)
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
+@Composable
+fun ClusterInfoContentPreview() {
+ SampleAppTheme {
+ ClusterInfoContent(
+ popupTitle = "Cluster summary",
+ clusterInfoList = mutableListOf(
+ AnnotatedString("This is a cluster description")
+ )
+ )
+ }
+}
diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt
new file mode 100644
index 000000000..759f38519
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/ComposeMapView.kt
@@ -0,0 +1,93 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.LifecycleOwner
+import com.arcgismaps.mapping.view.MapView
+import kotlinx.coroutines.launch
+
+/**
+ * Wraps the MapView in a Composable function.
+ */
+@Composable
+fun ComposeMapView(
+ modifier: Modifier = Modifier,
+ mapViewModel: MapViewModel
+) {
+ // get an instance of the current lifecycle owner
+ val lifecycleOwner = LocalLifecycleOwner.current
+ // collect the latest state of the MapViewState
+ val mapViewState by mapViewModel.mapViewState.collectAsState()
+ // create and add MapView to the activity lifecycle
+ val mapView = createMapViewInstance(lifecycleOwner)
+
+ // wrap the MapView as an AndroidView
+ AndroidView(
+ modifier = modifier,
+ factory = { mapView },
+ // recomposes the MapView on changes in the MapViewState
+ update = { mapView ->
+ mapView.apply {
+ map = mapViewState.arcGISMap
+ setViewpoint(mapViewState.viewpoint)
+ }
+ }
+ )
+
+ // launch coroutine functions in the composition's CoroutineContext
+ LaunchedEffect(Unit) {
+ launch {
+ mapView.onSingleTapConfirmed.collect {
+ mapViewModel.dismissBottomSheet()
+ // call identifyLayers when a tap event occurs
+ val identifyResult = mapView.identifyLayers(it.screenCoordinate, 3.0, false)
+ mapViewModel.handleIdentifyResult(identifyResult)
+ }
+ }
+ launch {
+ mapView.onPan.collect{
+ mapViewModel.dismissBottomSheet()
+ }
+ }
+ }
+}
+
+/**
+ * Create the MapView instance and add it to the Activity lifecycle
+ */
+@Composable
+fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView {
+ // create the MapView
+ val mapView = MapView(LocalContext.current)
+ // add the side effects for MapView composition
+ DisposableEffect(lifecycleOwner) {
+ lifecycleOwner.lifecycle.addObserver(mapView)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(mapView)
+ }
+ }
+ return mapView
+}
diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt
new file mode 100644
index 000000000..61bd5dfff
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/components/MapViewModel.kt
@@ -0,0 +1,238 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components
+
+import android.app.Application
+import android.graphics.Typeface
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import android.text.style.StyleSpan
+import android.text.style.UnderlineSpan
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.core.text.HtmlCompat
+import androidx.lifecycle.AndroidViewModel
+import com.arcgismaps.LoadStatus
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.PortalItem
+import com.arcgismaps.mapping.Viewpoint
+import com.arcgismaps.mapping.layers.FeatureLayer
+import com.arcgismaps.mapping.popup.FieldsPopupElement
+import com.arcgismaps.mapping.popup.TextPopupElement
+import com.arcgismaps.mapping.view.IdentifyLayerResult
+import com.arcgismaps.portal.Portal
+import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.R
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+class MapViewModel(
+ application: Application,
+ private val sampleCoroutineScope: CoroutineScope
+) : AndroidViewModel(application) {
+ // set the MapView mutable stateflow
+ val mapViewState = MutableStateFlow(MapViewState())
+
+ // create a ViewModel to handle dialog interactions
+ val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel()
+
+ // Flag indicating whether feature reduction is enabled or not
+ val isFeatureReductionEnabled = mutableStateOf(true)
+
+ // Flag to show or dismiss the LoadingDialog
+ val showLoadingDialog = mutableStateOf(false)
+
+ // Flag to show or dismiss the bottom sheet
+ val showClusterSummaryBottomSheet = mutableStateOf(false)
+
+ // Initialize clusterInfoList which holds the popup details
+ val clusterInfoList = mutableStateListOf()
+
+ // the title of the popup result
+ val popupTitle = mutableStateOf("")
+
+ init {
+ // show loading dialog to indicate that the map is loading
+ showLoadingDialog.value = true
+ // load the portal and create a map from the portal item
+ val portalItem = PortalItem(
+ Portal(application.getString(R.string.portal_url)),
+ "8916d50c44c746c1aafae001552bad23"
+ )
+ // set the map to be displayed in the layout's MapView
+ mapViewState.value.arcGISMap = ArcGISMap(portalItem)
+
+ sampleCoroutineScope.launch {
+ mapViewState.value.arcGISMap.load().onSuccess {
+ showLoadingDialog.value = false
+ }
+ }
+ }
+
+ /**
+ `* Toggle the FeatureLayer's featureReduction property
+ */
+ fun toggleFeatureReduction() {
+ val map = mapViewState.value.arcGISMap
+ isFeatureReductionEnabled.value = !isFeatureReductionEnabled.value
+ if (map.loadStatus.value == LoadStatus.Loaded) {
+ map.operationalLayers.forEach { layer ->
+ when (layer) {
+ is FeatureLayer -> {
+ layer.featureReduction?.isEnabled = isFeatureReductionEnabled.value
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+
+ /**
+ * Identify the feature layer results and display the resulting popup element information
+ */
+ fun handleIdentifyResult(result: Result>) {
+ sampleCoroutineScope.launch {
+ result.onSuccess { identifyResultList ->
+ // initialize the string for each tap event resulting in a new identifyResultList
+ clusterInfoList.clear()
+ popupTitle.value = ""
+ identifyResultList.forEach { identifyLayerResult ->
+ val popups = identifyLayerResult.popups
+ popups.forEach { popup ->
+ // set the popup title
+ popupTitle.value = popup.title
+ // show the bottom sheet for the popup content
+ showClusterSummaryBottomSheet.value = true
+ popup.evaluateExpressions().onSuccess {
+ popup.evaluatedElements.forEach { popupElement ->
+ when (popupElement) {
+ is FieldsPopupElement -> {
+ popupElement.fields.forEach { popupField ->
+ // convert popupField.label embedded with html tags using HtmlCompat.fromHtml
+ clusterInfoList.add(
+ HtmlCompat.fromHtml(
+ "${popupField.label}: ${popup.getFormattedValue(popupField)}",
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ ).toAnnotatedString()
+ )
+ }
+ }
+
+ is TextPopupElement -> {
+ // convert popupElement.text message embedded with html tags using HtmlCompat.fromHtml
+ clusterInfoList.add(
+ HtmlCompat.fromHtml(
+ popupElement.text,
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ ).toAnnotatedString()
+ )
+ }
+
+ else -> {
+ clusterInfoList.add(
+ HtmlCompat.fromHtml(
+ "Unsupported popup element: ${popupElement.javaClass.name}",
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ ).toAnnotatedString()
+ )
+ }
+ }
+ }
+ }.onFailure { error ->
+ messageDialogVM.showMessageDialog(
+ title = "Error in evaluating popup expression: ${error.message.toString()}",
+ description = error.cause.toString()
+ )
+ }
+ }
+ }
+ }.onFailure { error ->
+ messageDialogVM.showMessageDialog(
+ title = "Error in identify: ${error.message.toString()}",
+ description = error.cause.toString()
+ )
+ }
+ }
+ }
+
+ /**
+ * Dismiss the bottomsheet
+ */
+ fun dismissBottomSheet() {
+ showClusterSummaryBottomSheet.value = false
+ }
+
+ /**
+ * Helper function which converts a [Spanned] into an [AnnotatedString] trying to keep as much formatting as possible.
+ * [AnnotatedString] is supported in compose via the Text composable
+ *
+ * Currently supports `bold`, `italic`, `underline` and `color`.
+ * More info can be found at:
+ * https://stackoverflow.com/questions/66494838/android-compose-how-to-use-html-tags-in-a-text-view and
+ * https://medium.com/@kevinskrei/annotated-text-in-jetpack-compose-8dc596ed62d
+ */
+ private fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
+ val spanned = this@toAnnotatedString
+ append(spanned.toString())
+ getSpans(0, spanned.length, Any::class.java).forEach { span ->
+ val start = getSpanStart(span)
+ val end = getSpanEnd(span)
+ when (span) {
+ is StyleSpan -> when (span.style) {
+ Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
+ Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
+ Typeface.BOLD_ITALIC -> addStyle(
+ SpanStyle(
+ fontWeight = FontWeight.Bold,
+ fontStyle = FontStyle.Italic
+ ), start, end
+ )
+ }
+
+ is UnderlineSpan -> addStyle(
+ SpanStyle(textDecoration = TextDecoration.Underline),
+ start,
+ end
+ )
+
+ is ForegroundColorSpan -> addStyle(
+ SpanStyle(color = Color(span.foregroundColor)),
+ start,
+ end
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Class that represents the MapView state
+ */
+data class MapViewState(
+ var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight),
+ val viewpoint: Viewpoint = Viewpoint(39.8, -98.6, 10e7)
+)
diff --git a/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt
new file mode 100644
index 000000000..35761a5a9
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/java/com/esri/arcgismaps/sample/displaypointsusingclusteringfeaturereduction/screens/MainScreen.kt
@@ -0,0 +1,118 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.screens
+
+import android.app.Application
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components.ClusterInfoContent
+import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components.ComposeMapView
+import com.esri.arcgismaps.sample.displaypointsusingclusteringfeaturereduction.components.MapViewModel
+import com.esri.arcgismaps.sample.sampleslib.components.BottomSheet
+import com.esri.arcgismaps.sample.sampleslib.components.LoadingDialog
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleTypography
+
+/**
+ * Main screen layout for the sample app
+ */
+@Composable
+fun MainScreen(sampleName: String, application: Application) {
+
+ // coroutineScope that will be cancelled when this call leaves the composition
+ val sampleCoroutineScope = rememberCoroutineScope()
+ // create a ViewModel to handle MapView interactions
+ val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) }
+
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) },
+ content = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ // composable function that wraps the MapView
+ ComposeMapView(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f),
+ mapViewModel = mapViewModel
+ )
+ // Button to enable/disable featureReduction property
+ Row(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = "Feature clustering",
+ style = SampleTypography.bodyMedium
+ )
+ Switch(
+ checked = mapViewModel.isFeatureReductionEnabled.value,
+ onCheckedChange = {
+ mapViewModel.toggleFeatureReduction()
+ })
+ }
+
+ // display a MessageDialog if the sample encounters an error
+ mapViewModel.messageDialogVM.apply {
+ if (dialogStatus) {
+ MessageDialog(
+ title = messageTitle,
+ description = messageDescription,
+ onDismissRequest = ::dismissDialog
+ )
+ }
+ }
+
+ // display a LoadingDialog to indicate the map loading status
+ if (mapViewModel.showLoadingDialog.value) {
+ LoadingDialog(loadingMessage = "Loading map...")
+ }
+ }
+
+ // display a bottom sheet to show popup details
+ BottomSheet(isVisible = mapViewModel.showClusterSummaryBottomSheet.value) {
+ ClusterInfoContent(
+ popupTitle = mapViewModel.popupTitle.value,
+ clusterInfoList = mapViewModel.clusterInfoList,
+ onDismiss = { mapViewModel.dismissBottomSheet() }
+ )
+ }
+ }
+ )
+}
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/drawable-v24/ic_launcher_foreground.xml b/display-points-using-clustering-feature-reduction/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..c7bd21dbd
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/drawable/ic_launcher_background.xml b/display-points-using-clustering-feature-reduction/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..6d8cae103
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-hdpi/ic_launcher.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a2f590828
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-hdpi/ic_launcher_round.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1b5239980
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-mdpi/ic_launcher.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ff10afd6e
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-mdpi/ic_launcher_round.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..115a4c768
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xhdpi/ic_launcher.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dcd3cd808
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..459ca609d
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxhdpi/ic_launcher.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8ca12fe02
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8e19b410a
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b824ebdd4
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4c19a13c2
Binary files /dev/null and b/display-points-using-clustering-feature-reduction/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/display-points-using-clustering-feature-reduction/src/main/res/values/strings.xml b/display-points-using-clustering-feature-reduction/src/main/res/values/strings.xml
new file mode 100644
index 000000000..ed09fca70
--- /dev/null
+++ b/display-points-using-clustering-feature-reduction/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Display points using clustering feature reduction
+ https://www.arcgis.com/
+
diff --git a/edit-feature-attachments/src/main/java/com/esri/arcgismaps/sample/editfeatureattachments/MainActivity.kt b/edit-feature-attachments/src/main/java/com/esri/arcgismaps/sample/editfeatureattachments/MainActivity.kt
index eff0c87c3..416ba0798 100644
--- a/edit-feature-attachments/src/main/java/com/esri/arcgismaps/sample/editfeatureattachments/MainActivity.kt
+++ b/edit-feature-attachments/src/main/java/com/esri/arcgismaps/sample/editfeatureattachments/MainActivity.kt
@@ -186,14 +186,14 @@ class MainActivity : AppCompatActivity() {
val dialog = createLoadingDialog("Fetching attachment data").show()
// create folder /ArcGIS/Attachments in external storage
- val fileDir = File(externalCacheDir?.path + "/Attachments")
+ val fileDir = File(getExternalFilesDir(null)?.path + "/Attachments")
fileDir.mkdirs()
// create the file with the attachment name
val file = File(fileDir, attachment.name)
// file provider URI
val contentUri = FileProvider.getUriForFile(
- applicationContext, getString(R.string.provider_authority), file
+ this, getString(R.string.provider_authority), file
)
// open the file in gallery
val imageIntent = Intent().apply {
diff --git a/edit-feature-attachments/src/main/res/xml/provider_paths.xml b/edit-feature-attachments/src/main/res/xml/provider_paths.xml
index 8d13fa177..8645c23d9 100644
--- a/edit-feature-attachments/src/main/res/xml/provider_paths.xml
+++ b/edit-feature-attachments/src/main/res/xml/provider_paths.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 35978c42b..49e9a889d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Aug 17 15:28:39 PDT 2022
+#Mon Aug 21 14:16:03 PDT 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/identify-layer-features/.gitignore b/identify-layer-features/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/identify-layer-features/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/identify-layer-features/README.md b/identify-layer-features/README.md
new file mode 100644
index 000000000..816e71b8d
--- /dev/null
+++ b/identify-layer-features/README.md
@@ -0,0 +1,33 @@
+# Identify layer features
+
+Identify features in all layers in a map.
+
+![Image of identify layers](identify-layer-features.png)
+
+## Use case
+
+"Identify layers" operation allows users to tap on a map, returning features at that location across multiple layers. Because some layer types have sublayers, the sample recursively counts results for sublayers within each layer.
+
+## How to use the sample
+
+Tap to identify features. A bottom text banner will show all layers with features under the tapped location, as well as the number of features.
+
+## How it works
+
+1. The tapped position is passed to `MapView.identifyLayers(...)` method.
+2. For each `IdentifyLayerResult` in the results, features are counted.
+ * Note: there is one identify result per layer with matching features; if the feature count is 0, that means a sublayer contains the matching features.
+
+## Relevant API
+
+* IdentifyLayerResult
+* IdentifyLayerResult.sublayerResults
+* LayerContent
+
+## Additional information
+
+The GeoView supports two methods of identify: `identifyLayer`, which identifies features within a specific layer and `identifyLayers`, which identifies features for all layers in the current view.
+
+## Tags
+
+identify, recursion, recursive, sublayers
diff --git a/identify-layer-features/README.metadata.json b/identify-layer-features/README.metadata.json
new file mode 100644
index 000000000..8a0b8d3a1
--- /dev/null
+++ b/identify-layer-features/README.metadata.json
@@ -0,0 +1,32 @@
+{
+ "category": "Search and Query",
+ "description": "Identify features in all layers in a map.",
+ "formal_name": "IdentifyLayerFeatures",
+ "ignore": false,
+ "images": [
+ "identify-layer-features.png"
+ ],
+ "keywords": [
+ "identify",
+ "recursion",
+ "recursive",
+ "sublayers",
+ "IdentifyLayerResult",
+ "IdentifyLayerResult.sublayerResults",
+ "LayerContent"
+ ],
+ "language": "kotlin",
+ "redirect_from": "",
+ "relevant_apis": [
+ "IdentifyLayerResult",
+ "IdentifyLayerResult.sublayerResults",
+ "LayerContent"
+ ],
+ "snippets": [
+ "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt",
+ "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt"
+ ],
+ "title": "Identify layer features"
+}
diff --git a/identify-layer-features/build.gradle b/identify-layer-features/build.gradle
new file mode 100644
index 000000000..2a368331a
--- /dev/null
+++ b/identify-layer-features/build.gradle
@@ -0,0 +1,48 @@
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ applicationId "com.esri.arcgismaps.sample.identifylayerfeatures"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ buildConfigField("String", "API_KEY", API_KEY)
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$kotlinCompilerExt"
+ }
+
+ namespace 'com.esri.arcgismaps.sample.identifylayerfeatures'
+}
+
+dependencies {
+ // lib dependencies from rootProject build.gradle
+ implementation "androidx.core:core-ktx:$ktxAndroidCore"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ktxLifecycle"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$ktxLifecycle"
+ implementation "androidx.activity:activity-compose:$composeActivityVersion"
+ // Jetpack Compose Bill of Materials
+ implementation platform("androidx.compose:compose-bom:$composeBOM")
+ // Jetpack Compose dependencies
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.material3:material3"
+ implementation "androidx.compose.ui:ui-tooling"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation project(path: ':samples-lib')
+}
diff --git a/identify-layer-features/identify-layer-features.png b/identify-layer-features/identify-layer-features.png
new file mode 100644
index 000000000..bb990c388
Binary files /dev/null and b/identify-layer-features/identify-layer-features.png differ
diff --git a/identify-layer-features/proguard-rules.pro b/identify-layer-features/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/identify-layer-features/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/identify-layer-features/src/main/AndroidManifest.xml b/identify-layer-features/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c6647b46d
--- /dev/null
+++ b/identify-layer-features/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt
new file mode 100644
index 000000000..8b43d63dc
--- /dev/null
+++ b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/MainActivity.kt
@@ -0,0 +1,56 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.identifylayerfeatures
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.identifylayerfeatures.screens.MainScreen
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // authentication with an API key or named user is
+ // required to access basemaps and other location services
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)
+
+ setContent {
+ SampleAppTheme {
+ IdentifyLayerFeaturesApp()
+ }
+ }
+ }
+
+ @Composable
+ private fun IdentifyLayerFeaturesApp() {
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainScreen(
+ sampleName = getString(R.string.app_name),
+ application = application
+ )
+ }
+ }
+}
diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt
new file mode 100644
index 000000000..7bec430bb
--- /dev/null
+++ b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/ComposeMapView.kt
@@ -0,0 +1,89 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.identifylayerfeatures.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.LifecycleOwner
+import com.arcgismaps.mapping.view.MapView
+
+/**
+ * Wraps the MapView in a Composable function.
+ */
+@Composable
+fun ComposeMapView(
+ modifier: Modifier = Modifier,
+ mapViewModel: MapViewModel
+) {
+ // get an instance of the current lifecycle owner
+ val lifecycleOwner = LocalLifecycleOwner.current
+ // collect the latest state of the MapViewState
+ val mapViewState by mapViewModel.mapViewState.collectAsState()
+ // create and add MapView to the activity lifecycle
+ val mapView = createMapViewInstance(lifecycleOwner)
+
+ // wrap the MapView as an AndroidView
+ AndroidView(
+ modifier = modifier,
+ factory = { mapView },
+ // recomposes the MapView on changes in the MapViewState
+ update = { mapView ->
+ mapView.apply {
+ map = mapViewState.arcGISMap
+ setViewpoint(mapViewState.viewpoint)
+ }
+ }
+ )
+
+ // launch coroutine functions in the composition's CoroutineContext
+ LaunchedEffect(Unit) {
+ mapView.onSingleTapConfirmed.collect {
+ // call identifyLayers when a tap event occurs
+ val identifyResult = mapView.identifyLayers(
+ screenCoordinate = it.screenCoordinate,
+ tolerance = 12.0,
+ returnPopupsOnly = false,
+ maximumResults = 10
+ )
+ mapViewModel.handleIdentifyResult(identifyResult)
+ }
+ }
+}
+
+/**
+ * Create the MapView instance and add it to the Activity lifecycle
+ */
+@Composable
+fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView {
+ // create the MapView
+ val mapView = MapView(LocalContext.current)
+ // add the side effects for MapView composition
+ DisposableEffect(lifecycleOwner) {
+ lifecycleOwner.lifecycle.addObserver(mapView)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(mapView)
+ }
+ }
+ return mapView
+}
diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt
new file mode 100644
index 000000000..e1b216712
--- /dev/null
+++ b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/components/MapViewModel.kt
@@ -0,0 +1,147 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.identifylayerfeatures.components
+
+import android.app.Application
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.AndroidViewModel
+import com.arcgismaps.data.ServiceFeatureTable
+import com.arcgismaps.geometry.Point
+import com.arcgismaps.geometry.SpatialReference
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.Viewpoint
+import com.arcgismaps.mapping.layers.ArcGISMapImageLayer
+import com.arcgismaps.mapping.layers.FeatureLayer.Companion.createWithFeatureTable
+import com.arcgismaps.mapping.view.IdentifyLayerResult
+import com.esri.arcgismaps.sample.identifylayerfeatures.R
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+class MapViewModel(
+ application: Application,
+ private val sampleCoroutineScope: CoroutineScope
+) : AndroidViewModel(application) {
+ // set the MapView mutable stateflow
+ val mapViewState = MutableStateFlow(MapViewState())
+
+ // create a ViewModel to handle dialog interactions
+ val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel()
+
+ // string text to display the identify layer results
+ val bottomTextBanner = mutableStateOf("Tap on the map to identify feature layers")
+
+ init {
+ // create a feature layer of damaged property data
+ val featureTable = ServiceFeatureTable(application.getString(R.string.damage_assessment))
+ val featureLayer = createWithFeatureTable(featureTable)
+
+ // create a layer with world cities data
+ val mapImageLayer = ArcGISMapImageLayer(application.getString(R.string.world_cities))
+ sampleCoroutineScope.launch {
+ mapImageLayer.load().onSuccess {
+ mapImageLayer.apply {
+ subLayerContents.value[1].isVisible = false
+ subLayerContents.value[2].isVisible = false
+ }
+ }.onFailure { error ->
+ // show the message dialog and pass the error message to be displayed in the dialog
+ messageDialogVM.showMessageDialog(error.message.toString(), error.cause.toString())
+ }
+ }
+
+ // create a topographic map
+ val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
+ // add world cities layer
+ operationalLayers.add(mapImageLayer)
+ // add damaged property data
+ operationalLayers.add(featureLayer)
+ }
+ // assign the map to the map view
+ mapViewState.value.arcGISMap = map
+ }
+
+ /**
+ * Identify the feature layer results and display the resulting information
+ */
+ fun handleIdentifyResult(result: Result>) {
+ sampleCoroutineScope.launch {
+ result.onSuccess { identifyResultList ->
+ val message = StringBuilder()
+ var totalCount = 0
+ identifyResultList.forEach { identifyLayerResult ->
+ val geoElementsCount = geoElementsCountFromResult(identifyLayerResult)
+ val layerName = identifyLayerResult.layerContent.name
+ message.append(layerName).append(": ").append(geoElementsCount)
+
+ // add new line character if not the final element in array
+ if (identifyLayerResult != identifyResultList[identifyResultList.size - 1]) {
+ message.append("\n")
+ }
+ totalCount += geoElementsCount
+ }
+ // if any elements were found show the results, else notify user that no elements were found
+ if (totalCount > 0) {
+ bottomTextBanner.value = "Number of elements found:\n${message}"
+ } else {
+ bottomTextBanner.value = "Number of elements found: N/A"
+ messageDialogVM.showMessageDialog(
+ title = "No element found",
+ description = "Tap an area on the map with visible features"
+ )
+ }
+ }.onFailure { error ->
+ messageDialogVM.showMessageDialog(
+ title = "Error identifying results: ${error.message.toString()}",
+ description = error.cause.toString()
+ )
+ }
+ }
+ }
+
+ /**
+ * Gets a count of the GeoElements in the passed result layer.
+ * This method recursively calls itself to descend into sublayers and count their results.
+ * @param result from a single layer.
+ * @return the total count of GeoElements.
+ */
+ private fun geoElementsCountFromResult(result: IdentifyLayerResult): Int {
+ var subLayerGeoElementCount = 0
+ for (sublayerResult in result.sublayerResults) {
+ // recursively call this function to accumulate elements from all sublayers
+ subLayerGeoElementCount += geoElementsCountFromResult(sublayerResult)
+ }
+ return subLayerGeoElementCount + result.geoElements.size
+ }
+}
+
+/**
+ * Data class that represents the MapView state
+ */
+data class MapViewState(
+ var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight),
+ var viewpoint: Viewpoint = Viewpoint(
+ center = Point(
+ x = -10977012.785807,
+ y = 4514257.550369,
+ spatialReference = SpatialReference(wkid = 3857)
+ ),
+ scale = 68015210.0
+ )
+)
diff --git a/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt
new file mode 100644
index 000000000..e81ad166d
--- /dev/null
+++ b/identify-layer-features/src/main/java/com/esri/arcgismaps/sample/identifylayerfeatures/screens/MainScreen.kt
@@ -0,0 +1,86 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.identifylayerfeatures.screens
+
+import android.app.Application
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.esri.arcgismaps.sample.identifylayerfeatures.components.ComposeMapView
+import com.esri.arcgismaps.sample.identifylayerfeatures.components.MapViewModel
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+
+/**
+ * Main screen layout for the sample app
+ */
+@Composable
+fun MainScreen(sampleName: String, application: Application) {
+ // coroutineScope that will be cancelled when this call leaves the composition
+ val sampleCoroutineScope = rememberCoroutineScope()
+ // create a ViewModel to handle MapView interactions
+ val mapViewModel = remember { MapViewModel(application, sampleCoroutineScope) }
+
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) },
+ content = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ ) {
+ // composable function that wraps the MapView
+ ComposeMapView(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f)
+ .animateContentSize(),
+ mapViewModel = mapViewModel
+ )
+ // Bottom text to display the identify results
+ Row(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxWidth()
+ .animateContentSize()
+ ) {
+ Text(text = mapViewModel.bottomTextBanner.value)
+ }
+ // display a dialog if the sample encounters an error
+ mapViewModel.messageDialogVM.apply {
+ if (dialogStatus) {
+ MessageDialog(
+ title = messageTitle,
+ description = messageDescription,
+ onDismissRequest = ::dismissDialog
+ )
+ }
+ }
+ }
+ }
+ )
+}
diff --git a/identify-layer-features/src/main/res/drawable-v24/ic_launcher_foreground.xml b/identify-layer-features/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..c7bd21dbd
--- /dev/null
+++ b/identify-layer-features/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/identify-layer-features/src/main/res/drawable/ic_launcher_background.xml b/identify-layer-features/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..6d8cae103
--- /dev/null
+++ b/identify-layer-features/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/identify-layer-features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/identify-layer-features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/identify-layer-features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/identify-layer-features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/identify-layer-features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/identify-layer-features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/identify-layer-features/src/main/res/mipmap-hdpi/ic_launcher.png b/identify-layer-features/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a2f590828
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-hdpi/ic_launcher_round.png b/identify-layer-features/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1b5239980
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-mdpi/ic_launcher.png b/identify-layer-features/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ff10afd6e
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-mdpi/ic_launcher_round.png b/identify-layer-features/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..115a4c768
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-xhdpi/ic_launcher.png b/identify-layer-features/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dcd3cd808
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/identify-layer-features/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..459ca609d
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-xxhdpi/ic_launcher.png b/identify-layer-features/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8ca12fe02
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/identify-layer-features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8e19b410a
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/identify-layer-features/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b824ebdd4
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/identify-layer-features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/identify-layer-features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4c19a13c2
Binary files /dev/null and b/identify-layer-features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/identify-layer-features/src/main/res/values/strings.xml b/identify-layer-features/src/main/res/values/strings.xml
new file mode 100644
index 000000000..5a6b8c777
--- /dev/null
+++ b/identify-layer-features/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+ Identify layer features
+
+ https://sampleserver6.arcgisonline.com/arcgis/rest/services/DamageAssessment/FeatureServer/0
+
+
+ https://sampleserver6.arcgisonline.com/arcgis/rest/services/SampleWorldCities/MapServer
+
+
diff --git a/samples-lib/src/main/java/com/esri/arcgismaps/sample/sampleslib/components/BottomSheet.kt b/samples-lib/src/main/java/com/esri/arcgismaps/sample/sampleslib/components/BottomSheet.kt
new file mode 100644
index 000000000..fa2e5985f
--- /dev/null
+++ b/samples-lib/src/main/java/com/esri/arcgismaps/sample/sampleslib/components/BottomSheet.kt
@@ -0,0 +1,52 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.sampleslib.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+/**
+ * Composable component used to display a custom bottom sheet for samples.
+ * The bottom sheet can display any @Composable content passed to the [bottomSheetContent],
+ * and the visibility can be toggled using [isVisible]
+ */
+@Composable
+fun BottomSheet(
+ isVisible: Boolean,
+ bottomSheetContent: @Composable() () -> Unit
+) {
+ BoxWithConstraints(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ AnimatedVisibility(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ visible = isVisible,
+ enter = slideInVertically{height -> height} + fadeIn(),
+ exit = slideOutVertically{height -> height} + fadeOut()
+ ) {
+ bottomSheetContent()
+ }
+ }
+}
diff --git a/show-callout/.gitignore b/show-callout/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/show-callout/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/show-callout/README.md b/show-callout/README.md
new file mode 100644
index 000000000..90268ac74
--- /dev/null
+++ b/show-callout/README.md
@@ -0,0 +1,32 @@
+# Show callout
+
+Show a callout with the latitude and longitude of user-tapped points.
+
+![Show Callout App](show-callout.png)
+
+## Use case
+
+Callouts are used to display temporary detail content on a map. You can display text and arbitrary UI controls in callouts.
+
+## How to use the sample
+
+Tap anywhere on the map. A callout showing the WGS84 coordinates for the tapped point will appear.
+
+## How it works
+
+1. When the user taps, get the tapped location(map point) from the `SingleTapConfirmedEvent`.
+2. Project the point's geometry to WGS84 using `GeometryEngine.projectOrNull(mapPoint, SpatialReference.wgs84())`.
+3. Create a new Android TextView object and set its text to the coordinate string from the point.
+4. Show the `Callout` on the map view using `mapView.callout.show()` which takes the above created View and WGS84 point as parameters.
+5. Center the map on the tapped location using `mapView.setViewpointCenter`.
+
+## Relevant API
+
+* Callout
+* GeometryEngine
+* MapView
+* Point
+
+## Tags
+
+balloon, bubble, callout, flyout, flyover, info window, popup, tap
diff --git a/show-callout/README.metadata.json b/show-callout/README.metadata.json
new file mode 100644
index 000000000..e2f1371c5
--- /dev/null
+++ b/show-callout/README.metadata.json
@@ -0,0 +1,38 @@
+{
+ "category": "Maps",
+ "description": "Show a callout with the latitude and longitude of user-tapped points.",
+ "formal_name": "ShowCallout",
+ "ignore": false,
+ "images": [
+ "show-callout.png"
+ ],
+ "keywords": [
+ "balloon",
+ "bubble",
+ "callout",
+ "flyout",
+ "flyover",
+ "info window",
+ "popup",
+ "tap",
+ "Callout",
+ "GeometryEngine",
+ "MapView",
+ "Point"
+ ],
+ "language": "kotlin",
+ "redirect_from": "",
+ "relevant_apis": [
+ "Callout",
+ "GeometryEngine",
+ "MapView",
+ "Point"
+ ],
+ "snippets": [
+ "src/main/java/com/esri/arcgismaps/sample/showcallout/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/showcallout/components/ComposeMapView.kt",
+ "src/main/java/com/esri/arcgismaps/sample/showcallout/components/MapViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/showcallout/screens/MainScreen.kt"
+ ],
+ "title": "Show callout"
+}
diff --git a/show-callout/build.gradle b/show-callout/build.gradle
new file mode 100644
index 000000000..acdf36f1f
--- /dev/null
+++ b/show-callout/build.gradle
@@ -0,0 +1,48 @@
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ applicationId "com.esri.arcgismaps.sample.showcallout"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ buildConfigField("String", "API_KEY", API_KEY)
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$kotlinCompilerExt"
+ }
+
+ namespace 'com.esri.arcgismaps.sample.showcallout'
+}
+
+dependencies {
+ // lib dependencies from rootProject build.gradle
+ implementation "androidx.core:core-ktx:$ktxAndroidCore"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ktxLifecycle"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$ktxLifecycle"
+ implementation "androidx.activity:activity-compose:$composeActivityVersion"
+ // Jetpack Compose Bill of Materials
+ implementation platform("androidx.compose:compose-bom:$composeBOM")
+ // Jetpack Compose dependencies
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.material3:material3"
+ implementation "androidx.compose.ui:ui-tooling"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation project(path: ':samples-lib')
+}
diff --git a/show-callout/proguard-rules.pro b/show-callout/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/show-callout/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/show-callout/show-callout.png b/show-callout/show-callout.png
new file mode 100644
index 000000000..ed7650a81
Binary files /dev/null and b/show-callout/show-callout.png differ
diff --git a/show-callout/src/main/AndroidManifest.xml b/show-callout/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c6647b46d
--- /dev/null
+++ b/show-callout/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/MainActivity.kt b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/MainActivity.kt
new file mode 100644
index 000000000..8d777cb37
--- /dev/null
+++ b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/MainActivity.kt
@@ -0,0 +1,56 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.showcallout
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.showcallout.screens.MainScreen
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // authentication with an API key or named user is
+ // required to access basemaps and other location services
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)
+
+ setContent {
+ SampleAppTheme {
+ ShowCalloutApp()
+ }
+ }
+ }
+
+ @Composable
+ private fun ShowCalloutApp() {
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainScreen(
+ sampleName = getString(R.string.app_name),
+ application = application
+ )
+ }
+ }
+}
diff --git a/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/components/ComposeMapView.kt b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/components/ComposeMapView.kt
new file mode 100644
index 000000000..c4d4904b9
--- /dev/null
+++ b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/components/ComposeMapView.kt
@@ -0,0 +1,101 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.showcallout.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.arcgismaps.mapping.view.MapView
+import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
+import kotlinx.coroutines.launch
+
+/**
+ * Wraps the MapView in a Composable function.
+ */
+@Composable
+fun ComposeMapView(
+ modifier: Modifier = Modifier,
+ mapViewModel: MapViewModel,
+ onSingleTap: (SingleTapConfirmedEvent) -> Unit = {}
+) {
+ // get an instance of the current lifecycle owner
+ val lifecycleOwner = LocalLifecycleOwner.current
+ // collect the latest state of the MapViewState
+ val mapViewState by mapViewModel.mapViewState.collectAsState()
+ // create and add MapView to the activity lifecycle
+ val mapView = createMapViewInstance(lifecycleOwner).apply {
+ map = mapViewState.arcGISMap
+ setViewpoint(mapViewState.viewpoint)
+ // enable animated callout
+ callout.isAnimationEnabled = true
+ }
+
+ // wrap the MapView as an AndroidView
+ AndroidView(
+ modifier = modifier,
+ factory = { mapView },
+ // recomposes the MapView on changes in the MapViewState
+ update = { mapView ->
+ mapView.apply {
+ val latlonPoint = mapViewModel.latLonPoint
+ latlonPoint?.let {
+ // show callout at the tapped location using the set View
+ callout.show(
+ mapViewModel.calloutContent,
+ latlonPoint
+ )
+ lifecycleOwner.lifecycleScope.launch {
+ // center the map on the tapped location
+ setViewpointCenter(latlonPoint)
+ }
+ }
+ }
+ }
+ )
+
+ // launch coroutine functions in the composition's CoroutineContext
+ LaunchedEffect(Unit) {
+ mapView.onSingleTapConfirmed.collect {
+ onSingleTap(it)
+ }
+ }
+}
+
+/**
+ * Create the MapView instance and add it to the Activity lifecycle
+ */
+@Composable
+fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView {
+ // create the MapView
+ val mapView = MapView(LocalContext.current)
+ // add the side effects for MapView composition
+ DisposableEffect(lifecycleOwner) {
+ lifecycleOwner.lifecycle.addObserver(mapView)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(mapView)
+ }
+ }
+ return mapView
+}
diff --git a/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/components/MapViewModel.kt b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/components/MapViewModel.kt
new file mode 100644
index 000000000..d151f2218
--- /dev/null
+++ b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/components/MapViewModel.kt
@@ -0,0 +1,66 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.showcallout.components
+
+import android.app.Application
+import android.widget.TextView
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.AndroidViewModel
+import com.arcgismaps.geometry.GeometryEngine
+import com.arcgismaps.geometry.Point
+import com.arcgismaps.geometry.SpatialReference
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.Viewpoint
+import com.esri.arcgismaps.sample.showcallout.R
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class MapViewModel(private val application: Application) : AndroidViewModel(application) {
+ // set the MapView mutable stateflow
+ val mapViewState = MutableStateFlow(MapViewState(application))
+ // View to show callout
+ var calloutContent: TextView by mutableStateOf(TextView(application))
+ // initialize lat long point
+ var latLonPoint: Point? by mutableStateOf(null)
+
+ fun onMapTapped(mapPoint: Point?) {
+ // get map point from the Single tap event
+ mapPoint?.let { point ->
+ // convert the point to WGS84 for obtaining lat/lon format
+ latLonPoint = GeometryEngine.projectOrNull(
+ point,
+ SpatialReference.wgs84()
+ ) as Point
+ // set the callout text to display point coordinates
+ calloutContent.text = application.getString(
+ R.string.callout_text,
+ latLonPoint?.y,
+ latLonPoint?.x
+ )
+ }
+ }
+}
+
+/**
+ * Data class that represents the MapView state
+ */
+data class MapViewState(val application: Application) {
+ var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight)
+ var viewpoint: Viewpoint = Viewpoint(34.056295, -117.195800, 1000000.0)
+}
diff --git a/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/screens/MainScreen.kt b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/screens/MainScreen.kt
new file mode 100644
index 000000000..3b8c2c3d4
--- /dev/null
+++ b/show-callout/src/main/java/com/esri/arcgismaps/sample/showcallout/screens/MainScreen.kt
@@ -0,0 +1,58 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.showcallout.screens
+
+import android.app.Application
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+import com.esri.arcgismaps.sample.showcallout.components.ComposeMapView
+import com.esri.arcgismaps.sample.showcallout.components.MapViewModel
+
+/**
+ * Main screen layout for the sample app
+ */
+@Composable
+fun MainScreen(sampleName: String, application: Application) {
+ // create a ViewModel to handle MapView interactions
+ val mapViewModel = MapViewModel(application)
+
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) },
+ content = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ ) {
+ // composable function that wraps the MapView
+ ComposeMapView(
+ modifier = Modifier.fillMaxSize(),
+ mapViewModel = mapViewModel,
+ onSingleTap = { singleTapConfirmedEvent ->
+ mapViewModel.onMapTapped(singleTapConfirmedEvent.mapPoint)
+ }
+
+ )
+ }
+ }
+ )
+}
diff --git a/show-callout/src/main/res/drawable-v24/ic_launcher_foreground.xml b/show-callout/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..c7bd21dbd
--- /dev/null
+++ b/show-callout/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/show-callout/src/main/res/drawable/ic_launcher_background.xml b/show-callout/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..6d8cae103
--- /dev/null
+++ b/show-callout/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/show-callout/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/show-callout/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/show-callout/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/show-callout/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/show-callout/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/show-callout/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/show-callout/src/main/res/mipmap-hdpi/ic_launcher.png b/show-callout/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a2f590828
Binary files /dev/null and b/show-callout/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/show-callout/src/main/res/mipmap-hdpi/ic_launcher_round.png b/show-callout/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1b5239980
Binary files /dev/null and b/show-callout/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/show-callout/src/main/res/mipmap-mdpi/ic_launcher.png b/show-callout/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ff10afd6e
Binary files /dev/null and b/show-callout/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/show-callout/src/main/res/mipmap-mdpi/ic_launcher_round.png b/show-callout/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..115a4c768
Binary files /dev/null and b/show-callout/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/show-callout/src/main/res/mipmap-xhdpi/ic_launcher.png b/show-callout/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dcd3cd808
Binary files /dev/null and b/show-callout/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/show-callout/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/show-callout/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..459ca609d
Binary files /dev/null and b/show-callout/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/show-callout/src/main/res/mipmap-xxhdpi/ic_launcher.png b/show-callout/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8ca12fe02
Binary files /dev/null and b/show-callout/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/show-callout/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/show-callout/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8e19b410a
Binary files /dev/null and b/show-callout/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/show-callout/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/show-callout/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b824ebdd4
Binary files /dev/null and b/show-callout/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/show-callout/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/show-callout/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4c19a13c2
Binary files /dev/null and b/show-callout/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/show-callout/src/main/res/values/strings.xml b/show-callout/src/main/res/values/strings.xml
new file mode 100644
index 000000000..9c8df94f8
--- /dev/null
+++ b/show-callout/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Show callout
+ "Lat: %.4f, Lon: %.4f"
+
diff --git a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt
index 757a8c067..66e95e015 100644
--- a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt
+++ b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/ComposeMapView.kt
@@ -18,17 +18,12 @@ package com.esri.arcgismaps.sample.showmagnifier.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.LifecycleOwner
import com.arcgismaps.mapping.view.MapView
-import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
-import kotlinx.coroutines.launch
/**
* Wraps the MapView in a Composable function.
diff --git a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt
index 094fa8af3..f92e11575 100644
--- a/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt
+++ b/show-magnifier/src/main/java/com/esri/arcgismaps/sample/showmagnifier/components/MapViewModel.kt
@@ -17,15 +17,11 @@
package com.esri.arcgismaps.sample.showmagnifier.components
import android.app.Application
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.view.MapViewInteractionOptions
-import kotlinx.coroutines.flow.MutableStateFlow
class MapViewModel(application: Application) : AndroidViewModel(application) {
// get an instance of the MapView state
diff --git a/show-portal-user-info/README.md b/show-portal-user-info/README.md
new file mode 100644
index 000000000..44323a149
--- /dev/null
+++ b/show-portal-user-info/README.md
@@ -0,0 +1,42 @@
+# Show portal user info
+
+Retrieve a user's details via a Portal.
+
+![Image of access portal user info](show-portal-user-info.png)
+
+## Use case
+
+This portal information can be used to provide a customized UI experience for the user. For example, you can show a thumbnail of the user in the application to indicate that they are currently logged in. Additionally, apps such as ArcGIS Field Maps use this functionality to integrate with Portal.
+
+## How to use the sample
+
+Enter your ArcGIS Online credentials for the specified URL.
+
+## How it works
+
+1. On startup, the app presents the user with an editable text field containing a portal URL.
+2. Upon pressing the "Load" button, a portal will be created and loaded.
+3. If the portal is secured, it may potentially issue an authentication challenge.
+4. If the portal is successfully loaded, the info screen below will display the portal info, otherwise it will display the loading error.
+5. Upon successful login, get a `PortalUser` using `portal.user`. Get user attributes using:
+ * `portalUser.portalName`
+ * `portalUser.fullName`
+ * `portalUser.email`
+ * `portalUser.creationDate`
+ * `portalUser.thumbnail.image`
+6. The "Sign out" button clears any saved credentials.
+
+## Relevant API
+
+* OAuthUserConfiguration
+* Portal
+* PortalInfo
+* PortalUser
+
+## About the data
+
+This sample signs into your ArcGIS online account and displays the user's profile information.
+
+## Tags
+
+account, avatar, bio, cloud and portal, email, login, picture, profile, user, username
diff --git a/show-portal-user-info/README.metadata.json b/show-portal-user-info/README.metadata.json
new file mode 100644
index 000000000..131ab8255
--- /dev/null
+++ b/show-portal-user-info/README.metadata.json
@@ -0,0 +1,39 @@
+{
+ "category": "Cloud and Portal",
+ "description": "Retrieve a user's details via a Portal.",
+ "formal_name": "ShowPortalUserInfo",
+ "ignore": false,
+ "images": [
+ "show-portal-user-info.png"
+ ],
+ "keywords": [
+ "account",
+ "avatar",
+ "bio",
+ "cloud and portal",
+ "email",
+ "login",
+ "picture",
+ "profile",
+ "user",
+ "username",
+ "OAuthUserConfiguration",
+ "Portal",
+ "PortalInfo",
+ "PortalUser"
+ ],
+ "language": "kotlin",
+ "redirect_from": "",
+ "relevant_apis": [
+ "OAuthUserConfiguration",
+ "Portal",
+ "PortalInfo",
+ "PortalUser"
+ ],
+ "snippets": [
+ "src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/components/AppViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/screens/MainScreen.kt"
+ ],
+ "title": "Show portal user info"
+}
diff --git a/show-portal-user-info/build.gradle b/show-portal-user-info/build.gradle
new file mode 100644
index 000000000..f9ce23f71
--- /dev/null
+++ b/show-portal-user-info/build.gradle
@@ -0,0 +1,52 @@
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ applicationId "com.esri.arcgismaps.sample.showportaluserinfo"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ buildConfigField("String", "API_KEY", API_KEY)
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$kotlinCompilerExt"
+ }
+
+ namespace 'com.esri.arcgismaps.sample.showportaluserinfo'
+}
+
+dependencies {
+ // lib dependencies from rootProject build.gradle
+ implementation "androidx.core:core-ktx:$ktxAndroidCore"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ktxLifecycle"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$ktxLifecycle"
+ implementation "androidx.activity:activity-compose:$composeActivityVersion"
+ // Jetpack Compose Bill of Materials
+ implementation platform("androidx.compose:compose-bom:$composeBOM")
+ // Jetpack Compose dependencies
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.material3:material3"
+ implementation "androidx.compose.ui:ui-tooling"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation "androidx.browser:browser:$androidBrowserVersion"
+ implementation project(path: ':samples-lib')
+ // Toolkit dependencies
+ implementation(platform("com.esri:arcgis-maps-kotlin-toolkit-bom:$arcgisToolkitVersion"))
+ implementation('com.esri:arcgis-maps-kotlin-toolkit-authentication')
+}
diff --git a/show-portal-user-info/proguard-rules.pro b/show-portal-user-info/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/show-portal-user-info/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/show-portal-user-info/show-portal-user-info.png b/show-portal-user-info/show-portal-user-info.png
new file mode 100644
index 000000000..416b7d3d5
Binary files /dev/null and b/show-portal-user-info/show-portal-user-info.png differ
diff --git a/show-portal-user-info/src/main/AndroidManifest.xml b/show-portal-user-info/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..0c2c9df0b
--- /dev/null
+++ b/show-portal-user-info/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/MainActivity.kt b/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/MainActivity.kt
new file mode 100644
index 000000000..10fecc3c7
--- /dev/null
+++ b/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/MainActivity.kt
@@ -0,0 +1,56 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.showportaluserinfo
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.showportaluserinfo.screens.MainScreen
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // authentication with an API key or named user is
+ // required to access basemaps and other location services
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)
+
+ setContent {
+ SampleAppTheme {
+ ShowPortalUserInfoApp()
+ }
+ }
+ }
+
+ @Composable
+ private fun ShowPortalUserInfoApp() {
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainScreen(
+ sampleName = getString(R.string.app_name),
+ application = application
+ )
+ }
+ }
+}
diff --git a/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/components/AppViewModel.kt b/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/components/AppViewModel.kt
new file mode 100644
index 000000000..a9fa9119b
--- /dev/null
+++ b/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/components/AppViewModel.kt
@@ -0,0 +1,124 @@
+/*
+ *
+ * Copyright 2023 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.esri.arcgismaps.sample.showportaluserinfo.components
+
+import android.app.Application
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.arcgismaps.ArcGISEnvironment
+import com.arcgismaps.httpcore.authentication.OAuthUserConfiguration
+import com.arcgismaps.portal.Portal
+import com.arcgismaps.toolkit.authentication.AuthenticatorState
+import com.arcgismaps.toolkit.authentication.signOut
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
+import com.esri.arcgismaps.sample.showportaluserinfo.R
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class AppViewModel(private val application: Application) : AndroidViewModel(application) {
+
+ val authenticatorState: AuthenticatorState = AuthenticatorState()
+
+ // create a ViewModel to handle dialog interactions as a public val
+ val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel()
+
+ private val noPortalInfoText = application.getString(R.string.no_portal_info)
+ private val startInfoText = application.getString(R.string.start_info_text)
+ private val arcGISUrl = application.getString(R.string.portal_url)
+ private val oAuthUserConfiguration = OAuthUserConfiguration(
+ arcGISUrl,
+ // This client ID is for sample purposes only. For use of the Authenticator in your own app,
+ // create your own client ID. For more info see: https://developers.arcgis.com/documentation/mapping-apis-and-services/security/tutorials/register-your-application/
+ application.getString(R.string.oauth_client_id),
+ application.getString(R.string.oauth_redirect_uri)
+ )
+
+ private val _portalUserName = MutableStateFlow(String())
+ val portalUserName: StateFlow = _portalUserName.asStateFlow()
+
+ private val _emailID = MutableStateFlow(String())
+ val emailID: StateFlow = _emailID.asStateFlow()
+
+ private val _userCreationDate = MutableStateFlow(String())
+ val userCreationDate: StateFlow = _userCreationDate.asStateFlow()
+
+ private val _portalName = MutableStateFlow(String())
+ val portalName: StateFlow = _portalName.asStateFlow()
+
+ private val defaultBitmap = BitmapFactory.decodeResource(application.resources, R.drawable.user)
+
+ private val _userThumbnail: MutableStateFlow = MutableStateFlow(defaultBitmap)
+ val userThumbnail: StateFlow = _userThumbnail.asStateFlow()
+
+ private val _infoText: MutableStateFlow = MutableStateFlow(startInfoText)
+ val infoText: StateFlow = _infoText.asStateFlow()
+
+ private val _isLoading: MutableStateFlow = MutableStateFlow(false)
+ val isLoading: StateFlow = _isLoading.asStateFlow()
+
+ private val _url: MutableStateFlow = MutableStateFlow(arcGISUrl)
+ val url: StateFlow = _url.asStateFlow()
+ fun setUrl(newUrl: String) {
+ _url.value = newUrl
+ }
+
+ fun signOut() = viewModelScope.launch {
+ _isLoading.value = true
+ ArcGISEnvironment.authenticationManager.signOut()
+ _infoText.value = startInfoText
+ _isLoading.value = false
+ _portalUserName.value = ""
+ _emailID.value = ""
+ _userCreationDate.value = ""
+ _portalName.value = ""
+ _userThumbnail.value = defaultBitmap
+ }
+
+ fun loadPortal() = viewModelScope.launch {
+ _isLoading.value = true
+ authenticatorState.oAuthUserConfiguration = oAuthUserConfiguration
+ val portal = Portal(url.value, Portal.Connection.Authenticated)
+ portal.load().also {
+ _isLoading.value = false
+ }.onFailure {
+ messageDialogVM.showMessageDialog(application.getString(R.string.load_portal_fail), it.message.toString())
+ }.onSuccess {
+ portal.portalInfo?.apply {
+ _portalUserName.value = this.user?.fullName ?: noPortalInfoText
+ _emailID.value = this.user?.email ?: noPortalInfoText
+ _portalName.value = this.portalName ?: noPortalInfoText
+ // get the created date
+ val date = Date.from(this.user?.creationDate)
+ val dateFormat = SimpleDateFormat("dd-MMM-yyyy", Locale.US)
+ _userCreationDate.value = dateFormat.format(date)
+ this.user?.thumbnail?.load()?.onSuccess {
+ _userThumbnail.value = this.user?.thumbnail?.image?.bitmap ?: defaultBitmap
+ }
+ }
+ _infoText.value = application.getString(R.string.load_portal_success)
+ }
+ }
+}
diff --git a/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/screens/MainScreen.kt b/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/screens/MainScreen.kt
new file mode 100644
index 000000000..33262bd5f
--- /dev/null
+++ b/show-portal-user-info/src/main/java/com/esri/arcgismaps/sample/showportaluserinfo/screens/MainScreen.kt
@@ -0,0 +1,256 @@
+/* Copyright 2023 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.esri.arcgismaps.sample.showportaluserinfo.screens
+
+import android.app.Application
+import android.graphics.Bitmap
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.arcgismaps.toolkit.authentication.AuthenticatorState
+import com.arcgismaps.toolkit.authentication.DialogAuthenticator
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+import com.esri.arcgismaps.sample.showportaluserinfo.components.AppViewModel
+
+/**
+ * Main screen layout for the sample app
+ */
+@Composable
+fun MainScreen(sampleName: String, application: Application) {
+
+ val appViewModel = viewModel { AppViewModel(application) }
+ val authenticatorState: AuthenticatorState = appViewModel.authenticatorState
+
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) },
+ content = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ ) {
+ val infoText = appViewModel.infoText.collectAsState().value
+ val isLoading = appViewModel.isLoading.collectAsState().value
+ PortalDetails(
+ url = appViewModel.url.collectAsState().value,
+ onSetUrl = appViewModel::setUrl,
+ onSignOut = appViewModel::signOut,
+ onLoadPortal = appViewModel::loadPortal
+ )
+ InfoScreen(
+ infoText = infoText,
+ username = appViewModel.portalUserName.collectAsState().value,
+ email = appViewModel.emailID.collectAsState().value,
+ creationDate = appViewModel.userCreationDate.collectAsState().value,
+ portalName = appViewModel.portalName.collectAsState().value,
+ userThumbnail = appViewModel.userThumbnail.collectAsState().value,
+ isLoading = isLoading
+ )
+ // display a dialog if the sample encounters an error
+ appViewModel.messageDialogVM.apply {
+ if (dialogStatus) {
+ MessageDialog(
+ title = messageTitle,
+ description = messageDescription,
+ onDismissRequest = ::dismissDialog
+ )
+ }
+ }
+ }
+ DialogAuthenticator(authenticatorState = authenticatorState)
+ }
+ )
+}
+
+/**
+ * Allows the user to enter a [url] and load a portal.
+ * It uses OAuth under the hood, and has a button to clear credentials.
+ *
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun PortalDetails(
+ url: String,
+ onSetUrl: (String) -> Unit,
+ onSignOut: () -> Unit,
+ onLoadPortal: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ horizontalAlignment = CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ // The Url text field
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = url,
+ onValueChange = onSetUrl,
+ label = { Text("Portal URL") },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Uri,
+ imeAction = ImeAction.Go
+ ),
+ keyboardActions = KeyboardActions(onAny = { onLoadPortal() }),
+ singleLine = true
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
+ // Clear credential button
+ Button(
+ onClick = {
+ onSignOut()
+ keyboardController?.hide()
+ focusManager.clearFocus()
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary)
+ ) {
+ Text(text = "Sign out")
+ }
+ // Load button
+ Button(
+ onClick = {
+ onLoadPortal()
+ keyboardController?.hide()
+ focusManager.clearFocus()
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
+ ) {
+ Text(text = "Load portal")
+ }
+ }
+ }
+}
+
+/**
+ * Displays messages to the user. This may be used to display instructions, portal info, or error messages.
+ *
+ */
+@Composable
+private fun InfoScreen(
+ infoText: String,
+ username: String,
+ email: String,
+ creationDate: String,
+ portalName: String,
+ userThumbnail: Bitmap,
+ isLoading: Boolean
+) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .padding(8.dp),
+ ) {
+ LazyColumn {
+ item {
+ Box(Modifier.fillMaxWidth()) {
+ if (isLoading) CircularProgressIndicator(
+ Modifier.align(Center).padding(10.dp)
+ )
+ else Text(
+ modifier = Modifier.align(Center).padding(10.dp),
+ textAlign = TextAlign.Center,
+ text = infoText
+ )
+ }
+ Divider()
+ Row(
+ modifier = Modifier
+ .padding(10.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Image(
+ bitmap = userThumbnail.asImageBitmap(),
+ contentDescription = "User Thumbnail",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .clip(CircleShape)
+ .size(150.dp)
+ )
+ }
+ Divider()
+ Row(modifier = Modifier.padding(10.dp)) {
+ Text(text = "Username: ", fontWeight = FontWeight.Bold)
+ Text(text = username)
+ }
+ Divider()
+ Row(modifier = Modifier.padding(10.dp)) {
+ Text(text = "E-mail: ", fontWeight = FontWeight.Bold)
+ Text(text = email)
+ }
+ Divider()
+ Row(modifier = Modifier.padding(10.dp)) {
+ Text(text = "Member Since: ", fontWeight = FontWeight.Bold)
+ Text(text = creationDate)
+ }
+ Divider()
+
+ Row(modifier = Modifier.padding(10.dp)) {
+ Text(text = "Portal Name: ", fontWeight = FontWeight.Bold)
+ Text(text = portalName)
+ }
+ Divider()
+ }
+ }
+ }
+}
+
+
diff --git a/show-portal-user-info/src/main/res/drawable-v24/ic_launcher_foreground.xml b/show-portal-user-info/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..c7bd21dbd
--- /dev/null
+++ b/show-portal-user-info/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/show-portal-user-info/src/main/res/drawable/ic_launcher_background.xml b/show-portal-user-info/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..6d8cae103
--- /dev/null
+++ b/show-portal-user-info/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/show-portal-user-info/src/main/res/drawable/user.png b/show-portal-user-info/src/main/res/drawable/user.png
new file mode 100644
index 000000000..d0eacbfe5
Binary files /dev/null and b/show-portal-user-info/src/main/res/drawable/user.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/show-portal-user-info/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/show-portal-user-info/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/show-portal-user-info/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/show-portal-user-info/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/show-portal-user-info/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/show-portal-user-info/src/main/res/mipmap-hdpi/ic_launcher.png b/show-portal-user-info/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a2f590828
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-hdpi/ic_launcher_round.png b/show-portal-user-info/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1b5239980
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-mdpi/ic_launcher.png b/show-portal-user-info/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ff10afd6e
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-mdpi/ic_launcher_round.png b/show-portal-user-info/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..115a4c768
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-xhdpi/ic_launcher.png b/show-portal-user-info/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dcd3cd808
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/show-portal-user-info/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..459ca609d
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-xxhdpi/ic_launcher.png b/show-portal-user-info/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8ca12fe02
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/show-portal-user-info/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8e19b410a
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/show-portal-user-info/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b824ebdd4
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/show-portal-user-info/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/show-portal-user-info/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4c19a13c2
Binary files /dev/null and b/show-portal-user-info/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/show-portal-user-info/src/main/res/values/strings.xml b/show-portal-user-info/src/main/res/values/strings.xml
new file mode 100644
index 000000000..ca6f55eb5
--- /dev/null
+++ b/show-portal-user-info/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+
+ Show portal user info
+ rR5K4TEyhkPDcTkf
+ my-ags-app://auth
+
+ Enter a valid portal url to load it and see the portal details displayed here.
+
+ Portal loaded successfully but no portal info was found.
+ The Portal is loaded successfully. Please check the Portal details below
+ Failed to load portal
+ https://www.arcgis.com
+
+
diff --git a/version.gradle b/version.gradle
index 1f998b656..fc2be58e0 100644
--- a/version.gradle
+++ b/version.gradle
@@ -1,24 +1,26 @@
ext {
// ArcGIS Maps SDK for Kotlin version
- arcgisVersion = '200.2.0'
+ arcgisVersion = '200.3.0'
+ // ArcGIS Maps SDK for Kotlin Toolkit version
+ arcgisToolkitVersion = '200.3.0'
// SDK versions
compileSdkVersion = 33
minSdkVersion = 26
targetSdkVersion = 33
- versionCode = 2002000
- versionName = '200.2.0'
+ versionCode = 2003000
+ versionName = '200.3.0'
// Kotlin versions
- kotlinVersion = '1.8.21'
+ kotlinVersion = '1.9.10'
ktxLifecycle = '2.5.1'
- ktxFragmentsExt = '1.5.4'
- ktxActivityExt = '1.6.1'
- ktxAndroidCore = '1.9.0'
- kotlinCompilerExt = '1.4.7'
+ ktxFragmentsExt = '1.6.1'
+ ktxActivityExt = '1.7.2'
+ ktxAndroidCore = '1.10.1'
+ kotlinCompilerExt = '1.5.3'
// Compose versions
composeActivityVersion = '1.7.1'
composeBOM = '2023.05.01'
// Library versions
- appcompatVersion = '1.5.1'
+ appcompatVersion = '1.6.1'
constraintLayoutVersion = '2.1.4'
workVersion = '2.7.1'
multidexVersion = '2.0.1'
@@ -26,7 +28,7 @@ ext {
recyclerViewVersion = '1.1.0'
androidBrowserVersion = '1.5.0'
// Plugin versions
- gradleVersion = '8.0.0'
+ gradleVersion = '8.1.1'
// Java version
javaVersion = 1.17
}