Android app with Places

Location-based AR

In this tutorial we are going through all necessary steps to create a native app for Android that makes use of Onirix Places for Android.

The goal of this tutorial is to create an application that runs on your AR compatible device and allows you to place Points of Interest on a map to create outdoor experiences with location-based Augmented Reality (AR).

Setup and tools

Let's go over the tools we are going to need:

  1. Onirix Studio: the base for our AR project.
  2. Android Studio: the official IDE for Android development. It includes everything you need to build Android apps.
  3. Places Android Library: this library allows us to create Onirix Places experiences for Android.

Code

The complete source code for this example is avaible on Github, inside the onirix-examples repository.

Step 1: Create a new Places project with Onirix Studio

We start with creating a new project in Onirix Studio. In the following video we show you how to create a new project and how to add Points of Interest using our map creation tool.

Step 2: Create a new project with Android Studio

The next step is to download and install Android Studio. To create a new project, run Android Studio and select “Start a new Android Studio project”.

Create project in Android_Studio_S1

Choose project for “Phone and Tablet” with “Empty Activity”.

Create project in Android_Studio_S2

Assign a name and package name, save the location and set a minimum API level (we recommend level 19 or higher). Make sure that you select “User AndroidX artifacts”, which provides us with the functionality of the AndroidX library. Click “Finish” to create the project.

Create project in Android_Studio_S3

Step 3: Include the Onirix Places library for Android

To import the Places library, we have to edit the following files:

Configuration_gradle_files

build.gradle (Project: name_project)

We have to add a custom maven repository to "_build.gradle (Project: nameproject)". The repository should contain the following items. Please note that the order is important for dependency resolution.

allprojects {
   repositories {
       google()
       jcenter()
       maven {
           url 'http://archiva.neosentec.com/repository/internal/'
       }
   }
}

build.gradle (Module: app)

In "build.gradle (Module: app)" we need to include:

apply plugin: 'com.android.application'

android {
   compileSdkVersion 28
   defaultConfig {
       applicationId "com.onirix.myfirstplaces"
       minSdkVersion 19
       targetSdkVersion 28
       versionCode 1
       versionName "1.0"
   }
   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   dataBinding {
       enabled = true
   }
   compileOptions {
       targetCompatibility 1.8
       sourceCompatibility 1.8
   }
}

dependencies {
   implementation fileTree(dir: 'libs', include: ['*.jar'])
   implementation 'androidx.appcompat:appcompat:1.0.2'
   implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
   implementation 'com.onirix:places:1.1.0'
}

Step 4: Create the Android app

Now, we have everything we need to create our first Android application. In the next few steps, we are going to edit a number of files.

MainActivity.java

In "MainActivity" we need to manage the permissions for the camera and the users's location. This is necessary for the application to work.

private List<String> permissionsToRequest;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       boolean hasPermissions = arePermissionsGranted(android.Manifest.permission.CAMERA,
               Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION);

       if (hasPermissions) {
           startActivity(new Intent(this, ARMapActivity.class));
       } else {
           requestPermissions();
       }
   }

   public boolean arePermissionsGranted(String... permissions) {

       permissionsToRequest = new ArrayList<>();

       for (String permission : permissions) {
           if (!(ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED)) {
               permissionsToRequest.add(permission);
           }
       }

       return permissionsToRequest.size() == 0;
   }

   public void requestPermissions() {
       ActivityCompat.requestPermissions(this, permissionsToRequest.toArray(new String[] {}), 1);
   }

Once permission is granted, we can access the AR Activity we are going to implement in the next step.

@Override
   public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
       if (grantResults.length > 0) {
           int deniedPermissions = 0;
           for (int grantResult : grantResults) {
               if (grantResult == PackageManager.PERMISSION_DENIED) {
                   deniedPermissions++;
               }
           }

           if (deniedPermissions > 0) {
               requestPermissions();
           } else {
               startActivity(new Intent(this, ARMapActivity.class));
           }
       } else {
           requestPermissions();
       }
   }

ARMapActivity.java

Our AR map is defined as an Activity. For this tutorial, we create “ARMapActivity” which is extended from "com.onirix.places.ui.MapActivity".

This makes the following listening and callback methods available to us:

  • onMapLoaded: this method is called when the map is loaded. If there is any problem, map will be null and an error message will be returned.
  • onLocationUpdate: this method is called when the location is being updated by the GPS or the Network provider.
  • onPlaceTouched: this method is called whenever a POI is touched.
  • onFocusChanged: this method is called whenever a POI enters the crosshair in the middle of the screen.
