Sunday, 6 January 2013

Display an SVG inside an ImageView or a Canvas


Objective

Display an SVG inside an ImageView or a Canvas.

SVG are really convenient because you don't have to create an image for each density,  since it can be scaled with no loss of quality.
Here is an example of an App entirely done with SVG :

Issue


The Android framework does not support SVG by default.


Solution

Steps
1. Download the latest version of the svg library here : http://code.google.com/p/svg-android/downloads/list
2. Add the jar to your project by copying it into the libs directory
3. Copy your SVG in your res/raw or assets/ directory
4. Load the SVG and create a Drawable or PictureDrawable depending on your need.


Details
1. Download the latest version of the svg library here: 

This library offers a limited support of SVG and is not maintained anymore.

You can eventually download a fork here which has more features of SVG supported  :
List of new features of this version:

2. Add the jar to your project by copying it into the libs/ directory

If you don't have a libs/ directory, create one at the root of your project and copy the jar inside. In Eclipse, this should automatically add it to your build path.

3. Copy your SVG in your res/raw or assets/ directory

You can copy your image directly in the res/raw folder or you can organize them in different directories if you use the assets/ directory.

4. Load the SVG and create a Drawable or PictureDrawable depending on your need. 

If your image is in the raw folder and you want to display it in an ImageView, you can use the following code :

   
        // Parse the SVG file from the resource
        SVG svg = SVGParser.getSVGFromResource(getResources(), R.raw.android);
        // Get a drawable from the parsed SVG and set it as the drawable for the ImageView
        imageView.setImageDrawable(svg.createPictureDrawable());

If your SVG is in the asset directory, you can display it like this :

 
   try {
        // Specify the path (relative to the 'assets' folder)
        final SVG svg = SVGParser.getSVGFromAsset(getAssets(), "android.svg");
        imageView.setImageDrawable(svg.createPictureDrawable());
    } catch (IOException e) {
        // Handle IOException here
    }

If you want to draw your SVG directly on a canvas, you will have to create a Picture instead of a PictureDrawable:
 
    // Parse the SVG file from the resource
    SVG svg = SVGParser.getSVGFromResource(getResources(), R.raw.android);
    // Get the picture
    Picture picture = svg.getPicture();
    // Draw picture in canvas
    // Note: use transforms such as translate, scale and rotate to position the picture correctly
    canvas.drawPicture(picture);

You can get more details on how to use the library on the original documentation: http://code.google.com/p/svg-android/wiki/Tutorial

Saturday, 3 November 2012

Display a Preference Fragment compatible with old devices by using the compatibility library

Objective


Display a Preference Fragment compatible with old devices by using the compatibility library.

Issue


The compatibility library does not contain the PreferenceFragment class, so you can't display preferences inside a fragment.

Solution

Steps
1. Add the compatibility library to your project
2. Add the PreferenceListFragment class to your project
3. Create a fragment that extends the PreferenceListFragment class
4. Create the xml file corresponding to the settings you want to display and add it to the res/xml directory
5. Implements the interface OnPreferenceAttachedListener in your activity
6. Add the settings fragment to your activity
Details
1. Add the compatibility library to your project

If you are using Eclipse, you just have to copy and paste the android-support-v4.jar into the libs directory of your project.

2. Add the PreferenceListFragment class to your project

The original source code has been posted on this forum :
http://forum.xda-developers.com/showthread.php?t=1363906

   
public class PreferenceListFragment extends ListFragment{
    
    private PreferenceManager mPreferenceManager;
    
    /**
     * The starting request code given out to preference framework.
     */
    private static final int FIRST_REQUEST_CODE = 100;
    
