More articles like this one, you can visit HUAWEI Developer Forum and Medium.
All About Maps
Let's talk about maps. I started an open source project called All About Maps (https://github.com/ulusoyca/AllAboutMaps). In this project I aim to demonstrate how we can implement the same map related use cases with different map providers in one codebase. We will use Mapbox Maps, Google Maps, and Huawei HMS Map Kit. This project uses following libraries and patterns:
MVVM pattern with Android Jetpack Libraries
Kotlin Coroutines for asynchronous operations
Dagger2 Dependency Injection
Android Clean Architecture
Note: The codebase changes by time. You can always find the latest code in develop branch. The code when this article is written can be seen by choosing the tag: episode_1-parse-gpx:
https://github.com/ulusoyca/AllAboutMaps/tree/episode_1-parse-gpx/
Motivation
Why do we need maps in our apps? What are the features a developer would expect from a map SDK? Let's try to list some:
Showing a coordinate on a map with camera options (zoom, tilt, latitude, longitude, bearing)
Adding symbols, photos, polylines, polygons to map
Handle user gestures (click, pinch, move events)
Showing maps with different map styles (Outdoor, Hybrid, Satallite, Winter, Dark etc.)
Data visualization (heatmaps, charts, clusters, time-lapse effect)
Offline map visualization (providing map tiles without network connectivity)
Generate snapshot image of a bounded region
We can probably add more items but I believe this is the list of features which all map provider companies would most likely provide. Knowing that we can achieve the same tasks with different map providers, we should not create huge dependencies to any specific provider in our codebase. When a product owner (PO) tells to developers to switch from Google Maps to Mapbox Maps, or Huawei Maps, developers should never see it as a big deal. It is software development. Business as usual.
One would probably think why a PO would want to switch from one map provider to another. In many cases, the reason is not the technical details. For example, Google Play Services may not be available in some devices or regions like China. Another case is when a company X which has a subscription to Mapbox, acquires company Y which uses Google Maps. In this case the transition to one provider is more efficient. Change in the terms of services, and pricing might be other motivations.
We need competition in the market! Let's switch easily when needed but how dependencies make things worse? Problematic dependencies in the codebase are usually created by developing software like there is no tomorrow. It is not always developers' fault. Tight schedules, anti-refactoring minded teams, unorganized plannings may cause careless coding and then eventually to technical depts. In this project, I aim to show how we can encapsulate the import lines below belonging to three different map providers to minimum number of classes with minimum lines:
import com.huawei.hms.maps.*
import com.google.android.gms.maps.*
import com.mapbox.mapboxsdk.maps.*
It should be noted that the way of achieving this in this post is just one proposal. There are always alternative and better ways of implementations. In the end, as software developers, we should deliver our tasks time-efficiently, without over-engineering.
About the project
In the home page of the project you will see the list of tutorials. Since this is the first blog post, there is only one item for now. To make our life easier with RecyclerViews, I use Epoxy library by Airbnb in the project. Once you click the buttons in the card, it will take to the detail page. Using bottom sheet we can switch between map providers. Note that Huawei Map Kit requires a Huawei mobile phone.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
In this first blog post, we will parse the GPX file of 120 km route of Cappadocia Ultra Trail race and show the route and check points (food stations) on map. I finished this race in 23 hours 45 mins and you can also read my experience here (https://link.medium.com/uWmrWLAzR6). GPX is an open standart which contains route points that constructs a polyline and waypoints which are the attraction location. In this case, the waypoints represents the food and aid stations in the race. We will show the route with a polyline and waypoints with markers on map.
Architecture
Architecture is definitely not an overrated concept. Since the early days of Android, we have been seeking for the best architectural patterns that suits with Android development. We have heard of MVC, MVP, MVVM, MVI and many other patterns will emerge. The change and adaptation to a new pattern is inevitable by time. We should keep in mind some basic and commonly accepted concepts like SOLID principles, seperation of concerns, maintainability, readibility, testablity etc. so that we can switch to between patterns easily when needed.
Nowadays, widely accepted architecture in Android community is modularization with Clean Architecture. If you have time to invest more, I would strongly suggest Joe Birch's clean architecture tutorials. As Joe suggests in his tutorials, we do not have to apply every rule line by line but instead we take whatever we feel like is needed. Here is my take and how I modularized the All About Maps app:
Note that dependency injection with Dagger2 is the core of this implementation. If you are not familiar with the concept, I strongly suggest you to read the best Dagger2 tutorial in the wild Dagger2 world by Nimrod Dayan.
Domain Module
Many of us are excited to start implementation with UI to see the results immediately but we should patiently build our blocks. We shall start with the domain module since we will put our business logic and define the entities and user interactions there.
First question: What entities do we need for a Map app?
We don't have to put every entity at once. Since our first tutorial is about drawing polylines and symbols we will need the following data:
LatLng class which holds Latitude and Longitude
Point which represents a geo-coordinate.
RouteInfo that holds points to be used to draw route and waypoints
Let's see the implementations:
Code:
inline class Latitude(val value: Float)
inline class Longitude(val value: Float)
Code:
data class LatLng(
val latitude: Latitude,
val longitude: Longitude
)
Code:
data class Point(
val latitude: Latitude,
val longitude: Longitude,
val altitude: Float? = null,
val name: String? = null
) {
val latLng: LatLng
get() = LatLng(latitude, longitude)
}
Code:
data class RouteInfo(
val routePoints: List<Point> = emptyList(),
val wayPoints: List<Point> = emptyList()
)
I could have used Float primitive type for Latitude and Longitude fields. However, I strongly suggest you to take advantage of Kotlin inline classes. In my relatively long career of working on maps, I spent hours on issues caused by mistakenly using longitude for latitude values.
Note that LatLng class is available in all Map SDKs. However, all the modules below the domain layer should use only our own LatLng to prevent the dependency to map SDKs in those modules. In the app layer we can map our LatLng class to corresponding classes:
Code:
import com.ulusoy.allaboutmaps.domain.entities.LatLng
import com.mapbox.mapboxsdk.geometry.LatLng as MapboxLatLng
import com.huawei.hms.maps.model.LatLng as HuaweiLatLng
import com.google.android.gms.maps.model.LatLng as GoogleLatLang
fun LatLng.toMapboxLatLng() = MapboxLatLng(
latitude.value.toDouble(),
longitude.value.toDouble()
)
fun LatLng.toHuaweiLatLng() = HuaweiLatLng(
latitude.value.toDouble(),
longitude.value.toDouble()
)
fun LatLng.toGoogleLatLng() = GoogleLatLang(
latitude.value.toDouble(),
longitude.value.toDouble()
)
Second question: What actions user can trigger?
Domain module contains the uses cases (interactors) that an application can perform to achieve goals based on user interactions. The code in this module is less likely to change compared to other modules. Business is business. For example, this application has one job for now: showing the route info with a polyline and markers. It can get the route info from a web server, a database or in this case from application resource file which is a GPX file. Neither the app module nor the domain module doesn't care where the route points and waypoints are retrieved from. It is not their concern. The concerns are seperated.
Lets see the use case definition in our domain module:
Code:
class GetRouteInfoUseCase
@Inject constructor(
private val routeInfoRepository: RouteInfoRepository
) {
suspend operator fun invoke(): RouteInfo {
return routeInfoRepository.getRouteInfo()
}
}
Code:
interface RouteInfoRepository {
suspend fun getRouteInfo(): RouteInfo
}
RouteInfoRepository is an interface that lives in the domain module and it is a contract between domain and datasource modules. Its concrete implementation lives in the datasource module.
Datasource Module
Datasource module is an abstraction world. Life here is based on interfaces. The domain module communicates with datasource module through the repository interface, then datasource module orchestrates the data flow in repository class and returns the final value.
Here, the domain module asks for the route info. Datasource module decides what to return after retrieving data from different data sources. For the sake of simplicity, in this case we have only one datasource: GPX parser. The route info is extracted from a GPX file. We don't know where, and how. Let's see the code:
Here is the concrete implementation of RouteInfoRepository interface. Route info datasource is injected as constructor parameter to this class.
Code:
class RouteInfoDataRepository
@Inject constructor(
@Named("GPX_DATA_SOURCE")
private val gpxFileDatasource: RouteInfoDatasource
) : RouteInfoRepository {
override suspend fun getRouteInfo(): RouteInfo {
return gpxFileDatasource.parseGpxFile()
}
}
Here is our one only route info data source: GpxFileDataSource. It still doesn't know how to get the data from gpx file. However, it knows where to get the data from thanks to contract GpxFileParser
Code:
class GpxFileDatasource
@Inject constructor(
private val gpxFileParser: GpxFileParser
): RouteInfoDatasource {
override suspend fun parseGpxFile(): RouteInfo {
return gpxFileParser.parseGpxFile()
}
}
What is a GPX file? How is it parsed? Where is the file located? Datasource doesn't care about these details. It only knows that the concrete implementation of GpxFileParser will return the RouteInfo. Here is the contract between the datasource and the concrete implementation:
Code:
interface GpxFileParser {
suspend fun parseGpxFile(): RouteInfo
}
Is it already too confusing with too many abstractions around? Is it overengineering? You might be right and choose to have less abstractions when you have one datasource like in this case. However, in real world, we have multiple datasources. Data is all around us. It may come from web server, from database or from our connected devices such as wearables. The benefit here is when things get more complicated with multiple datasources. Let's think about this complicated scenario.
App asks for the route info through a use case class.
Domain module forwards the request to data source.
Datasource orchestrates the data in the repository class
It first asks to Web servers (remote data source) to get the route info. However, the user is offline. Thus, the remote data source is not available.
Then it checks what we have locally by first checking if the route info is available in database or not.
It is not available in database but we have a gpx file in our resource folder (I know it doesn't make sense but to give an example).
The repository class asks GPX parser to parse the file and return the desired RouteInfo data.
Too complicated? It would be this much easier to implement this code in repository class based on the scenario:
Code:
class RouteInfoDataRepository
@Inject constructor(
@Named("GPX_DATA_SOURCE")
private val gpxFileDatasource: RouteInfoDatasource,
@Named("REMOTE_DATA_SOURCE")
private val remoteDatasource: RouteInfoDatasource,
@Named("DATABASE_SOURCE")
private val localDatasource: RouteInfoDatasource
) : RouteInfoRepository {
override suspend fun getRouteInfo(): RouteInfo? {
var routeInfo = remoteDatasource.parseGpxFile()
if (routeInfo == null) {
Timber.d("Route info is not available in remote source, now trying local database")
routeInfo = localDatasource.parseGpxFile()
if (routeInfo == null) {
Timber.d("Route info is not available in local database. Let's hope we have a gpx file in the app resource folder")
gpxFileDatasource.parseGpxFile()
}
}
return routeInfo
}
}
Thanks to Kotlin coroutines we can write these asynchronous operations sequentially.
For full content, you can visit HUAWEI Developer Forum.
Related
HUAWEI Location Kit provides you with the tools to build ultra-precise location services into your apps, by utilizing GNSS, Wi-Fi, base stations, and a range of cutting-edge hybrid positioning technologies. Location Kit-supported solutions give your apps a leg up in a ruthlessly competitive marketplace, making it easier than ever for you to serve a vast, global user base.
Location Kit currently offers three main functions: fused location, geofence, and activity identification. When used in conjunction with the Map SDK, which is supported in 200+ countries and regions and 100+ languages, you'll be able to bolster your apps with premium mapping services that enjoy a truly global reach.
Fused location provides easy-to-use APIs that are capable of obtaining the user's location with meticulous accuracy, and doing so while consuming a minimal amount of power. HW NLP, Huawei's exclusive network location service, makes use of crowdsourced data to achieve heightened accuracy. Such high-precision, cost-effective positioning has enormous implications for a broad array of mobile services, including ride hailing navigation, food delivery, travel, and lifestyle services, providing customers and service providers alike with the high-value, real time information that they need.
To avoid boring you with the technical details, we've provided some specific examples of how positioning systems, geofence, activity identification, map display and route planning services can be applied in the real world.
For instance, you can use Location kit to obtain the user's current location and create a 500-meter geofence radius around it, which can be used to determine the user's activity status when the geofence is triggered, then automatically plan a route based on this activity status (for example, plan a walking route when the activity is identified as walking), and have it shown on the map.
This article addresses the following functions:
Fused location: Incorporates GNSS, Wi-Fi, and base station data via easy-to-use APIs, making it easy for your app to obtain device location information.
Activity identification: Identifies the user's motion status, using the acceleration sensor, network information, and magnetometer, so that you can tailor your app to account for the user's behavior.
Geofence: Allows you to set virtual geographic boundaries via APIs, to send out timely notifications when users enter, exit, or remain with the boundaries.
Map display: Includes the map display, interactive features, map drawing, custom map styles, and a range of other features.
Route planning: Provides HTTP/HTTPS APIs for you to initiate requests using HTTP/HTTPS, and obtain the returned data in JSON format.
Usage scenarios:
Using high-precision positioning technology to obtain real time location and tracking data for delivery or logistics personnel, for optimally efficient services. In the event of accidents or emergencies, the location of personnel could also be obtained with ease, to ensure their quick rescue.
Creating a geofence in the system, which can be used to monitor an important or dangerous area at all times. If someone enters such an area without authorization, the system could send out a proactive alert. This solution can also be linked with onsite video surveillance equipment. When an alert is triggered, the video surveillance camera could pop up to provide continual monitoring, free of any blind spots.
Tracking patients with special needs in hospitals and elderly residents in nursing homes, in order to provide them with the best possible care. Positioning services could be linked with wearable devices, for attentive 24/7 care in real time.
Using the map to directly find destinations, and perform automatic route planning.
I. Advantages of Location Kit and Map Kit
Low-power consumption (Location Kit): Implements geofence using the chipset, for optimized power efficiency
High precision (Location Kit): Optimizes positioning accuracy in urban canyons, correctly identifying the roadside of the user. Sub-meter positioning accuracy in open areas, with RTK (Real-time kinematic) technology support. Personal information, activity identification, and other data are not uploaded to the server while location services are performed. As the data processor, Location Kit only uses data, and does not store it.
Personalized map displays (Map Kit): Offers enriching map elements and a wide range of interactive methods for building your map.
Broad-ranging place searches (Map Kit): Covers 130+ million POIs and 150+ million addresses, and supports place input prompts.
Global coverage: Supports 200+ countries/regions, and 40+ languages.
For more information and development guides, please visit: https://developer.huawei.com/consumer/en/hms/huawei-MapKit
II. Demo App Introduction
In order to illustrate how to integrate Location Kit and Map Kit both easily and efficiently, we've provided a case study here, which shows the simplest coding method for running the demo.
This app is used to create a geofence on the map based on the location when the user opens the app. The user can drag on the red mark to set a destination. After being confirmed, when the user triggers the geofence condition, the app will automatically detect their activity status and plan a route for the user, such as planning a walking route if the activity status is walking, or cycling route if the activity status is cycling. You can also implement real-time voice navigation for the planned route.
III. Development Practice
You need to set the priority (which is 100 by default) before requesting locations. To request the precise GPS location, set the priority to 100. To request the network location, set the priority to 102 or 104. If you only need to passively receive locations, set the priority to 105.
Parameters related to activity identification include VEHICLE (100), BIKE (101), FOOT (102), and STILL (103).
Geofence-related parameters include ENTER_GEOFENCE_CONVERSION (1), EXIT_GEOFENCE_CONVERSION (2), and DWELL_GEOFENCE_CONVERSION (4).
The following describes how to run the demo using source code, helping you understand the implementation details.
Preparations
Preparing Tools
Huawei phones (It is recommended that multiple devices be tested)
Android Studio
2.Registering as a Developer
Register as a Huawei developer.
Create an app in AppGallery Connect.
Create an app in AppGallery Connect by referring to Location Kit development preparations or Map Kit development preparations.
Enable Location Kit and Map Kit for the app on the Manage APIs page.
Add the SHA-256 certificate fingerprint.
Download the agconnect-services.json file and add it to the app directory of the project.
Create an Android demo project.
Learn about the function restrictions.
To use the route planning function of Map Kit, refer to Supported Countries/Regions (Route Planning).
To use other services of Map Kit, refer to Supported Countries/Regions.
Device TypeFeatureOS VersionHMS Core (APK) VersionHuawei phonesFused locationEMUI 5.0 or later4.0.0 or laterGeofenceEMUI 8.0 or later4.0.4 or laterActivity identificationEMUI 9.1.1 or later3.0.2 or laterNon-Huawei Android phonesFused locationAndroid 8.0 or later (API level 26 or higher)4.0.1 or laterGeofenceAndroid 5.0 or later (API level 21 or higher)4.0.4 or laterActivity identificationNot supportedNot supported
Running the Demo App
Install the app on the test device after debugging the project in Android Studio successfully
Replace the project package name and JSON file with those of your own.
Tap related button in the demo app to create a geofence which has a radius of 200 and is centered on the current location automatically pinpointed by the demo app.
Drag the mark point on the map to select a destination.
View the route that is automatically planned based on the current activity status when the geofence is triggered.
The following figure shows the demo effect:
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Key Steps
Add the Huawei Maven repository to the project-level build.gradle file.
Add the following Maven repository address to the project-level build.gradle file of your Android Studio project:
Code:
buildscript {
repositories {
maven { url 'http://developer.huawei.com/repo/'}
}
dependencies {
...
// Add the AppGallery Connect plugin configuration.
classpath 'com.huawei.agconnect:agcp:1.4.2.300'
}
}allprojects {
repositories {
maven { url 'http://developer.huawei.com/repo/'}
}
}
Add dependencies on the SDKs in the app-level build.gradle file.
Code:
dependencies {
implementation 'com.huawei.hms:location:5.1.0.300'
implementation 'com.huawei.hms:maps:5.2.0.302' }
3. Add the following configuration to the next line under apply plugin: 'com.android.application' in the file header:
apply plugin: 'com.huawei.agconnect'
Note:
You must configure apply plugin: 'com.huawei.agconnect' under apply plugin: 'com.android.application'.
The minimum Android API level (minSdkVersion) required for the HMS Core Map SDK is 19.
4. Declare system permissions in the AndroidManifest.xml file.
Location Kit uses GNSS, Wi-Fi, and base station data for fused location, enabling your app to quickly and accurately obtain users' location information. Therefore, Location Kit requires permissions to access Internet, obtain the fine location, and obtain the coarse location. If your app needs to continuously obtain the location information when it runs in the background, you also need to declare the ACCESS_BACKGROUND_LOCATION permission in the AndroidManifest.xml file:
Code:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="com.huawei.hms.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
Note: Because the ACCESS_FINE_LOCATION, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, and ACTIVITY_RECOGNITION permissions are dangerous system permissions, you need to dynamically apply for these permissions. If you do not have the permissions, Location Kit will reject to provide services for your app.
Key Code
I. Map Display
Currently, the Map SDK supports two map containers: SupportMapFragment and MapView. This document uses the SupportMapFragment container.
Add a Fragment object in the layout file (for example: activity_main.xml), and set map attributes in the file.
Code:
<fragment
android:id="@+id/mapfragment_routeplanningdemo"
android:name="com.huawei.hms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
To use a map in your app, implement the OnMapReadyCallback API.
RoutePlanningActivity extends AppCompatActivity implements OnMapReadyCallback
Load SupportMapView in the onCreate method, call getMapAsync to register the callback.
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.mapfragment_routeplanningdemo);
if (fragment instanceof SupportMapFragment) {
SupportMapFragment mSupportMapFragment = (SupportMapFragment) fragment;
mSupportMapFragment.getMapAsync(this);
}
Call the onMapReady callback to obtain the HuaweiMap object.
Code:
@Override
public void onMapReady(HuaweiMap huaweiMap) {
hMap = huaweiMap;
hMap.setMyLocationEnabled(true);
hMap.getUiSettings().setMyLocationButtonEnabled(true);
}
II. Function Implementation
Check the permissions.
Code:
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
if (ActivityCompat.checkSelfPermission(context,
"com.huawei.hms.permission.ACTIVITY_RECOGNITION") != PackageManager.PERMISSION_GRANTED) {
String[] permissions = {"com.huawei.hms.permission.ACTIVITY_RECOGNITION"};
ActivityCompat.requestPermissions((Activity) context, permissions, 1);
Log.i(TAG, "requestActivityTransitionButtonHandler: apply permission");
}
} else {
if (ActivityCompat.checkSelfPermission(context,
"android.permission.ACTIVITY_RECOGNITION") != PackageManager.PERMISSION_GRANTED) {
String[] permissions = {"android.permission.ACTIVITY_RECOGNITION"};
ActivityCompat.requestPermissions((Activity) context, permissions, 2);
Log.i(TAG, "requestActivityTransitionButtonHandler: apply permission");
}
}
Check whether the location permissions have been granted. If no, the location cannot be obtained.
Code:
settingsClient.checkLocationSettings(locationSettingsRequest)
.addOnSuccessListener(locationSettingsResponse -> {
fusedLocationProviderClient
.requestLocationUpdates(mLocationRequest, mLocationCallback, Looper.getMainLooper())
.addOnSuccessListener(aVoid -> {
//Processing when the API call is successful.
});
})
.addOnFailureListener(e -> {});
if (null == mLocationCallbacks) {
mLocationCallbacks = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
if (locationResult != null) {
List<HWLocation> locations = locationResult.getHWLocationList();
if (!locations.isEmpty()) {
for (HWLocation location : locations) {
hMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(), location.getLongitude()), 14));
latLngOrigin = new LatLng(location.getLatitude(), location.getLongitude());
if (null != mMarkerOrigin) {
mMarkerOrigin.remove();
}
MarkerOptions options = new MarkerOptions()
.position(latLngOrigin)
.title("Hello Huawei Map")
.snippet("This is a snippet!");
mMarkerOrigin = hMap.addMarker(options);
removeLocationUpdatesWith();
}
}
}
}
@Override
public void onLocationAvailability(LocationAvailability locationAvailability) {
if (locationAvailability != null) {
boolean flag = locationAvailability.isLocationAvailable();
Log.i(TAG, "onLocationAvailability isLocationAvailable:" + flag);
}
}
};
}
III. Geofence and Ground Overlay Creation
Create a geofence based on the current location and add a round ground overlay on the map.
Code:
GeofenceRequest.Builder geofenceRequest = new
GeofenceRequest.Builder geofenceRequest = new GeofenceRequest.Builder();
geofenceRequest.createGeofenceList(GeoFenceData.returnList());
geofenceRequest.setInitConversions(7);
try {
geofenceService.createGeofenceList(geofenceRequest.build(), pendingIntent)
.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(Task<Void> task) {
if (task.isSuccessful()) {
Log.i(TAG, "add geofence success!");
if (null == hMap) {
return; }
if (null != mCircle) {
mCircle.remove();
mCircle = null;
}
mCircle = hMap.addCircle(new CircleOptions()
.center(latLngOrigin)
.radius(500)
.strokeWidth(1)
.fillColor(Color.TRANSPARENT));
} else {Log.w(TAG, "add geofence failed : " + task.getException().getMessage());}
}
});
} catch (Exception e) {
Log.i(TAG, "add geofence error:" + e.getMessage());
}
// Geofence service
Code:
<receiver
android:name=".GeoFenceBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name=".GeoFenceBroadcastReceiver.ACTION_PROCESS_LOCATION" />
</intent-filter>
</receiver>
if (intent != null) {
final String action = intent.getAction();
if (ACTION_PROCESS_LOCATION.equals(action)) {
GeofenceData geofenceData = GeofenceData.getDataFromIntent(intent);
if (geofenceData != null && isListenGeofence) {
int conversion = geofenceData.getConversion();
MainActivity.setGeofenceData(conversion);
}
}
}
Mark the selected point on the map to obtain the destination information, check the current activity status, and plan routes based on the detected activity status.
Code:
hMap.setOnMapClickListener(latLng -> {
latLngDestination = new LatLng(latLng.latitude, latLng.longitude);
if (null != mMarkerDestination) {
mMarkerDestination.remove();
}
MarkerOptions options = new MarkerOptions()
.position(latLngDestination)
.title("Hello Huawei Map");
mMarkerDestination = hMap.addMarker(options);
if (identification.getText().equals("To exit the fence,Your activity is about to be detected.")) {
requestActivityUpdates(5000);
}
});
// Activity identification API
activityIdentificationService.createActivityIdentificationUpdates(detectionIntervalMillis, pendingIntent)
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.i(TAG, "createActivityIdentificationUpdates onSuccess");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "createActivityIdentificationUpdates onFailure:" + e.getMessage());
}
});
// URL of the route planning API (cycling route is used as an example): https://mapapi.cloud.huawei.com/mapApi/v1/routeService/bicycling?key=API KEY
NetworkRequestManager.getBicyclingRoutePlanningResult(latLngOrigin, latLngDestination,
new NetworkRequestManager.OnNetworkListener() {
@Override
public void requestSuccess(String result) {
generateRoute(result);
}
@Override
public void requestFail(String errorMsg) {
Message msg = Message.obtain();
Bundle bundle = new Bundle();
bundle.putString("errorMsg", errorMsg);
msg.what = 1;
msg.setData(bundle);
mHandler.sendMessage(msg);
}
});
Note:
The route planning function provides a set of HTTPS-based APIs used to plan routes for walking, cycling, and driving and calculate route distances. The APIs return route data in JSON format and provide the route planning capabilities.
The route planning function can plan walking, cycling, and driving routes.
You can try to plan a route from one point to another point and then draw the route on the map, achieving the navigation effects.
Related Parameters
In indoor environments, the navigation satellite signals are usually weak. Therefore, HMS Core (APK) will use the network location mode, which is relatively slow compared with the GNSS location. It is recommended that the test be performed outdoors.
In Android 9.0 or later, you are advised to test the geofence outdoors. In versions earlier than Android 9.0, you can test the geofence indoors.
Map Kit is unavailable in the Chinese mainland. Therefore, the Android SDK, JavaScript API, Static Map API, and Directions API are unavailable in the Chinese mainland. For details, please refer to Supported Countries/Regions.
In the Map SDK for Android 5.0.0.300 and later versions, you must set the API key before initializing a map. Otherwise, no map data will be displayed.
Currently, the driving route planning is unavailable in some countries and regions outside China. For details about the supported countries and regions, please refer to the Huawei official website.
Before building the APK, configure the obfuscation configuration file to prevent the HMS Core SDK from being obfuscated.
Open the obfuscation configuration file proguard-rules.pro in the app's root directory of your project and add configurations to exclude the HMS Core SDK from obfuscation.
If you are using AndResGuard, add its trustlist to the obfuscation configuration file.
For details, please visit the following link: https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-sdk-config-obfuscation-scripts-0000001061882229
To learn more, visit the following links:
Documentation on the HUAWEI Developers website:
https://developer.huawei.com/consumer/en/hms/huawei-locationkit
https://developer.huawei.com/consumer/en/hms/huawei-MapKit
To download the demo and sample code, please visit GitHub.
To solve integration problems, please go to Stack Overflow at the following link:
https://stackoverflow.com/questions/tagged/huawei-mobile-services?tab=Newest
To learn more, please visit:
HUAWEI Developers official website
Development Guide
Reddit to join developer discussions
GitHub or Gitee to download the demo and sample code
Stack Overflow to solve integration problems
Follow our official account for the latest HMS Core-related news and updates.
Original Source
Can we get the location offline?
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
With the increasing popularity of the mobile Internet, mobile apps are now becoming an integral part of our daily lives and provide increasingly more diverse functions that bring many benefits to users. One such function is searching for Point of Interests (POIs) or places, such as banks and restaurants, in an app.
When a user searches for a POI in an app, besides general information about the POI, such as the name and location, they also expect to be shown other relevant details. For example, when searching for a POI in a taxi-hailing app, a user usually expects the app to display both the searched POI and other nearby POIs, so that the user can select the most convenient pick-up and drop-off point. When searching for a bank branch in a mobile banking app, a user usually wants the app to show both the searched bank branch and nearby POIs of a similar type and their details such as business hours, telephone numbers, and nearby roads.
However, showing POI details in an app is usually a challenge for developers of non-map-related apps, because it requires a large amount of detailed POI data that is generally hard to collect for most app developers. So, wouldn't it be great if there was a service which an app can use to provide users with information about POI (such as the business hours and ratings) when they search for different types of POIs (such as hotels, restaurants, and scenic spots) in the app?
Fortunately, HMS Core Site Kit provides a one-stop POI search service, which boasts more than 260 million POIs in over 200 countries and regions around the world. In addition, the service supports more than 70 languages, empowering users to search for places in their own native languages. The place detail search function in the kit allows an app to obtain information about a POI, such as the name, address, and longitude and latitude, based on the unique ID of the POI. For example, a user can search for nearby bank branches in a mobile banking app, and view information about each branch, such as their business hours and telephone numbers, or search for the location of a scenic spot and view information about nearby hotels and weather forecasts in a travel app, thanks to the place detail search function. The place detail search function can even be utilized by location-based games that can use the function to show in-game tasks and rankings of other players at a POI when a player searches for the POI in the game.
Th integration process for this kit is straightforward, which I'll demonstrate below.
Demo
Integration Procedure
PreparationsBefore getting started, you'll need to make some preparations, such as configuring your app information in AppGallery Connect, integrating the Site SDK, and configuring the obfuscation configuration file.
If you use Android Studio, you can integrate the SDK into your project via the Maven repository. The purpose of configuring the obfuscation configuration file is to prevent the SDK from being obfuscated.
You can follow instructions here to make relevant preparations. In this article, I won't be describing the preparation steps.
Developing Place Detail SearchAfter making relevant preparations, you will need to implement the place detail search function for obtaining POI details. The process is as follows:
1. Declare a SearchService object and use SearchServiceFactory to instantiate the object.
2. Create a DetailSearchRequest object and set relevant parameters.
The object will be used as the request body for searching for POI details. Relevant parameters are as follows:
siteId: ID of a POI. This parameter is mandatory.
language: language in which search results are displayed. English will be used if no language is specified, and if English is unavailable, the local language will be used.
children: indicates whether to return information about child nodes of the POI. The default value is false, indicating that child node information is not returned. If this parameter is set to true, all information about child nodes of the POI will be returned.
3. Create a SearchResultListener object to listen for the search result.
4. Use the created SearchService object to call the detailSearch() method and pass the created DetailSearchRequest and SearchResultListener objects to the method.
5. Obtain the DetailSearchResponse object using the created SearchResultListener object. You can obtain a Site object from the DetailSearchResponse object and then parse it to obtain the search results.
The sample code is as follows:
Code:
// Declare a SearchService object.
private SearchService searchService;
// Create a SearchService instance.
searchService = SearchServiceFactory.create(this, "
API key
");
// Create a request body.
DetailSearchRequest request = new DetailSearchRequest();
request.setSiteId("
C2B922CC4651907A1C463127836D3957
");
request.setLanguage("
fr
");
request.setChildren(
false
);
// Create a search result listener.
SearchResultListener<DetailSearchResponse> resultListener = new SearchResultListener<DetailSearchResponse>() {
// Return the search result when the search is successful.
@Override
public void onSearchResult(DetailSearchResponse result) {
Site site;
if (result == null || (site = result.getSite()) == null) {
return;
}
Log.i("TAG", String.format("siteId: '%s', name: %s\r\n", site.getSiteId(), site.getName()));
}
// Return the result code and description when a search exception occurs.
@Override
public void onSearchError(SearchStatus status) {
Log.i("TAG", "Error : " + status.getErrorCode() + " " + status.getErrorMessage());
}
};
// Call the place detail search API.
searchService.detailSearch(request, resultListener);
You have now completed the integration process and your app should be able to show users details about the POIs they search for.
Conclusion
Mobile apps are now an integral part of our daily life. To improve user experience and provide users with a more convenient experience, mobile apps are providing more and more functions such as POI search.
When searching for POIs in an app, besides general information such as the name and location of the POI, users usually expect to be shown other context-relevant information as well, such as business hours and similar POIs nearby. However, showing POI details in an app can be challenging for developers of non-map-related apps, because it requires a large amount of detailed POI data that is usually hard to collect for most app developers.
In this article, I demonstrated how I solved this challenge using the place detail search function, which allows my app to show POI details to users. The whole integration process is straightforward and cost-efficient, and is an effective way to show POI details to users.
In the mobile Internet era, people are increasingly using mobile apps for a variety of different purposes, such as buying products online, hailing taxis, and much more. When using such an app, a user usually needs to manually enter their address for package delivery or search for an appropriate pick-up and drop-off location when they hail a taxi, which can be inconvenient.
To improve user experience, many apps nowadays allow users to select a point on the map and then use the selected point as the location, for example, for package delivery or getting on or off a taxi. Each location has a longitude-latitude coordinate that pinpoints its position precisely on the map. However, longitude-latitude coordinates are simply a string of numbers and provide little information to the average user. It would therefore be useful if there was a tool which an app can use to convert longitude-latitude coordinates into human-readable addresses.
Fortunately, the reverse geocoding function in HMS Core Location Kit can obtain the nearest address to a selected point on the map based on the longitude and latitude of the point. Reverse geocoding is the process of converting a location as described by geographic coordinates (longitude and latitude) to a human-readable address or place name, which is much more useful information for users. It permits the identification of nearby street addresses, places, and subdivisions such as neighborhoods, counties, states, and countries.
Generally, the reverse geocoding function can be used to obtain the nearest address to the current location of a device, show the address or place name when a user taps on the map, find the address of a geographic location, and more. For example, with reverse geocoding, an e-commerce app can show users the detailed address of a selected point on the map in the app; a ride-hailing or takeout delivery app can show the detailed address of a point that a user selects by dragging the map in the app or tapping the point on the map in the app, so that the user can select the address as the pick-up address or takeout delivery address; and an express delivery app can utilize reverse geocoding to show the locations of delivery vehicles based on the passed longitude-latitude coordinates, and intuitively display delivery points and delivery routes to users.
Bolstered by a powerful address parsing capability, the reverse geocoding function in this kit can display addresses of locations in accordance with local address formats with an accuracy as high as 90%. In addition, it supports 79 languages and boasts a parsing latency as low as 200 milliseconds.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
DemoThe file below is a demo of the reverse geocoding function in this kit.
Preparations
Before getting started with the development, you will need to make the following preparations:
Register as a Huawei developer and complete identity verification on the HUAWEI Developers website. You can click here to find out the detailed registration and identity verification procedure.
Create a project and then create an app in the project in AppGallery Connect. Before doing so, you must have a Huawei developer account and complete identity verification.
Generate a signing certificate fingerprint and configure it in AppGallery Connect. The signing certificate fingerprint is used to verify the authenticity of an app. Before releasing an app, you must generate a signing certificate fingerprint locally based on the signing certificate and configure it in AppGallery Connect.
Integrate the Location SDK into your app. If you are using Android Studio, you can integrate the SDK via the Maven repository.
Here, I won't be describing how to generate and configure a signing certificate fingerprint and integrate the SDK. You can click here to learn about the detailed procedure.
Development Procedure
After making relevant preparations, you can perform the steps below to use the reverse geocoding service in your app. Before using the service, ensure that you have installed HMS Core (APK) on your device.
1. Create a geocoding service client.
In order to call geocoding APIs, you first need to create a GeocoderService instance in the onClick() method of GeocoderActivity in your project. The sample code is as follows:
Code:
Locale locale = new Locale("zh", "CN");
GeocoderService geocoderService = LocationServices.getGeocoderService(GeocoderActivity.this, locale);
2. Obtain the reverse geocoding information.
To empower your app to obtain the reverse geocoding information, you need to call the getFromLocation() method of the GeocoderService object in your app. This method will return a List<HWLocation> object containing the location information based on the set GetFromLocationRequest object.
a. Set reverse geocoding request parameters.
There are three request parameters in the GetFromLocationRequest object, which indicate the latitude, longitude, and maximum number of returned results respectively. The sample code is as follows:
Code:
// Parameter 1: latitude
// Parameter 2: longitude
// Parameter 3: maximum number of returned results
// Pass valid longitude-latitude coordinates. If the coordinates are invalid, no geographical information will be returned. Outside China, pass longitude-latitude coordinates located outside China and ensure that the coordinates are correct.
GetFromLocationRequest getFromLocationRequest = new GetFromLocationRequest(39.985071, 116.501717, 5);
b. Call the getFromLocation() method to obtain reverse geocoding information.
The obtained reverse geocoding information will be returned in a List<HWLocation> object. You can add listeners using the addOnSuccessListener() and addOnFailureListener() methods, and obtain the task execution result using the onSuccess() and onFailure() methods.
The sample code is as follows:
Code:
private void getReverseGeocoding() {
// Initialize the GeocoderService object.
if (geocoderService == null) {
geocoderService = new GeocoderService(this, new Locale("zh", "CN"));
}
geocoderService.getFromLocation(getFromLocationRequest)
.addOnSuccessListener(new OnSuccessListener<List<HWLocation>>() {
@Override
public void onSuccess(List<HWLocation> hwLocation) {
// TODO: Define callback for API call success.
if (null != hwLocation && hwLocation.size() > 0) {
Log.d(TAG, "hwLocation data set quantity: " + hwLocation.size());
Log.d(TAG, "CountryName: " + hwLocation.get(0).getCountryName());
Log.d(TAG, "City: " + hwLocation.get(0).getCity());
Log.d(TAG, "Street: " + hwLocation.get(0).getStreet());
}
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
// TODO: Define callback for API call failure.
}
});
}
Congratulations, your app is now able to use the reverse geocoding function to obtain the address of a location based on its longitude and latitude.
Conclusion
The quick development and popularization of the mobile Internet has caused many changes to our daily lives. One such change is that more and more people are using mobile apps on a daily basis, for example, to buy daily necessities or hail a taxi. These tasks traditionally require users to manually enter the delivery address or pick-up and drop-off location addresses. Manually entering such addresses is inconvenient and prone to mistakes.
To solve this issue, many apps allow users to select a point on the in-app map as the delivery address or the address for getting on or off a taxi. However, the point on the map is usually expressed as a set of longitude-latitude coordinates, which most users will find hard to understand.
As described in this post, my app resolves this issue using the reverse geocoding function, which is proven a very effective way for obtaining human-readable addresses based on longitude-latitude coordinates. If you are looking for a solution to such issues, have a try to find out if this is what your app needs.
You have likely seen user reviews complaining about how the online shopping experiences, in particular the inability to try on clothing items before purchase. Augmented reality (AR) enabled virtual try-on has resolved this longstanding issue, making it possible for users to try on items before purchase.
Virtual try-on allows the user to try on clothing, or accessories like watches, glasses, and makeup, virtually on their phone. Apps that offer AR try-on features empower their users to make informed purchases, based on which items look best and fit best, and therefore considerably improve the online shopping experience for users. For merchants, AR try-on can both boost conversion rates and reduce return rates, as customers are more likely to be satisfied with what they have purchased after the try-on. That is why so many online stores and apps are now providing virtual try-on features of their own.
When developing an online shopping app, AR is truly a technology that you can't miss. For example, if you are building an app or platform for watch sellers, you will want to provide a virtual watch try-on feature, which is dependent on real-time hand recognition and tracking. This can be done with remarkable ease in HMS Core AR Engine, which provides a wide range of basic AR capabilities, including hand skeleton tracking, human body tracking, and face tracking. Once you have integrated this tool kit, your users will be able to try on different watches virtually within your app before purchases. Better yet, the development process is highly streamlined. During the virtual try-on, the user's hand skeleton is recognized in real time by the engine, with a high degree of precision, and virtual objects are superimposed on the hand. The user can even choose to place an item on their fingertip! Next I will show you how you can implement this marvelous capability.
Demo
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
ImplementationAR Engine provides a hand skeleton tracking capability, which identifies and tracks the positions and postures of up to 21 hand skeleton points, forming a hand skeleton model.
Thanks to the gesture recognition capability, the engine is able to provide AR apps with fun, interactive features. For example, your app will allow users to place virtual objects in specific positions, such as on the fingertips or in the palm, and enable the virtual hand to perform intricate movements.
Now I will show you how to develop an app that implements AR watch virtual try-on based on this engine.
Integration ProcedureRequirements on the Development EnvironmentJDK: 1.8.211 or later
Android Studio: 3.0 or later
minSdkVersion: 26 or later
targetSdkVersion: 29 (recommended)
compileSdkVersion: 29 (recommended)
Gradle version: 6.1.1 or later (recommended)
Make sure that you have downloaded the AR Engine APK from AppGallery and installed it on the device.
If you need to use multiple HMS Core kits, use the latest versions required for these kits.
Preparations1. Before getting started, you will need to register as a Huawei developer and complete identity verification on the HUAWEI Developers website. You can click here to find out the detailed registration and identity verification procedure.
2. Before getting started, integrate the AR Engine SDK via the Maven repository into your development environment.
3. The procedure for configuring the Maven repository address in Android Studio varies for Gradle plugin earlier than 7.0, Gradle plugin 7.0, and Gradle plugin 7.1 or later. You need to configure it according to the specific Gradle plugin version.
4. Take Gradle plugin 7.0 as an example:
Open the project-level build.gradle file in your Android Studio project and configure the Maven repository address.
Go to buildscript > repositories and configure the Maven repository address for the SDK.
Code:
buildscript {
repositories {
google()
jcenter()
maven {url "https://developer.huawei.com/repo/" }
}
}
Open the project-level settings.gradle file and configure the Maven repository address for the HMS Core SDK.
Code:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
repositories {
google()
jcenter()
maven {url "https://developer.huawei.com/repo/" }
}
}
}
5. Add the following build dependency in the dependencies block.
Code:
dependencies {
implementation 'com.huawei.hms:arenginesdk:{version}
}
App Development1. Check whether AR Engine has been installed on the current device. If so, your app will be able to run properly on the device. If not, you need to prompt the user to install AR Engine on the device, for example, by redirecting the user to AppGallery and prompting the user to install it. The sample code is as follows:
Code:
boolean isInstallArEngineApk =AREnginesApk.isAREngineApkReady(this);
if (!isInstallArEngineApk) {
// ConnectAppMarketActivity.class is the activity for redirecting users to AppGallery.
startActivity(new Intent(this, com.huawei.arengine.demos.common.ConnectAppMarketActivity.class));
isRemindInstall = true;
}
2. Initialize an AR scene. AR Engine supports five scenes, including motion tracking (ARWorldTrackingConfig) scene, face tracking (ARFaceTrackingConfig) scene, hand recognition (ARHandTrackingConfig) scene, human body tracking (ARBodyTrackingConfig) scene, and image recognition (ARImageTrackingConfig) scene.
Call ARHandTrackingConfig to initialize the hand recognition scene.
Code:
mArSession = new ARSession(context);
ARHandTrackingConfig config = new ARHandTrackingconfig(mArSession);
3. After obtaining an ARhandTrackingconfig object, you can set the front or rear camera. The sample code is as follows:
Code:
Config.setCameraLensFacing(ARConfigBase.CameraLensFacing.FRONT);
4. After obtaining config, configure it in ArSession, and start hand recognition.
Code:
mArSession.configure(config);
mArSession.resume();
5. Initialize the HandSkeletonLineDisplay class, which draws the hand skeleton based on the coordinates of the hand skeleton points.
Code:
Class HandSkeletonLineDisplay implements HandRelatedDisplay{
// Methods used in this class are as follows:
// Initialization method.
public void init(){
}
// Method for drawing the hand skeleton. When calling this method, you need to pass the ARHand object to obtain data.
public void onDrawFrame(Collection<ARHand> hands,){
// Call the getHandskeletonArray() method to obtain the coordinates of hand skeleton points.
Float[] handSkeletons = hand.getHandskeletonArray();
// Pass handSkeletons to the method for updating data in real time.
updateHandSkeletonsData(handSkeletons);
}
// Method for updating the hand skeleton point connection data. Call this method when any frame is updated.
public void updateHandSkeletonLinesData(){
// Method for creating and initializing the data stored in the buffer object.
GLES20.glBufferData(…,mVboSize,…);
// Update the data in the buffer object.
GLES20.glBufferSubData(…,mPointsNum,…);
}
}
6. Initialize the HandRenderManager class, which is used to render the data obtained from AR Engine.
Code:
Public class HandRenderManager implements GLSurfaceView.Renderer{
// Set the ARSession object to obtain the latest data in the onDrawFrame method.
Public void setArSession(){
}
}
7. Initialize the onDrawFrame() method in the HandRenderManager class.
Code:
Public void onDrawFrame(){
// In this method, call methods such as setCameraTextureName() and update() to update the calculation result of ArEngine.
// Call this API when the latest data is obtained.
mSession.setCameraTextureName();
ARFrame arFrame = mSession.update();
ARCamera arCamera = arFrame.getCamera();
// Obtain the tracking result returned during hand tracking.
Collection<ARHand> hands = mSession.getAllTrackables(ARHand.class);
// Pass the obtained hands object in a loop to the method for updating gesture recognition information cyclically for processing.
For(ARHand hand : hands){
updateMessageData(hand);
}
}
8. On the HandActivity page, set a render for SurfaceView.
Code:
mSurfaceView.setRenderer(mHandRenderManager);
Setting the rendering mode.
mSurfaceView.setRenderMode(GLEurfaceView.RENDERMODE_CONTINUOUSLY);
ConclusionAugmented reality creates immersive, digital experiences that bridge the digital and real worlds, making human-machine interactions more seamless than ever. Fields like gaming, online shopping, tourism, medical training, and interior decoration have seen surging demand for AR apps and devices. In particular, AR is expected to dominate the future of online shopping, as it offers immersive experiences based on real-time interactions with virtual products, which is what younger generations are seeking for. This considerably improves user's shopping experience, and as a result helps merchants a lot in improving the conversion rate and reducing the return rate. If you are developing an online shopping app, virtual try-on is a must-have feature for your app, and AR Engine can give you everything you need. Try the engine to experience what smart, interactive features it can bring to users, and how it can streamline your development.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Hey, guys! I'm still working on my mobile multiplayer survival game. In my article titled Build a Game That Features Local In-App Purchases , I shared my experience of configuring in-app product information in the language and currency of the country or region where the user's account is located, which streamlines the purchase journey for users and boosts monetization.
Some new challenges have arisen, though. When an in-app product is configured, I need to test its purchase process before it can be brought online. Hence, I need a virtual purchase environment that doesn't actually charge me real money. Sandbox testing it is.
Aside from this, network latency or abnormal process termination can sometimes cause data of the app and the in-app purchases server to be out of synchronization. In this case, my app won't deliver the virtual products users have just purchased. This same issue can be pretty tricky for many developers and operations personnel as we don't want to see a dreaded 1 star on the "About this app" screen of our app on app stores or users venting their anger about our apps on tech forums. Of course my app lets users request a refund by filing a ticket to start the process, but guess how they feel about the extra time they have to put into this?
So I wondered how to implement sandbox testing and ensure a successful product delivery for my app. That's where HMS Core In-App Purchases (IAP) comes to the rescue. I integrated its SDK to do the trick. Let's see how it works.
Sandbox TestingSandbox testing of IAP supports end-to-end testing without real payments for joint debugging.
Preparing for Sandbox TestingI added a test account by going to Users and permissions > Sandbox > Test accounts. The test account needs to be a registered HUAWEI ID and will take effect between 30 minutes and an hour after it has been added.
As the app package I want to test hasn't been released in AppGallery Connect, its versionCode should exceed 0. For an app package once released in AppGallery Connect, the versionCode should be greater than that of the released one.
If you fail to access the sandbox when trying out the function, use the IapClient.isSandboxActivated (for Android) or HMSIAP.isSandboxActivated API (for HarmonyOS) in your app for troubleshooting.
Testing Non-Subscription PaymentsI signed in with the test account and installed the app to be tested on my phone. When a request was initiated to purchase a one-time product (stealth skill card), IAP detected that I was a test user, so it skipped the payment step and displayed a message indicating that the payment was successful.
It was impressively smooth. The purchase process in the sandbox testing environment accurately reflected what would happen in reality. I noticed that the purchaseType field on the receipt generated in IAP had a value of 0, indicating that the purchase was a sandbox test record.
Let's try out a non-consumable product — the chance to unlock a special game character. In the sandbox testing environment, I purchased it and consumed it, and then I could purchase this character again.
Sandbox testing for a one-time product on a phone
Testing Subscription RenewalThe purchase process of subscriptions is similar to that of one-time products but subscriptions have more details to consider, such as the subscription renewal result (success or failure) and subscription period. Test subscriptions renew much faster than actual subscriptions. For example, the actual subscription period is 1 week, while the test subscription renews every 3 minutes.
Sandbox testing for a subscription on a phone
Sandbox testing helps me test new products before I launch them in my app.
Consumable Product RedeliveryWhen a user purchased a consumable such as a holiday costume, my app would call an API to consume it. However, if an exception occurred, the app would fail to determine whether the payment was successful, so the purchased product might not be delivered as expected.
Note: A non-consumable or subscription will not experience such a delivery failure because they don't need to be consumed.
I turned to IAP to implement consumable redelivery. The process is as follows.
Consumable Redelivery Process
Here's my development process.
1. Call obtainOwnedPurchases to obtain the purchase data of the consumable that has been purchased but not delivered. Specify priceType as 0 in OwnedPurchasesReq.
If this API is successfully called, IAP will return an OwnedPurchasesResult object, which contains the purchase data and signature data of all products purchased but not delivered. Use the public key allocated by AppGallery Connect to verify the signature.
The data of each purchase is a character string in JSON format and contains the parameters listed in InAppPurchaseData. Parse the purchaseState field from the InAppPurchaseData character string. If purchaseState of a purchase is 0, the purchase is successful. Deliver the required product for this purchase again.
Code:
// Construct an OwnedPurchasesReq object.
OwnedPurchasesReq ownedPurchasesReq = new OwnedPurchasesReq();
// priceType: 0: consumable; 1: non-consumable; 2: subscription
ownedPurchasesReq.setPriceType(0);
// Obtain the Activity object that calls the API.
final Activity activity = getActivity();
// Call the obtainOwnedPurchases API to obtain the order information about all consumable products that have been purchased but not delivered.
Task<OwnedPurchasesResult> task = Iap.getIapClient(activity).obtainOwnedPurchases(ownedPurchasesReq);
task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
@Override
public void onSuccess(OwnedPurchasesResult result) {
// Obtain the execution result if the request is successful.
if (result != null && result.getInAppPurchaseDataList() != null) {
for (int i = 0; i < result.getInAppPurchaseDataList().size(); i++) {
String inAppPurchaseData = result.getInAppPurchaseDataList().get(i);
String inAppSignature = result.getInAppSignature().get(i);
// Use the IAP public key to verify the signature of inAppPurchaseData.
// Check the purchase status of each product if the verification is successful. When the payment has been made, deliver the required product. After a successful delivery, consume the product.
try {
InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
int purchaseState = inAppPurchaseDataBean.getPurchaseState();
} catch (JSONException e) {
}
}
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
2. Call the consumeOwnedPurchase API to consume a delivered product.
Conduct a delivery confirmation for all products queried through the obtainOwnedPurchases API. If a product is already delivered, call the consumeOwnedPurchase API to consume the product and instruct the IAP server to update the delivery status. After the consumption is complete, the server resets the product status to available for purchase. Then the product can be purchased again.
[HEADING=1]Conclusion[/HEADING]
A 1-star app rating is an unwelcome sight for any developer. For game developers in particular, one of the major barriers to their app achieving a 5-star rating is a failed virtual product delivery.
I integrated HMS Core In-App Purchases into my mobile game to implement the consumable redelivery function, so now my users can smoothly make in-app purchases. Furthermore, when I need to launch a new skill card in the game, I can perform tests without having to fork out real money thanks to the kit.
I hope this practice helps you guys tackle similar challenges. If you have any other tips about game development that you'd like to share, please leave a comment.
}
});