Android CPIK Libraries
Contents
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
You can download and integrate the CPIK libraries files one of two ways:
- Automated method, using our public repository.
- Manual method, by logging into our Partner Portal.
Automated Method
Android
- Add the following to your project
build.gradle
file.
repositories
{
...
maven
{
url "https://trimblemaps.jfrog.io/artifactory/android"
}
...
}
- Add the following to your module
build.gradle
file.
dependencies {
...
implementation (group: 'com.trimblemaps', name: 'cpik-android', version: '10.XX.X.XXXX', ext: 'aar') //For example, version could be "10.19.4.348"
}
...
}
Manual Method
After downloading and extracting the CoPilot CPIK libraries .zip file from the partner portal, you should have access to everything needed to get up and running quickly.
CPIK libraries takes a library / plugin approach, therefore files and resources need to be included in your Android project. In the Library and Resource folder, copy the aar file to your project, as shown below. (Please note the folder names may vary slightly.)
Copy all files from | Copy all files to |
---|---|
Library and Resources/aar/* | <your project>/app/libs/ |
Note: If the libs folders does not exist in your project, you need to create it.
You should also verify that inside your module’s build.gradle
file is a line that ensures the aar file you just placed is compiled/implemented. A standard Android Studio project contains a line like this in the build.gradle
file:
implementation fileTree(dir: 'libs', include: ['*.aar'])
Adding native libraries when not using the .aar file
Copy all files from | Copy all files to |
---|---|
Library and Resources/native libs/* |
When including CPIK native libraries explicitly in Android Studio, ensure that your project structure reflects the jniLibs
location above. If there appears to be errors related to the libcopilot.so
at runtime, rename your app’s .apk file to .zip and inspect its contents to ensure that libcopilot.so
is present inside the lib
folder.
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" android:foregroundServiceType="location" />
<!-- 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 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_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.WAKE_LOCK"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<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.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<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 CPIK 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:
-
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. -
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 grants 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 CPIK libraries integration disables the ability for a user or app to download the map data over the air (Wi-Fi 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 Wi-Fi connection only. Note: This setting does not apply to Windows laptop versions of CoPilot, which cannot detect if a device is connected to the internet via Wi-Fi or a cellular network.