    private static final int MSG_BIND_PREFERENCES = 0;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                
                case MSG_BIND_PREFERENCES:
                    bindPreferences();
                    break;
            }
        }
    };
    private ListView lv;
    private int xmlId;
    
    public PreferenceListFragment(int xmlId){
        this.xmlId = xmlId;
    }
    //must be provided
    public PreferenceListFragment(){
        
    }
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle b){
        postBindPreferences();
        return lv;
    }
    
    @Override
    public void onDestroyView(){
        super.onDestroyView();
        ViewParent p = lv.getParent();
        if(p != null)
            ((ViewGroup)p).removeView(lv);
    }

    @Override
    public void onCreate(Bundle b) {
        super.onCreate(b);
        if(b != null)
            xmlId = b.getInt("xml");
        mPreferenceManager = onCreatePreferenceManager();
        lv = (ListView) LayoutInflater.from(getActivity()).inflate(R.layout.preference_list_content, null);
        lv.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
        addPreferencesFromResource(xmlId);
        postBindPreferences();
        ((OnPreferenceAttachedListener)getActivity()).onPreferenceAttached(getPreferenceScreen(), xmlId);
    }

    @Override
    public void onStop(){
        super.onStop();
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
            m.setAccessible(true);
            m.invoke(mPreferenceManager);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        lv = null;
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
            m.setAccessible(true);
            m.invoke(mPreferenceManager);
           }catch(Exception e){
               e.printStackTrace();
           }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putInt("xml", xmlId);
        super.onSaveInstanceState(outState);

    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
            m.setAccessible(true);
            m.invoke(mPreferenceManager, requestCode, resultCode, data);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    /**
     * Posts a message to bind the preferences to the list view.
     * <p>
     * Binding late is preferred as any custom preference types created in
     * {@link #onCreate(Bundle)} are able to have their views recycled.
     */
    private void postBindPreferences() {
        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
    }
    
    private void bindPreferences() {
        final PreferenceScreen preferenceScreen = getPreferenceScreen();
        if (preferenceScreen != null) {
            preferenceScreen.bind(lv);
        }
    }
    
    /**
     * Creates the {@link PreferenceManager}.
     * 
     * @return The {@link PreferenceManager} used by this activity.
     */
    private PreferenceManager onCreatePreferenceManager() {
        try{
            Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
            c.setAccessible(true);
            PreferenceManager preferenceManager = c.newInstance(this.getActivity(), FIRST_REQUEST_CODE);
            return preferenceManager;
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }
    }
    
    /**
     * Returns the {@link PreferenceManager} used by this activity.
     * @return The {@link PreferenceManager}.
     */
    public PreferenceManager getPreferenceManager() {
        return mPreferenceManager;
    }

    /**
     * Sets the root of the preference hierarchy that this activity is showing.
     * 
     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
     */
    public void setPreferenceScreen(PreferenceScreen preferenceScreen){
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
            m.setAccessible(true);
            boolean result = (Boolean) m.invoke(mPreferenceManager, preferenceScreen);
            if (result && preferenceScreen != null) {
                postBindPreferences();
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    
    /**
     * Gets the root of the preference hierarchy that this activity is showing.
     * 
     * @return The {@link PreferenceScreen} that is the root of the preference
     *         hierarchy.
     */
    public PreferenceScreen getPreferenceScreen(){
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
            m.setAccessible(true);
            return (PreferenceScreen) m.invoke(mPreferenceManager);
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }
    }
    
    /**
     * Adds preferences from activities that match the given {@link Intent}.
     * 
     * @param intent The {@link Intent} to query activities.
     */
    public void addPreferencesFromIntent(Intent intent) {
        throw new RuntimeException("too lazy to include this bs");
    }
    
    /**
     * Inflates the given XML resource and adds the preference hierarchy to the current
     * preference hierarchy.
     * 
     * @param preferencesResId The XML resource ID to inflate.
     */
    public void addPreferencesFromResource(int preferencesResId) {   
        try{
            Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
            m.setAccessible(true);
            PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(mPreferenceManager, getActivity(), preferencesResId, getPreferenceScreen());
            setPreferenceScreen(prefScreen);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    
    /**
     * Finds a {@link Preference} based on its key.
     * 
     * @param key The key of the preference to retrieve.
     * @return The {@link Preference} with the key, or null.
     * @see PreferenceGroup#findPreference(CharSequence)
     */
    public Preference findPreference(CharSequence key) {
        if (mPreferenceManager == null) {
            return null;
        }
        return mPreferenceManager.findPreference(key);
    }
    
    public interface OnPreferenceAttachedListener{
        public void onPreferenceAttached(PreferenceScreen root, int xmlId);
    }
    
}

3. Create a fragment that extends the PreferenceListFragment class
   

    public class SettingsFragment extends PreferenceListFragment implements
        SharedPreferences.OnSharedPreferenceChangeListener,
        PreferenceListFragment.OnPreferenceAttachedListener {
    public static final String SHARED_PREFS_NAME = "settings";

    @Override
    public void onCreate(Bundle icicle) {

        super.onCreate(icicle);
        PreferenceManager preferenceManager = getPreferenceManager();
        preferenceManager.setSharedPreferencesName(SHARED_PREFS_NAME);
        addPreferencesFromResource(R.xml.settings);
        preferenceManager.getSharedPreferences()
                .registerOnSharedPreferenceChangeListener(this);

    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
            String key) {
    }

    @Override
    public void onPreferenceAttached(PreferenceScreen root, int xmlId) {
        if (root == null)
            return;
    }
}


4. Create the xml file corresponding to the settings you want to display and add it to the res/xml directory.

Here is an example of how it could look like :
   

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

    <PreferenceCategory android:title="My Settings" >
        <CheckBoxPreference
            android:summary="My option 1"
            android:title="Description of my option 1" />
        
        <CheckBoxPreference
            android:summary="My option 1"
            android:title="Description of my option 2" />
    </PreferenceCategory>
   

</PreferenceScreen>
5. Implements the interface OnPreferenceAttachedListener in your activity
  

Public class MainActivity extends FragmentActivity implements OnPreferenceAttachedListener {

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

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

    @Override
    public void onPreferenceAttached(PreferenceScreen root, int xmlId) {
    }
}
6. Add the settings fragment to your activity

There are several ways to do this, in this example, I am just adding it in the layout of the main activity.

   
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<fragment
    android:id="@+id/myfragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    class="com.fordemobile.preferencelistfragment.SettingsFragment" />

</LinearLayout>

Wednesday, 19 September 2012

Use SMS to send and receive raw data within your App

Objective


Use SMS to send and receive raw data within your App.

Issue


When you want send information with the function SMSManager#sendTextMessage, the sent SMS is stored in the sent messages list, then it triggers a notification when the recipient received this message and the system store it in the inbox in plain text.
With this method, there is not security and it's annoying for the user, because he has to clean up its inbox.

Solution

Steps
1. Send your message on a specific port with the SMSManager#sendDataMessage function
2. Register a BroadcastReceiver on the same port
3. Add the required permissions in your manifest
Details
1. Send your message on a specific port with the SMSManager#sendDataMessage function
   

    public void sendMessage(final byte[] message) {
        //define the phone number
        final String port = "90";
        final String phoneNumber = "5146792345"
        //intent broadcasted when the SMS is sent
        final PendingIntent sendIntent = PendingIntent.getBroadcast(
                this.context, 0, new Intent(0), 0);
        //intent broadcasted when the SMS is received
        final PendingIntent delivery = PendingIntent.getBroadcast(
                this.context, 0, new Intent(0), 0);
        //send data
        final SmsManager smsManager = SmsManager.getDefault();
        smsManager.sendDataMessage(phoneNumber, "", port,
                message, sendIntent, delivery);
    }


2. Register a BroadcastReceiver on the same port
   

        BroadcastReceiver receivedBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                final Bundle bundle = intent.getExtras();
                SmsMessage[] msgs = null;

                if (null != bundle) {
                    final Object[] pdus = (Object[]) bundle.get("pdus");
                    msgs = new SmsMessage[pdus.length];
                    byte[] data = null;                    
                    //read data
                    for (int i = 0; i < msgs.length; i++) {
                        msgs[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
                        data = msgs[i].getUserData();
                    }
                    if (data != null) {
                       //use data
                    }
                }
            }
        };
        //register the receiver
        final String port = "90";
        final IntentFilter intentFilter = new IntentFilter(
                "android.intent.action.DATA_SMS_RECEIVED");
        intentFilter.addDataScheme("sms");
        intentFilter.addDataAuthority("*", port);
        this.context.registerReceiver(this.receivedBroadcastReceiver,
                intentFilter);


3. Add the required permissions in your manifest.
   

    <uses -permission="-permission" android:name="android.permission.SEND_SMS" />
    <uses -permission="-permission" android:name="android.permission.RECEIVE_SMS" />


Wednesday, 8 August 2012

Display an oAuth login webpage in a dialog

Objective


Display the login web page of an oAuth API in a dialog.

Issue


When you want to use an API with oAuth (like Facebook, Twitter, Instagram, ....), you need to redirect the user to the login webpage of the API. Unfortunately it opens the web browser and leaves your application, which might be disturbing for the user.

Solution

Steps
1. Create a layout with a WebView.
2. Create a DialogFragment and loads the previously created layout
3. Create a custom WebViewClient and override the shouldOverrideUrlLoading method to listen when a new URL is loaded and check when the login is successful.
4. Assign this custom WebViewClient to the WebView.
5. Display the dialog from your Activity.
Details
1. Create a layout with a webview oauth_screen.xml
   

<LinearLayout android:layout_gravity="center" android:layout_height="400dp" android:layout_width="320dp" android:minheight="400dp" android:minwidth="320dp" android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android">

    <WebView android:id="@+id/web_oauth" android:layout_height="400dp" android:layout_width="320dp">

</WebView></LinearLayout>


2. Create a DialogFragment and loads the previously created layout
3. Create a custom WebViewClient and override the shouldOverrideUrlLoading method to listen when a new URL is loaded and check when the login is successful.
4. Assign this custom WebViewClient to the WebView.


OAuthFragment.java
   
public class OAuthFragment extends DialogFragment {

    private WebView webViewOauth;

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

    private static class MyWebViewClient extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            //check if the login was successful and the access token returned
            //this test depend of your API
            if (url.contains("access_token=")) {
                //save your token
                saveAccessToken(url);
                return true;
            }
            BaseActivity.logEvent(Consts.EVENT_CALLBACK + "Login Failed", true);
            return false;
        }
    }

    private void saveAccessToken(String url) {
        // extract the token if it exists
        String paths[] = url.split("access_token=");
        if (paths.length > 1) {
            ApplicationData.getInstance().setAccessToken(paths[1]);
            final SharedPreferences sharedPreferences = getActivity()
                    .getSharedPreferences(Consts.SHARED_PREFS_NAME, 0);
            final Editor edit = sharedPreferences.edit();
            edit.putString(Consts.KEY_ACCESS_TOKEN, paths[1]);
            edit.putBoolean(Consts.KEY_IS_LOGGED_OUT, false);
            edit.commit();

            Intent intent2 = new Intent(getActivity(), WallScreenActivity.class);
            intent2.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent2);
            return;
        }
    }

    @Override
    public void onViewCreated(View arg0, Bundle arg1) {
        super.onViewCreated(arg0, arg1);
        try {
            //load the url of the oAuth login page
            webViewOauth
                    .loadUrl("https://xxxx.com/oauth/authorize/?");
            //set the web client
            webViewOauth.setWebViewClient(new MyWebViewClient());
            //activates JavaScript (just in case)
            WebSettings webSettings = webViewOauth.getSettings();
            webSettings.setJavaScriptEnabled(true);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        //Retrieve the webview
        View v = inflater.inflate(R.layout.oauth_screen, container, false);
        webViewOauth = (WebView) v.findViewById(R.id.web_oauth);
        getDialog().setTitle("Use your Instagram account");
        return v;
    }
}

