Build one set of code for China and global map styles

intermediate
Java
Prerequisite

Familiarity with Android Studio and Java and completion of the Tracking device location for Android tutorial.

Note

This tutorial includes loading a Mapbox China map style. Loading this type of style requires a special China Mapbox access token. Contact our sales team to start the process of receiving this special access token.

Traditional map services are either blocked in China or experience slow internet connections. Mapbox's mapbox.cn infrastructure has unparalleled speed advantages for anyone using Mapbox maps in China or through Chinese mobile carriers internationally. Built on top of the Mapbox Maps SDK for Android, the Mapbox China Plugin for Android automatically configures the Maps SDK to make sure that the correct Mapbox API endpoints are being called. Accurate endpoints make sure that a mobile device retrieves the correct map tiles, map styles, and other location data information. Additionally, the plugin handles shifting GeoJSON geometries (for example Polygons, LineStrings, and Points) so data is accurately placed on the map.

Most developers do not want to create and maintain two apps: one for Chinese users and another for other global users. This tutorial will instruct you on how to create one application that determines where the device is (in China or outside China) and takes the appropriate actions such as setting the correct Mapbox access token and loading the correct map style.

Global style loaded when the device is in San Francisco:

China style loaded when the device is in China:

Getting started

This guide assumes that you are familiar with Java and Android Studio. Here are the resources that you’ll need before getting started:

Handle location permissions

Edit tracking device location code

There are a few important differences between the application you built in the Tracking device location for Android tutorial and the application you'll build here. The primary difference is that you will specify which map style to display (and which token to use to display it) based on the device's location at runtime instead of specifying it ahead of time.

To prepare for this difference, delete the following lines of code inside of onCreate():

    Mapbox.getInstance(this, getString(R.string.access_token));
    setContentView(R.layout.activity_style_basic_symbol_layer);
    mapView = findViewById(R.id.mapView);
    mapView.onCreate(savedInstanceState);
    mapView.getMapAsync(this);

After deleting those lines, your onCreate() override method should be empty:

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

}

You can also delete the entire activity_main.xml file or at least the MapView inside of the XML file. You will programmatically create the map view later in this tutorial.

Request location permissions

Determining whether the device is inside or outside of China's borders is dependent on knowing where the device is. This requires asking for and receiving permission to have access to the device's location.

Check for location permissions in the now empty onCreate() method. Once location permissions are granted, the code inside PermissionsManager.areLocationPermissionsGranted(this) will run.

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

    if (PermissionsManager.areLocationPermissionsGranted(this)) {


    } else {
      permissionsManager = new PermissionsManager(this);
      permissionsManager.requestLocationPermissions(this);
    }    
}

Initialize the LocationEngine

Initialize the LocationEngine object once location permissions are granted. Creating the object includes the LocationEngineCallback callback that will deliver device location updates inside the onSuccess() method.

Make sure to remove initLocationEngine() from your project's enableLocationComponent() method so the LocationEngine object is set up well before the LocationComponent is created. initLocationEngine() should only run inside the PermissionsManager.areLocationPermissionsGranted(this) brackets.

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

    if (PermissionsManager.areLocationPermissionsGranted(this)) {

      initLocationEngine();

    } else {
      permissionsManager = new PermissionsManager(this);
      permissionsManager.requestLocationPermissions(this);
    }    
}

Determine device location

It's helpful to globally declare a Boolean (deviceInChina) that will store the status of the device's location: true if it is in China and false if it is not. This variable will be helpful in the rest of the tutorial.

