Skip to content

SDK Libraries Set up Guide

In this guide we are going to create a very simple Android application with a MainActivity.java that displays CoPilot inside a view. We’ll also cover licensing CoPilot using Account Manager credentials. The very first step is to create a new project in Android Studio with a MainActivity.java and the accompanying layout file.

Libraries and Assets

After downloading and extracting the CoPilot SDK Libraries .zip file, you should have access to everything needed to get up and running quickly.

SDK Libraries takes a library / plugin approach, therefore certain files and resources need to be included in your Android project. Below are files that should be copied from the com.alk.copilot.generic_enterprise_cpik folder to their location in your project. (Please note the folder names may vary slightly.)

Copy all files from Copy all files to
Library and Resources/assets/* <​your project>/app/src/main/assets/
Library and Resources/native libs/* <​your project>/app/src/main/jniLibs
Library and Resources/jars/* <​your project>/app/libs/

Note

If the assets/libs/jniLibs folders do not exist in your project, you will need to create them. Additionally, for the jniLibs folder you only need to include the CPU architecture that you need.

You should also verify that inside your module’s build.gradle file is a line that ensures the jar files you just placed are compiled/implemented. A standard Android Studio project will contain a line like this in the build.gradle file:

implementation fileTree(dir: 'libs', include: ['*.jar'])

AndroidManifest.xml

In this section, we’ll cover some changes that are required to the AndroidManifest.xml file, including permissions and some meta tags.

Add the following metadata and service tags inside the <application> element in your manifest:

<service  android:name="com.alk.copilot.CopilotService" android:enabled="true" />
<!-- This should always be "true" -->
<meta-data android:value="true" android:name="ALK_isCustomFragment" />

<!-- "true" if you want CoPilot to store data (maps, speech, etc) in the default install directory
"false" if you want CoPilot to store data using the external storage dir (usually on the sdcard) -->
<meta-data android:value="true" android:name="ALK_useInternalStorage" />

<!-- "true" if you will be starting CoPilot in the background and do not want to show a splash screen. false" otherwise -->
<meta-data android:value="true" android:name="ALK_doBackgroundStart" />
<meta-data android:name="ALK_useOpenGL" android:value="true" />

You will also need to add some additional parameters inside your <manifest> element, a sibling to the <application> element we worked with above.

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="22"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
    <uses-permission android:name="com.android.vending.BILLING"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

Once these changes have been applied, you may need to ask Gradle to do a project sync. Once complete, our AndroidManifest.xml file should look something like this:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.trimble.maps.cpikstarter">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="22" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />
    <uses-permission android:name="com.android.vending.BILLING" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <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/AppTheme">
        <!-- The CoPilot Service -->
        <service android:name="com.alk.copilot.CopilotService" android:enabled="true" />
        <!-- This should always be "true" -->
        <meta-data android:name="ALK_isCustomFragment" android:value="true" />
        <!-- "True" if you want CoPilot to store data (maps, speech etc.) in the default install
         directory. "False" if you want to use external or private storage -->
        <meta-data android:name="ALK_useInternalStorage" android:value="true" />
        <!-- "True" if you will be starting CoPilot in the background and do not want to show a splash screen -->
        <meta-data android:name="ALK_doBackgroundStart" android:value="true" />
        <!-- "True" if you want to make use of OpenGL. Should typically be set to true unless used
        on older, lower spec. devices -->
        <meta-data android:name="ALK_useOpenGL" android:value="true" />
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Binding the Service

Add the following to your MainActivity.java:

private static final int FOREGROUND_SERVICE_ID = 3283;
private CopilotService.CopilotBinder copilotBinder = null;

/**
 * We're going to use this trigger the start of the service, it's going to bind the ServiceConnection
 * we make later in the activity
 */
public void startService() {
    Intent copilotServiceIntent = new Intent(this, CopilotService.class);
    this.bindService(copilotServiceIntent, copilotServiceConnection, Context.BIND_AUTO_CREATE);
}

/**
 * Here we're making a new ServiceConnection and overriding the methods we need to
 */