MyActivity.java
 
    //Display the oAuth web page in a dialog
    void showDialog() {
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.addToBackStack(null);

        // Create and show the dialog.
        OAuthFragment newFragment = new OAuthFragment();
        newFragment.show(ft, "dialog");
    }

Thursday, 28 June 2012

Use a MapView inside a fragment

Objective


Use a MapView inside a fragment.

Issue


An exception is thrown when using a layout with a MapView inside it :
Caused by: java.lang.IllegalArgumentException: MapViews can only be created inside instances of MapActivity.


Solution


  • Download android-support-v4-googlemaps
  • Create a libs directory at the root of your project
  • Add android-support-v4-r6-googlemaps.jar to this directory
  • Make your activity extends FragmentActivity instead of activity

Wednesday, 30 May 2012

Obtain your current location

Now that you know how to use Google Maps API to display markers and custom drawings on it, you may want to get your own location. You should get the Location Manager service and add a listener to it. If you want immediate coordinates, call the LocationManager#getLastKnownLocation function.
If you want to use those coordinates on a Map, don't forget to multiply them by 1E6.
Here is a more detailed snippet describing it:
       
public void obtainCoordinates() {

 //defines criteria of the coordinates provider you want
 final Criteria criteria = new Criteria();
 criteria.setAccuracy(Criteria.ACCURACY_FINE);
 criteria.setAltitudeRequired(false);
 criteria.setBearingRequired(false);
 criteria.setCostAllowed(true);
 criteria.setPowerRequirement(Criteria.POWER_LOW);

 LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
 final String bestProvider = this.locationManager.getBestProvider(
    criteria, true);

 //get immediate last known location if you want it
 Location location = this.locationManager.getLastKnownLocation(bestProvider);
 double longitude = 0.0;
 double latitude = 0.0;
 if (location != null) {
  latitude = location.getLatitude();
  longitude = location.getLongitude();
 }
 if ((latitude == 0) && (longitude == 0)) {
  final MyLocationListener listener = new MyLocationListener();
  this.locationManager.requestLocationUpdates(bestProvider, 0, 0, listener);
 } else {
  //use your coordinates
 }
}