@Override
public void onMapLoaded(Map map, String error) {}

@Override
public void onLocationUpdate(Location location) {}

@Override
public void onPlaceTouched(@NonNull PlaceARWrapper place) {}

@Override
public void onFocusChanged(@Nullable PlaceARWrapper newFocus) {}

Whenever an Activity extends from MapActivity, we have to initialize the configuration of the screen, the map and POIs. We also need to call the method "loadOnirixMap" inside “onCreate”:

private static final String TOKEN = "<VALUTE_TOKEN>";
private static final String MAP_OID = "<VALUTE_MAP_OID>";

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);

   getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

   MapConfig mapConfig = new MapConfig(this)
           .withVisionDistance(100)
           .withShowRadar(true)
           .withRadarDistance(100)
           .withUseUpdateRadius(true)
           .withUpdateRatio(0.5f)
           .withUpdateRadius(2 * 100);

   PlaceConfig placeConfig = new PlaceConfig(this)
           .withShowDistance(true)
           .withShowMarker(true)
           .withShowName(true);

   mapConfig.setConfigForCategory("default", placeConfig);
   loadOnirixMap(MAP_OID, TOKEN, mapConfig);
}

Let's take a look at this step-by-step:

Screen

This application must keep the screen on in order to display the map.

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

Map configuration

This class allows us to configure different display options for the map. In this example, we show a radar, distances etc.. For further information, we recommend reading Map Configuration within the Places SDK for Android.

MapConfig mapConfig = new MapConfig(this)
       .withVisionDistance(100)
       .withShowRadar(true)
       .withRadarDistance(100)
       .withUseUpdateRadius(true)
       .withUpdateRatio(0.5f)
       .withUpdateRadius(2 * 100);

Places (POI) configuration

This class allows us to configure different display options for POIs. In this example, we show the radar distance etc.. For further information, we recommend reading Places Configuration within the Places SDK for Android.

PlaceConfig placeConfig = new PlaceConfig(this)
           .withShowDistance(true)
           .withShowMarker(true)
           .withShowName(true);

Load Onirix Map

A call to "loadOnirixMap" allows us to load the map we want to display in our application.

private static final String TOKEN = "<VALUE_TOKEN>";
private static final String MAP_OID = "<VALUE_MAP_OID>";
……
loadOnirixMap(MAP_OID, AUTH_TOKEN, mapConfig);

We need to set some parameters:

  • Map Oid
  • Token
  • Map Config

To obtain the MAP_OID, perform the following steps:

To optain the TOKEN, copy it from the project card menu:

AndroidManifest.xml

In the last step, we open the "AndroidManifest.xml" and change the default theme of our application to something like “Theme.AppCompat.Light.DarkActionBar”, that extends from “Theme.AppCompat”.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.onirix.my_first_places">

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
       <activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
       <activity android:name=".ARMapActivity"></activity>
   </application>

</manifest>

Customization

Add a custom icon/logo to your view

We've added an empty ImageView inside the activity layout with the ID: R.id.logoIcon. You can replace this with your own logo or icon which is going to be used to display POIs. For best results, we recommend that your icon/logo is in square format. The following needs to be called inside the onCreate() lifecycle activity method:

ImageView logoIcon = (ImageView) findViewById(R.id.logoIcon);
logoIcon.setImageDrawable(getResources().getDrawable(R.drawable.your_icon))

ProGuard rules

If you want to enable ProGuard in your app, include the following rules:

-keep class com.google.**
-dontwarn com.google.**
-dontwarn okio.**
-dontwarn okhttp3.**
-keep class okhttp3.** { *; }
-dontwarn java.awt.**
-dontwarn android.graphics.**

# Keep native funcitons
-keepclasseswithmembers class * {
    native <methods>;
}

-keep class com.onirix.places_core.model.** { *; }
-keep class com.onirix.places.model.** { *; }

##---------------Begin: proguard configuration for Gson  ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature

# For using GSON @Expose annotation
-keepattributes *Annotation*

# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { *; }

# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
##---------------End: proguard configuration for Gson  ----------

##---------------Begin: proguard configuration for Retrofit  ----------
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod

# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
    @retrofit2.http.* <methods>;
}

# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**

# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit

# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.-KotlinExtensions
-dontwarn retrofit2.Platform$Java8
##---------------End: proguard configuration for Retrofit  ----------

That's it. Congratulations for completing this tutorial! If you ran into any problems, please get in touch with our support. We are always ready to answer your questions!