private ServiceConnection copilotServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        copilotBinder = (CopilotService.CopilotBinder) service;

        /**
         * Android changed the way it handles services in Oreo, so we check for that and adapt
         */
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            copilotBinder.startForeground(FOREGROUND_SERVICE_ID, getNotificationForService());
        } else {
            String channelId = getString(R.string.app_name);
            NotificationCompat.Builder builder = new NotificationCompat.Builder(
                MainActivity.this, channelId);
            builder.setContentTitle(getString(R.string.app_name)).setSmallIcon(R.drawable.ic_launcher_foreground);
            Intent resultIntent = new Intent(MainActivity.this, MainActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this,
                0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT);

            builder.setContentIntent(pendingIntent);
            copilotBinder.startForeground(FOREGROUND_SERVICE_ID, builder.build());
        }

    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

/**
 * For Oreo, we build a notification channel for the CoPilot service.
 * @return
 */
@TargetApi(26)
private synchronized Notification getNotificationForService() {
    NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    String channelId = getString(R.string.app_name);
    NotificationChannel notificationChannel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_DEFAULT);
    notificationChannel.setDescription(channelId);
    notificationChannel.setSound(null, null);

    notificationManager.createNotificationChannel(notificationChannel);
    return new Notification.Builder(this, channelId)
        .setContentTitle(this.getString(R.string.app_name))
        .setSmallIcon(R.drawable.ic_launcher_background)
        .setPriority(Notification.PRIORITY_DEFAULT)
        .build();

}

The above code creates a new ServiceConnection and overrides two of its methods, the most important one being onServiceConnected(). The code here triggers the foreground service and binds CoPilot once the startService() method is called. You’ll also notice the code does a check for the Android version. In Android 8.1 and above, Android changed the way it handles services and as a result we have to create a custom notification channel for our service in this case.

Now that we have our core functionality set up for creating and binding the CoPilot service, we need to ensure we have the necessary permissions before doing so.

Validating Permissions

Add the following code into your MainActivity.java:

public static final int REQUEST_CONST = 3110;

/**
 * Checks each permission to ensure the application has access. Only really need to check the
 * more 'intrusive' permissions like location, but left all in to be consistent with
 * AndroidManifest.xml
 * @return
 */
private boolean hasAllPermissions() {
    String[] permissions = new String[] {
        Manifest.permission.INTERNET,
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS,
            Manifest.permission.ACCESS_NETWORK_STATE,
            Manifest.permission.DISABLE_KEYGUARD,
            Manifest.permission.WAKE_LOCK,
            Manifest.permission.READ_CONTACTS,
            Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 ? Manifest.permission.READ_PHONE_STATE : Manifest.permission.ACCESS_WIFI_STATE,
            Manifest.permission.VIBRATE,
            Manifest.permission.ACCESS_WIFI_STATE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };
    for (int i = 0; i < permissions.length; i++)
        if (ActivityCompat.checkSelfPermission(this, permissions[i]) != PackageManager.PERMISSION_GRANTED)
            return false;
    return true;
}

/**
 * If we do not have all the permissions needed, use the Android run time method for requesting
 * permissions from the user. If permissions are granted, proceed with the setup of SDK Libraries.
 */