public class MixedChinaAndGlobalStyleActivity extends AppCompatActivity implements OnMapReadyCallback,
    LocationEngineCallback<LocationEngineResult>, PermissionsListener {

  private Boolean deviceInChina = null;

onSuccess() is where you're going to determine whether the device is in China or not. Set the deviceInChina boolean to null so that you know that the determination hasn't happened yet.

At this point, you should still have a private static class that implements LocationEngineCallback<LocationEngineResult> from the tracking device location for Android guide.

You'll be checking whether it's null inside the onSuccess() callback. If it's null, then you know that it's time to check whether the device is in China's borders.

The Mapbox China Plugin for Android includes a ChinaBoundsChecker class. The class offers various methods to help you quickly determine this. Any of the methods return a true/false boolean.

// Pass in an Android-system Location object
boolean locationIsInChina = ChinaBoundsChecker.locationIsInChina(this, Location);

// Pass in a Mapbox LatLng object
boolean latLngIsInChina = ChinaBoundsChecker.latLngIsInChina(this, new LatLng(lat, lng));

// Pass in a Mapbox Point object
boolean pointIsInChina = ChinaBoundsChecker.pointIsInChina(this, Point.fromLngLat(lng, lat));

Set your globally declared deviceInChina boolean equal to what ChinaBoundsChecker.locationIsInChina(this, Location); returns:

@Override
  public void onSuccess(LocationEngineResult result) {

    Location lastLocation = result.getLastLocation();

    if (deviceInChina == null) {

      // Check to see whether the device location is inside
      // or outside of China's borders
      deviceInChina = ChinaBoundsChecker.locationIsInChina(
          this, result.getLastLocation());


    }

@Override
public void onFailure(@NonNull Exception exception) {

}

Now you know whether the device is within China's borders or not.

Update the app based on location

Use the correct access token

You've already given the Mapbox object an access token when getInstance was run. The Mapbox class has a setAccessToken() method to update the token used by the Maps SDK for Android.

If the token you originally passed through getInstance() was from your Mapbox account's token page, it's a "global" access token. This token won't be able to load a Mapbox China style. If you passed a special China access token through getInstance(), it won't be able to load a Mapbox non-China style.

Use the deviceInChina boolean to see if the deviceInChina situation correctly matches the token passed through getInstance(). Meaning, if a global access token was passed through getInstance() and it turns out that deviceInChina == true, then you're going to switch to a special China access token.

@Override
  public void onSuccess(LocationEngineResult result) {

    Location lastLocation = result.getLastLocation();

    if (deviceInChina == null) {

      if (lastLocation != null) {
        deviceInChina = ChinaBoundsChecker.locationIsInChina(
            this, lastLocation);

            if (deviceInChina != null) {

                if (deviceInChina) {
                  Mapbox.setAccessToken(getString(R.string.china_access_token));
                } else {
                  Mapbox.setAccessToken(getString(R.string.access_token));
                }
          }         
      }
    }

  @Override
  public void onFailure(@NonNull Exception exception) {

  }
Note

Use Mapbox.getAccessToken() if you ever want to check what access token the Maps SDK is using at run time.

Add the map

Now that you've set the correct access token, it's time to programmatically create and display a map instead of relying on XML. You've already deleted the XML setup from onCreate() earlier on in this tutorial.

Globally declare the Bundle object named savedInstanceState. Set it equal to the savedInstanceState object returned by onCreate().

Your onCreate() should now look like this:

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

    this.savedInstanceState = savedInstanceState

    if (PermissionsManager.areLocationPermissionsGranted(this)) {

      initLocationEngine();

    } else {
      permissionsManager = new PermissionsManager(this);
      permissionsManager.requestLocationPermissions(this);
    }    
}

A regular MapView should be used when showing a global style. The Mapbox China Plugin for Android includes a ChinaMapView, which should be used when showing a China style.

Both a regular MapView and ChinaMapView require a MapboxMapOptions object when built programmatically.

override fun onSuccess(result: LocationEngineResult?) {
    val lastLocation = result?.lastLocation

    if (deviceInChina == null) {

    // Check to see whether the device location is inside
    // or outside of China's borders
    deviceInChina = ChinaBoundsChecker.locationIsInChina(
            this@MainActivity, result?.lastLocation)


    } else {
        Mapbox.setAccessToken(getString(R.string.china_access_token));
    }

    MapboxMapOptions options = MapboxMapOptions.createFromAttributes(this, null)
          .camera(
              new CameraPosition.Builder()
                  .target(new LatLng(lastLocation.getLatitude(),
                      lastLocation.getLongitude()))
                  .zoom(10.0)
                  .build());

      if (deviceInChina) {
        chinaMapView = new ChinaMapView(this, options);
        chinaMapView.onCreate(savedInstanceState);
        chinaMapView.getMapAsync(this);
        setContentView(chinaMapView);
      } else {
        globalMapView = new MapView(this, options);
        globalMapView.onCreate(savedInstanceState);
        globalMapView.getMapAsync(this);
        setContentView(globalMapView);
      }

}

override fun onFailure(exception: Exception) {

}

Set the map style

Now that you've run getMapAsync(this), the onMapReady() callback will eventually fire. This is where you'll set the appropriate style. deviceInChina will determine which style String you use to build a Style object.

Use the ChinaStyle class's static final Strings to access pre-made Mapbox China style URIs and use the Style class's static final Strings to access pre-made Mapbox global styles URIs.

@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
    this.mapboxMap = mapboxMap;

    // Set the map style based on whether the device is in or out of China

    mapboxMap.setStyle(new Style.Builder().fromUri(
        deviceInChina ? ChinaStyle.MAPBOX_DARK_CHINESE : Style.TRAFFIC_DAY),

        new Style.OnStyleLoaded() {
          @Override
          public void onStyleLoaded(@NonNull Style style) {

            // Add the LocationComponent device location puck to the map
            initLocationComponent(style);

            // Add data to the map on top of whatever style is loaded above.
          }
    });
}

Move location puck

The onSuccess() callback fires every time the device location changes. You need to tell the Maps SDK's LocationComponent about this new Location update so the Maps SDK moves the device location puck to the new location.

override fun onSuccess(result: LocationEngineResult?) {
    val lastLocation = result?.lastLocation

    if (deviceInChina == null) {

    // Check to see whether the device location is inside
    // or outside of China's borders
    deviceInChina = ChinaBoundsChecker.locationIsInChina(
            this@MainActivity, result?.lastLocation)


    } else {
        Mapbox.setAccessToken(getString(R.string.china_access_token));
    }

    MapboxMapOptions options = MapboxMapOptions.createFromAttributes(this, null)
          .camera(
              new CameraPosition.Builder()
                  .target(new LatLng(lastLocation.getLatitude(),
                      lastLocation.getLongitude()))
                  .zoom(10.0)
                  .build());

      if (deviceInChina) {
        chinaMapView = new ChinaMapView(this, options);
        chinaMapView.onCreate(savedInstanceState);
        chinaMapView.getMapAsync(this);
        setContentView(chinaMapView);
      } else {
        globalMapView = new MapView(this, options);
        globalMapView.onCreate(savedInstanceState);
        globalMapView.getMapAsync(this);
        setContentView(globalMapView);
      }

      if (locationComponent != null) {
         locationComponent.forceLocationUpdate(lastLocation);
      }
}

override fun onFailure(exception: Exception) {

}

Finished product

You've set up code to check whether the device is inside of China and then set up other Mapbox code to show an appropriate map style. You now have one set of code that works for any place in the world, rather than two separate apps.

Global style loaded when the device is in San Francisco:

China style loaded when the device is in China:

MainActivity.java
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.location.Location;
import android.os.Bundle;
import android.widget.Toast;
import com.mapbox.android.core.location.LocationEngine;
import com.mapbox.android.core.location.LocationEngineCallback;
import com.mapbox.android.core.location.LocationEngineProvider;
import com.mapbox.android.core.location.LocationEngineRequest;
import com.mapbox.android.core.location.LocationEngineResult;
import com.mapbox.android.core.permissions.PermissionsListener;
import com.mapbox.android.core.permissions.PermissionsManager;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.Point;
import com.mapbox.geojson.Polygon;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.location.LocationComponent;
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions;
import com.mapbox.mapboxsdk.location.modes.CameraMode;
import com.mapbox.mapboxsdk.location.modes.RenderMode;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.plugins.china.constants.ChinaStyle;
import com.mapbox.mapboxsdk.plugins.china.maps.ChinaMapView;
import com.mapbox.mapboxsdk.plugins.china.shift.ChinaBoundsChecker;
import com.mapbox.mapboxsdk.style.layers.FillLayer;
import com.mapbox.mapboxsdk.style.layers.LineLayer;
import com.mapbox.mapboxsdk.style.layers.PropertyFactory;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements OnMapReadyCallback,
LocationEngineCallback<LocationEngineResult>, PermissionsListener {
private MapboxMap mapboxMap;
private ChinaMapView chinaMapView;
private MapView globalMapView;
private Bundle savedInstanceState;
private Boolean deviceInChina;
private LocationComponent locationComponent;
private PermissionsManager permissionsManager;
private long defaultIntervalInMilliseconds = 1000;
private long defaultMaxWaitTime = defaultIntervalInMilliseconds * 5;
// Adjust the Styles below to see various China and global styles used in this example
private String chinaStyleToUse = ChinaStyle.MAPBOX_DARK_CHINESE;
private String globalStyleToUse = Style.TRAFFIC_DAY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.savedInstanceState = savedInstanceState;
// Check location permissions
locationPermissionCheckAndStart();
}
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
// Set the map style based on whether the device is in or out of China
mapboxMap.setStyle(new Style.Builder().fromUri(
deviceInChina ? chinaStyleToUse : globalStyleToUse),
new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NonNull Style style) {
// Add the LocationComponent device location puck to the map
initLocationComponent(style);
// Add data to the map on top of whatever style is loaded above.
initSource();
initLayers();
}
});
}
/**
* This callback fires whenever the device location changes. This is where the
* device location's coordinates are checked against China's borders. The
* Mapbox token and map type are then set up based on where the device is.
*/
@Override
public void onSuccess(LocationEngineResult result) {
Location lastLocation = result.getLastLocation();
if (deviceInChina == null) {
// Check to see whether the device location is inside
// or outside of China's borders
if (lastLocation != null) {
deviceInChina = ChinaBoundsChecker.locationIsInChina(
this, lastLocation);
}
if (deviceInChina != null) {
if (deviceInChina) {
Mapbox.setAccessToken(getString(R.string.china_access_token));
} else {
Mapbox.setAccessToken(getString(R.string.access_token));
}
initMap(deviceInChina,
MapboxMapOptions.createFromAttributes(this, null)
.camera(
new CameraPosition.Builder()
.target(new LatLng(lastLocation.getLatitude(),
lastLocation.getLongitude()))
.zoom(10.0)
.build()),
savedInstanceState);
}
}
if (locationComponent != null) {
locationComponent.forceLocationUpdate(lastLocation);
}
}
@Override
public void onFailure(@NonNull Exception exception) {
Toast.makeText(this, String.format("get location failed: %s",
exception.getLocalizedMessage()), Toast.LENGTH_SHORT).show();
}
/**
* Check location permissions. Start the permissions process if they're not already
* granted. Initialize the [LocationEngine] if they're already given.
*/
private void locationPermissionCheckAndStart() {
// Check if permissions are enabled and if not request
if (PermissionsManager.areLocationPermissionsGranted(this)) {
initLocationEngine();
} else {
permissionsManager = new PermissionsManager(this);
permissionsManager.requestLocationPermissions(this);
}
}
/**
* Initialize the map based on whether the device location is in or outside of China.
*/
private void initMap(
Boolean deviceInChina,
MapboxMapOptions mapboxMapOptions,
Bundle savedInstanceState
) {
if (deviceInChina) {
chinaMapView = new ChinaMapView(this, mapboxMapOptions);
chinaMapView.onCreate(savedInstanceState);
chinaMapView.getMapAsync(this);
setContentView(chinaMapView);
} else {
globalMapView = new MapView(this, mapboxMapOptions);
globalMapView.onCreate(savedInstanceState);
globalMapView.getMapAsync(this);
setContentView(globalMapView);
}
}
/**
* Initialize the [LocationComponent] to show the device location puck on top of whatever
* map style is loaded.
*/
@SuppressWarnings("MissingPermission")
private void initLocationComponent(@NonNull Style fullyLoadedStyle) {
locationComponent = mapboxMap.getLocationComponent();
// Activate the LocationComponent with LocationComponentActivationOptions
locationComponent.activateLocationComponent(LocationComponentActivationOptions.builder(
this,
fullyLoadedStyle).build());
// Enable to make the LocationComponent visible
locationComponent.setLocationComponentEnabled(true);
// Set the LocationComponent's camera mode
locationComponent.setCameraMode(CameraMode.NONE);
// Set the LocationComponent's render mode
locationComponent.setRenderMode(RenderMode.NORMAL);
}
@SuppressLint("MissingPermission")
/**
* Initialize the [LocationEngine] so that location change callbacks happen
*/
private void initLocationEngine() {
LocationEngine locationEngine = LocationEngineProvider.getBestLocationEngine(this);
locationEngine.requestLocationUpdates(
new LocationEngineRequest.Builder(defaultIntervalInMilliseconds)
.setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
.setMaxWaitTime(defaultMaxWaitTime).build(),
this, getMainLooper());
locationEngine.getLastLocation(this);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public void onExplanationNeeded(List<String> permissionsToExplain) {
Toast.makeText(this, R.string.user_location_permission_explanation,
Toast.LENGTH_LONG).show();
}
@Override
public void onPermissionResult(boolean granted) {
if (granted) {
locationPermissionCheckAndStart();
} else {
Toast.makeText(this, R.string.user_location_permission_not_granted, Toast.LENGTH_LONG).show();
finish();
}
}
/**
* Initialize map source to eventually show line and fill layers.
*/
private void initSource() {
mapboxMap.getStyle(new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NonNull Style style) {
List<List<Point>> masterPointList = new ArrayList<>();
List<Point> pointList = new ArrayList<>();
pointList.add(Point.fromLngLat(121.474113, 31.230784));
pointList.add(Point.fromLngLat(121.481752, 31.213315));
pointList.add(Point.fromLngLat(121.495914, 31.212434));
pointList.add(Point.fromLngLat(121.498403, 31.224325));
pointList.add(Point.fromLngLat(121.487331, 31.235407));
pointList.add(Point.fromLngLat(121.474113, 31.230784));
masterPointList.add(pointList);
Feature polygonFeature = Feature.fromGeometry(Polygon.fromLngLats((
masterPointList)));
GeoJsonSource geojsonSource = new GeoJsonSource("source",
polygonFeature);
style.addSource(geojsonSource);
}
});
}
/**
* Add line and fill layers to the map to show data on top of whatever style is loaded.
*/
private void initLayers() {
mapboxMap.getStyle(new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NonNull Style style) {
style.addLayer(new LineLayer("line-layer", "source").
withProperties(
PropertyFactory.lineColor(Color.parseColor("#ca59ff")),
PropertyFactory.lineWidth(5f)
));
style.addLayer(new FillLayer("fill-layer", "source").withProperties(
PropertyFactory.fillColor(Color.parseColor("#ca59ff")),
PropertyFactory.fillOpacity(.6f)
));
}
});
}
@Override
public void onResume() {
super.onResume();
if (chinaMapView != null) {
chinaMapView.onResume();
}
if (globalMapView != null) {
globalMapView.onResume();
}
}
@Override
protected void onStart() {
super.onStart();
if (chinaMapView != null) {
chinaMapView.onStart();
}
if (globalMapView != null) {
globalMapView.onStart();
}
}
@Override
protected void onStop() {
super.onStop();
if (chinaMapView != null) {
chinaMapView.onStop();
}
if (globalMapView != null) {
globalMapView.onStop();
}
}
@Override
public void onPause() {
super.onPause();
if (chinaMapView != null) {
chinaMapView.onPause();
}
if (globalMapView != null) {
globalMapView.onPause();
}
}
@Override
public void onLowMemory() {
super.onLowMemory();
if (chinaMapView != null) {
chinaMapView.onLowMemory();
}
if (globalMapView != null) {
globalMapView.onLowMemory();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (chinaMapView != null) {
chinaMapView.onDestroy();
}
if (globalMapView != null) {
globalMapView.onDestroy();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (chinaMapView != null) {
chinaMapView.onSaveInstanceState(outState);
}
if (globalMapView != null) {
globalMapView.onSaveInstanceState(outState);
}
}
}

Next steps

There are many possibilities when using the Mapbox China Plugin for Android.

Was this page helpful?