Here is the listener to the coordinates:

       
public class MyLocationListener implements LocationListener {

 @Override
 public void onLocationChanged(Location location) {
  double latitude = location.getLatitude();
  double longitude = location.getLongitude();
                //use your coordinates
 }

 @Override
 public void onProviderDisabled(String provider) {

 }

 @Override
 public void onProviderEnabled(String provider) {

 }

 @Override
 public void onStatusChanged(String provider, int status, Bundle extras) {

 }

}
Of course, you need to add those permissions to your AndroidManifest.xml
       
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION">
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION">

And don't forget to call LocationManager#removeUpdates on the listener when you don't need GPS coordinates anymore.

Wednesday, 9 May 2012

Add Google Map to your App - Part 3

On part 1 of this tutorial, we saw how to display a map on your application.
On part 2, we learn how to display markers and listen to tap event on your map.

Now let's finish this tutorial by drawing custom elements on it.

We will draw small points on Mexico City, Paris and Tokyo, then draw lines between those cities.
To do so, you have to create a custom overlay and use the Projection#toPixels function to convert your latitude and longitude coordinates to pixel coordinates.
Let's take our MapActivity and add those features:

package com.fordemobile.brightstars;

import java.util.List;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;

public class MapsActivity extends MapActivity {

 private MapView mapView;

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