public void permissionCheck() {
    if (!hasAllPermissions()) {
        ActivityCompat.requestPermissions(this, new String[] {
                Manifest.permission.INTERNET,
                    Manifest.permission.ACCESS_COARSE_LOCATION,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS,
                    Manifest.permission.ACCESS_NETWORK_STATE,
                    Manifest.permission.DISABLE_KEYGUARD,
                    Manifest.permission.WAKE_LOCK,
                    Manifest.permission.READ_CONTACTS,
                    Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 ? Manifest.permission.READ_PHONE_STATE : Manifest.permission.ACCESS_WIFI_STATE,
                    Manifest.permission.VIBRATE,
                    Manifest.permission.ACCESS_WIFI_STATE,
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
            },
            REQUEST_CONST);
    } else {
        setup();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    setup();
}

With the above we have two main functions:

  1. hasAllPermissions() is a boolean method that checks to see if all permissions are provided for the app, returns true if they are, false if they are not.

  2. permissionCheck() requests permissions from the user if it needs to, otherwise just starts the setup procedure of CoPilot. We will write the setup() method later.

We override the onRequestPermissionsResult() function. When the user grants or denies access when requested, they will return here. For simplicity sake, we’re assuming the user will grant all permissions as intended. However it’s recommended to add code here to handle a user denying permissions.

Controlling CoPilot’s Startup

At this point, the core code for starting the service is written, and the app has the permissions it needs to work with CoPilot itself. In this section we’re going to write a few listeners and hooks to dictate what CoPilot does on its start up. We’ll begin by licensing CoPilot when it starts up then ensuring the software is ready before interacting with it further. Start by adding the following code to you MainActivity.java:

public static final String AMS_USERNAME = "YOUR_USERNAME";
public static final String AMS_COMPANYID = "YOUR_COMPANY_ID";
private class LicenseHookListener extends LicenseListener {

    @Override
    public void onLicenseMgtLogin(LicenseActivationResponse activationStatus, LicenseMgtInfo loginInfo) {
        // This is fired when we have an activation response back from the licensing system
        super.onLicenseMgtLogin(activationStatus, loginInfo);
    }

    @Override
    public LicenseMgtInfo licenseMgtCredentialHook() {
        // This is fired on launch when registered as a hook, it either logs the user in as
        // a new user or if the user is already logged in with these credentials, does nothing.
        return new LicenseMgtInfo(AMS_USERNAME, AMS_COMPANYID);
    }
}

private class StartupListener extends CopilotListener {
    @Override
    public void onCPStartup() {
        displayCopilot();
    }
}

The above are two private classes that extend some of CoPilot’s listeners. The LicenseHookListener is going to be used to license and activate CoPilot. The licenseMgtCredentialHook() method is fired on launch and effectively provides the username and company ID to authenticate. If you’re unfamiliar with Trimble MAPS Account Manager licensing, please speak to your account manager. Additionally, there is a StartupListener that will fire onCPStartup() when CoPilot is ready to be interacted with. There’s a functionside that has not been written yet. We’ll get to that shortly.

Now that the listeners and hooks are in place, it’s time to register them. This should be done before the service is started to ensure they fire during the startup calls - this helps control your application’s workflow. Add the following to your MainActivity.java:

public void setup() {
    LicenseListener.registerHook(new LicenseHookListener());
    LicenseListener.registerListener(new LicenseHookListener());
    CopilotListener.registerListener(new StartupListener());
    startService();
}

The above creates the setup() function, this is called during the permissions check section of the code and basically registers the listeners and hooks with CoPilot, then starts the service.

Lastly, add the permissionCheck() function to your onCreate() for the activity to start the whole process.

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

    permissionCheck();
}

With that the CoPilot service, licensing and startup procedures are all running. However they’re running silently in the background - nothing is being displayed yet.

Display CoPilot

Inside your activity_main.xml file, or whichever layout file you use for MainActivity.java create a view that effectively fills the entire activity.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent" >

   <RelativeLayout
       android:id="@+id/copilotLayout"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" >
   </RelativeLayout>

</FrameLayout>

For the example above, we want CoPilot to take as much of the screen as is available to it. In your application, you may prefer to take a portion of the screen or even rework all the code written above into a fragment instead. However for now, we’ll stick to CoPilot taking the entire view. Once the XML is complete, it’s time to write a function to display CoPilot, add the following to your MainActivity.java:

private void displayCopilot() {
    View copilotView = CopilotMgr.getView();
    ((RelativeLayout) findViewById(R.id.copilotLayout)).addView(copilotView);
}

Finishing Up

With all of that, when you run your app you should see CoPilot display. You will also likely see a popup telling you no map data is found. By default, CoPilot in the SDK Libraries integration disables the ability for a user or app to download the map data over the air (wifi or data). Therefore, if you want to use CoPilot, you will either need to side load the map data on to the device or add a config to enable this feature. If you’re looking to do the latter, create a product.cfg file inside assets/copilot and insert the following values:

[Download]
"WiFiOnly"=1
"PreventDataDownload"=0

The above will enable you to download the maps over the air (you can now make use of the map download APIs too) but it also restricts the download to being on a WiFi connection only.


Last update: August 25, 2020