Skip to content

AR Stickers

There are situations when remote workers in the field need more information about a physical object or environment, such as a complex piece of machinery with many different physical components, for example. There may be relevant data such as descriptions, instructions, and/or video tutorials tied to each of these components. Some of these components may be replaced with new components in the future and require maintenance to keep the data updated. This AR Stickers module is designed to make it easier for a user to create, update, and visualize scenes with relevant information about a specific object or physical environment. AR Stickers supports two modes: edit-mode and view-only-mode.

Architecture

AR Stickers utilizes a handful of tools internally. It uses Android's ARCore and Sceneform libraries to provide the AR experience, androidx.navigation for navigating through fragments, and Retrofit library for network calls. These components are combined and maintained using the architecture design practices described by Android's guide to app architecture.

AR Stickers Architecture

Getting Started with AR Stickers

Gradle Set Up

Add the following to your project-level Gradle file:

dependencies {
    classpath 'com.google.ar.sceneform:plugin:1.17.1'
    classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5'
}

Also, add the following to your app-level Gradle file:

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation project(path: ':fiori-ar')
    implementation project(path: ':fiori')
    implementation 'com.sap.cloud.android:foundation:4.0.0'

    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

    def navigation_version = "2.4.0"
    implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
    implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
    implementation "androidx.navigation:navigation-runtime-ktx:$navigation_version"
}

Create an Application to Initialize the ArSceneRepository

ArSceneRepository is an interface that plays as the single source of truth for the AR Stickers module. The client application may create an implementation of this interface or may use the default ArSceneRepositoryImpl provided by the SDK. The ArSceneRepository is platform-agnostic as it is not Android-specific.

We will be using the default ArSceneRepositoryImpl implementation in this example. We need an Application class to instantiate an instance of ArSceneRepositoryImpl. The important method to note here is ArSceneServiceLocator.provideArSceneRepository(), as it takes three parameters: the application context, a CoroutineScope to handle tasks outside the Fragment lifecycle, and an OAuth2Config to help with authentication. All parameters in OAuth2Config are type String.

In your Application file, add the following code to create an instance of the repository layer:

import android.app.Application
import android.graphics.Bitmap
import com.sap.cloud.mobile.fiori.ar.data.ArSceneRepositoryImpl
import com.sap.cloud.mobile.fiori.ar.util.ArSceneServiceLocator
import com.sap.cloud.mobile.fiori.ar.network.OAuth2Config
import com.sap.cloud.mobile.foundation.authentication.AppLifecycleCallbackHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob

class ClientApplication : Application() {

    private var oAuth2Config : OAuth2Config = OAuth2Config(BASE_URL, appID, appVersion, clientId, redirectUrl, authUrl, tokenUrl, requestURL)

    val arAuthoringDataRepo: ArSceneRepositoryImpl<Bitmap>
        get() = ArSceneServiceLocator.provideArSceneRepository(this, CoroutineScope(SupervisorJob()), oAuth2Config)

    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(AppLifecycleCallbackHandler.getInstance())
    }
}

Create a MainActivity

Create an Activity to host your NavHostFragment:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Also create the associated activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <fragment
            android:id="@+id/nav_host"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/ar_stickers_demo_navigation"/>
    </FrameLayout>

</layout>

Create a Navigation Graph

Next we need to create a navigation graph. Here is an example called ar_stickers_demo_navigation.xml:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@id/client_fragment">
    <fragment
        android:id="@+id/client_fragment"
        android:name="com.sap.cloud.mobile.fiori.demo.arstickers.ClientFragment"
        tools:layout="@layout/fragment_ar_scene_list"/>
</navigation>

Now we are ready to implement edit-mode and/or view-only-mode.

Edit-Mode Example Implementation

The main entry point for edit-mode is EditImagesPagerFragment. Client applications are only required to navigate to this fragment to get started. The rest of the flow is handled internally. Use any Fragment in your current application as a starting point to take the user into an edit-mode.

In ar_stickers_demo_navigation.xml add the following line below the fragment element. This line allows the current navigation graph to navigate to a sub-navigation graph called navigation_ar_stickers_edit_mode, provided by the SDK:

<include app:graph="@navigation/navigation_ar_stickers_edit_mode" />

Here we have a Fragment named ClientFragment that uses a Button to navigate the user to the EditImagesPagerFragment. The button instantiates a navigation Action named actionClientFragmentToEditMode (which is already defined in ar_stickers_demo_navigation.xml). We also have to let the repository layer know that we have selected a scene by calling selectScene(Int, Bool), which takes an integer parameter as the scene ID and a Boolean indicating whether we are going to edit-mode. Note that the SDK does not know about any existing scenes on the server. It is up to the client application to provide these scene IDs (as integers) or pass -1 as the argument to create a new scene:

import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.AppCompatButton
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment

class ClientFragment : Fragment() {

  val viewModel = ...

  override fun onCreateView(
       inflater: LayoutInflater,
       container: ViewGroup?,
       savedInstanceState: Bundle?
    ): View {
        val view : View = inflater.inflate(R.layout.fragment_client, null)
        val button = view.findViewById<AppCompatButton>(R.id.button)
        button.text = "Create or Edit"
        button.setOnClickListener {
            // Pass in a known scene Id here to edit a scene, or pass -1 to create a new scene.
            navigateToCreateOrEditArScene(-1)
        }
        return view
    }

    private fun navigateToCreateOrEditArScene(arSceneId: Int) {
        val direction = ClientFragmentDirections.actionClientFragmentToEditMode()
        // Make sure to let the repository layer know about the selected scene, as well
        // as passing a Boolean value in the second parameter (true if editing, false if view-only).
        viewModel.repository.selectScene(arScene.id, true)
        NavHostFragment.findNavController(this).navigate(direction)
    }
}

Also create the associated XML:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

View-Only-Mode Example Implementation

The main entry point for view-only-mode is ScanGuideFragment. Client applications are only required to navigate to this fragment to get started. The rest of the flow is handled internally. Use any Fragment in your current application as a starting point to take the user into view-only-mode.

In ar_stickers_demo_navigation.xml, add the following line below the fragment element. This line allows the current navigation graph to navigate to a sub-navigation graph called navigation_ar_stickers_viewonly_mode.xml, provided by the SDK:

<include app:graph="@navigation/navigation_ar_stickers_viewonly_mode" />

As in the previous example, we can use the same ClientFragment with minor changes (the XML file remains the same as that of the previous example):

import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.AppCompatButton
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment

class ClientFragment : Fragment() {

  val viewModel = ...

  // Example scene id.
  var arSceneId = 3820239043

  override fun onCreateView(
      inflater: LayoutInflater,
      container: ViewGroup?,
      savedInstanceState: Bundle?
    ): View {
        val view : View = inflater.inflate(R.layout.fragment_client, null)
        val button = view.findViewById<AppCompatButton>(R.id.button)
        button.text = "View Scene"
        button.setOnClickListener {
            // Pass in your known scene Id to view a scene.
            navigateToViewArScene(arSceneId)
        }
        return view
    }

    private fun navigateToViewArScene(arSceneId: Int) {
        val direction = ClientFragmentDirections.actionClientFragmentToViewonlyMode()
        // Make sure to let the repository layer know about the selected scene, as well
        // as passing a Boolean value in the second parameter (true if editing, false if view-only).
        viewModel.repository.selectScene(arScene.id, false)
        NavHostFragment.findNavController(this).navigate(direction)
    }
}

End User Experience: How to Create or Edit an AR Scene

The following is a breakdown of the edit-mode user experience for creating, editing, and publishing an AR scene:

  1. Capture or select an image to represent the Reference Anchor
  2. Create AR cards
  3. Enter into AR mode and scan the Reference Image
  4. Add and place anchors near the scanned Reference Image
  5. Preview the anchors
  6. Adjust anchors if needed
  7. Publish/upload

1. Capture or Select an Image to Represent the Reference Anchor

Every AR scene needs exactly one mandatory anchor to be used as a "reference" for all other anchors, as it is treated as the origin (x=0, y=0, z=0) for these anchors to be placed relative to. All the user needs to do in this first step is click on the Capture button to select or capture an image. It is recommended to use an image with lots of distinct recognizable features such as a sticker. This image will be scanned when the user enters AR mode (step 3).

AR Capture Reference Image

2. Create AR Cards

Each anchor in AR (besides the reference anchor) will be associated with an Annotation Card that displays additional data about the physical object that anchor is tied to. Data includes a title, description, URL, language, action text, and a preview image. The user will need to provide this information upon creation.

Create Cards

3. Enter Into AR Mode and Scan the Reference Image

After the user has completed creating the Reference Image and the Annotation Cards, they are then able to enter AR mode to create a scene with that new data. The first thing they will be prompted to do is scan the newly created reference image with their camera (this is all done while in AR mode). A green checkbox should appear when the image is successfully tracked.

AR Scan Reference Image

4. Add and Place Anchors Near the Scanned Reference Image

Now that the Reference Image is being tracked in AR, it is time for the user to add anchors by clicking on the + button at the top end of the screen. Clicking this button will show a BottomSheetDialog containing a CollectionView of the recently created Annotation Cards. The user is able to select an Annotation Card they wish to place in the AR Scene and a blue sphere should appear in the center of the screen. The user is able to move their device around to move the sphere and "drop" it when they feel it is in an appropriate position (they are also able to drag it once it is dropped). They can repeat this process until all Annotation Cards are placed as anchors.

AR Place Anchor

5. Preview

Clicking the Preview button will convert all the spheres into AnnotationMarkers, showing RecyclerView of Annotation Cards at the bottom of the screen. This is what the end-user (such as a field worker) in view-only-mode will also see and interact with. The user can exit preview mode if they wish to continue editing.

AR Edit Anchor PLacement

6. Adjust Anchors if Needed

Clicking the Back button allows the user to adjust the anchor by dragging it with one finger.

7. Publish

Once the user is satisfied with the scene, they are able to publish it to the server.

Tips

For best results, try to keep child anchors no further than a few feet apart for best results. It is not required, but it is recommended, to try to keep the reference image in the camera's view. This ensures more accurate placing with all anchors.

Further Reading

Here are additional sources on the ARCore, AugmentedImage class, Sceneform, Android Navigation Component, and architecture design, respectively:


Last update: October 24, 2022