  setContentView(R.layout.map);
  // retrieves the map fro mthe layout
  this.mapView = (MapView) findViewById(R.id.mapView);
  // allow zoom in and zoom out features
  this.mapView.setBuiltInZoomControls(true);

  // create coordinates with latitude and longitude in microdegrees
  // (degrees * 1E6).
  final GeoPoint mexicoGeopoint = new GeoPoint(19240000, -99120000);
  final GeoPoint parisGeopoint = new GeoPoint(48833000, 2333000);
  final GeoPoint tokyoGeopoint = new GeoPoint(35667000, 139750000);

  // setup drawing properties
  final Paint paint = new Paint();
  paint.setAntiAlias(true);
  paint.setTextSize(18);
  paint.setFakeBoldText(true);
  paint.setColor(Color.RED);

  // add this overlay to the list of overlays of the map
  final List<Overlay> mapOverlays = this.mapView.getOverlays();
  mapOverlays.add(new Overlay() {
   @Override
   public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
     long when) {
    super.draw(canvas, mapView, shadow);

    // convert geo coordinates to screen pixels
    Point mexicoPoint = new Point();
    mapView.getProjection().toPixels(mexicoGeopoint, mexicoPoint);

    Point parisPoint = new Point();
    mapView.getProjection().toPixels(parisGeopoint, parisPoint);

    Point tokyoPoint = new Point();
    mapView.getProjection().toPixels(tokyoGeopoint, tokyoPoint);

    // draw small circles at the cities location
    canvas.drawCircle(mexicoPoint.x, mexicoPoint.y, 2, paint);
    canvas.drawCircle(parisPoint.x, parisPoint.y, 2, paint);
    canvas.drawCircle(tokyoPoint.x, tokyoPoint.y, 2, paint);

    // draw lines between cities
    canvas.drawLines(new float[] { mexicoPoint.x, mexicoPoint.y,
      parisPoint.x, parisPoint.y, parisPoint.x, parisPoint.y,
      tokyoPoint.x, tokyoPoint.y, tokyoPoint.x, tokyoPoint.y,
      mexicoPoint.x, mexicoPoint.y }, paint);
    
    // draw text
    canvas.drawText("Mexico City", mexicoPoint.x, mexicoPoint.y, paint);
    canvas.drawText("Paris", parisPoint.x, parisPoint.y, paint);
    canvas.drawText("Tokyo", tokyoPoint.x, tokyoPoint.y, paint);

    return true;
   }
  });
 }

 @Override
 protected boolean isRouteDisplayed() {
  return false;
 }
}

Here is the final result: