Bạn đã bao giờ tự hỏi làm thế nào các ứng dụng bản đồ, giao hàng hay mạng xã hội lại có thể biết chính xác vị trí của bạn không? Chìa khóa nằm ở Android Location API một công cụ mạnh mẽ cho phép các nhà phát triển truy cập và sử dụng dữ liệu vị trí trên thiết bị di động. Việc nắm vững API này không chỉ giúp bạn tạo ra những ứng dụng thông minh hơn mà còn mở ra vô vàn khả năng cho các dịch vụ dựa trên vị trí (location-based services).
Trong bài blog này, chúng ta sẽ cùng nhau khám phá cách sử dụng Android Location API để lấy vị trí hiện tại của người dùng một cách lập trình, từ đó hiển thị nó trong ứng dụng của chúng ta. Chúng ta sẽ đi sâu vào các khái niệm cốt lõi, cách cấu hình và triển khai code.
Giới thiệu về Android Location API
Có hai cách chính để thu thập vị trí người dùng trong ứng dụng Android của chúng ta:
android.location.LocationListener
: Đây là một phần của Android API cốt lõi.com.google.android.gms.location.LocationListener
: Thuộc về Google Play Services API. (
Android Location Services đã có mặt từ Android API 1. Mặc dù Google chính thức khuyến nghị sử dụng Google Play Location Service APIs cho các ứng dụng mới, Android Location Services API vẫn được sử dụng rộng rãi để phát triển các ứng dụng dựa trên vị trí cho những thiết bị không hỗ trợ Google Play Services.
Tìm hiểu LocationListener
Interface LocationListener
là một phần của Android Locations API, được sử dụng để nhận thông báo từ LocationManager
khi vị trí đã thay đổi. Class LocationManager
cung cấp quyền truy cập vào các dịch vụ định vị của hệ thống. Class LocationListener
cần phải implement các phương thức sau:
onLocationChanged(Location location)
: Được gọi khi vị trí đã thay đổi.onProviderDisabled(String provider)
: Được gọi khi nhà cung cấp (provider) bị người dùng tắt.onProviderEnabled(String provider)
: Được gọi khi nhà cung cấp được người dùng bật.onStatusChanged(String provider, int status, Bundle extras)
: Được gọi khi trạng thái của nhà cung cấp thay đổi.
Gói android.location
có hai phương tiện để thu thập dữ liệu vị trí:
LocationManager.GPS_PROVIDER
: Xác định vị trí bằng cách sử dụng vệ tinh. Tùy thuộc vào điều kiện, nhà cung cấp này có thể mất một thời gian để trả về vị trí chính xác.LocationManager.NETWORK_PROVIDER
: Xác định vị trí dựa trên sự có sẵn của các trạm phát sóng di động và điểm truy cập WiFi gần đó. Phương pháp này nhanh hơnGPS_PROVIDER
.
Trong hướng dẫn này, chúng ta sẽ tạo một Service
implement class LocationListener
để nhận các bản cập nhật vị trí định kỳ thông qua GPS Providers hoặc Network Providers.
Cấu trúc dự án Android Location API
Dự án của chúng ta bao gồm một class MainActivity.java
hiển thị nút “Get Location” và một class Service
tên là LocationTrack.java
.
Code Android Location API
Layout activity_main.xml
được định nghĩa dưới đây:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
xmlns:tools="<https://schemas.android.com/tools>"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.journaldev.gpslocationtracking.MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btn"
android:layout_centerInParent="true"
android:text="GET LOCATION" />
</RelativeLayout>
Class MainActivity.java
được định nghĩa dưới đây:
package com.journaldev.gpslocationtracking;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.util.ArrayList;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
public class MainActivity extends AppCompatActivity {
private ArrayList permissionsToRequest;
private ArrayList permissionsRejected = new ArrayList();
private ArrayList permissions = new ArrayList();
private final static int ALL_PERMISSIONS_RESULT = 101;
LocationTrack locationTrack;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
permissions.add(ACCESS_FINE_LOCATION);
permissions.add(ACCESS_COARSE_LOCATION);
permissionsToRequest = findUnAskedPermissions(permissions);
//get the permissions we have asked for before but are not granted..
//we will store this in a global list to access later.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (permissionsToRequest.size() > 0)
requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]), ALL_PERMISSIONS_RESULT);
}
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
locationTrack = new LocationTrack(MainActivity.this);
if (locationTrack.canGetLocation()) {
double longitude = locationTrack.getLongitude();
double latitude = locationTrack.getLatitude();
Toast.makeText(getApplicationContext(), "Longitude:" + Double.toString(longitude) + "\\\\nLatitude:" + Double.toString(latitude), Toast.LENGTH_SHORT).show();
} else {
locationTrack.showSettingsAlert();
}
}
});
}
private ArrayList findUnAskedPermissions(ArrayList wanted) {
ArrayList result = new ArrayList();
for (String perm : wanted) {
if (!hasPermission(perm)) {
result.add(perm);
}
}
return result;
}
private boolean hasPermission(String permission) {
if (canMakeSmores()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED);
}
}
return true;
}
private boolean canMakeSmores() {
return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case ALL_PERMISSIONS_RESULT:
for (String perms : permissionsToRequest) {
if (!hasPermission(perms)) {
permissionsRejected.add(perms);
}
}
if (permissionsRejected.size() > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) {
showMessageOKCancel("These permissions are mandatory for the application. Please allow access.",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(permissionsRejected.toArray(new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT);
}
}
});
return;
}
}
}
break;
}
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
locationTrack.stopListener();
}
}
Trong đoạn code trên, chúng ta đang triển khai cơ chế cấp quyền thời gian chạy (runtime permissions) được sử dụng trên các thiết bị Android 6.0+. Chúng ta đã thêm quyền ACCESS_FINE_LOCATION
và ACCESS_COARSE_LOCATION
vào file AndroidManifest.xml
. Khi người dùng nhấp vào nút, service class LocationTrack.java
sẽ được gọi. Nếu vị trí trả về là NULL
(thường xảy ra với GPS Provider), chúng ta gọi phương thức showSettingsAlert()
từ class LocationTrack.java
mà chúng ta sẽ tìm hiểu ngay sau đây. Khi activity bị hủy, phương thức stopListener()
được gọi để tắt các cập nhật vị trí. Class LocationTrack.java
được định nghĩa dưới đây:
public class LocationTrack extends Service implements LocationListener {
private final Context mContext;
boolean checkGPS = false;
boolean checkNetwork = false;
boolean canGetLocation = false;
Location loc;
double latitude;
double longitude;
private static final long MIN_DISTANCE_CHANGE_FOR_UPDATES = 10;
private static final long MIN_TIME_BW_UPDATES = 1000 * 60 * 1;
protected LocationManager locationManager;
public LocationTrack(Context mContext) {
this.mContext = mContext;
getLocation();
}
private Location getLocation() {
try {
locationManager = (LocationManager) mContext
.getSystemService(LOCATION_SERVICE);
// get GPS status
checkGPS = locationManager
.isProviderEnabled(LocationManager.GPS_PROVIDER);
// get network provider status
checkNetwork = locationManager
.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
if (!checkGPS && !checkNetwork) {
Toast.makeText(mContext, "No Service Provider is available", Toast.LENGTH_SHORT).show();
} else {
this.canGetLocation = true;
// if GPS Enabled get lat/long using GPS Services
if (checkGPS) {
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
}
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
MIN_TIME_BW_UPDATES,
MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
if (locationManager != null) {
loc = locationManager
.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (loc != null) {
latitude = loc.getLatitude();
longitude = loc.getLongitude();
}
}
}
/*if (checkNetwork) {
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
}
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
MIN_TIME_BW_UPDATES,
MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
if (locationManager != null) {
loc = locationManager
.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}
if (loc != null) {
latitude = loc.getLatitude();
longitude = loc.getLongitude();
}
}*/
}
} catch (Exception e) {
e.printStackTrace();
}
return loc;
}
public double getLongitude() {
if (loc != null) {
longitude = loc.getLongitude();
}
return longitude;
}
public double getLatitude() {
if (loc != null) {
latitude = loc.getLatitude();
}
return latitude;
}
public boolean canGetLocation() {
return this.canGetLocation;
}
public void showSettingsAlert() {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(mContext);
alertDialog.setTitle("GPS is not Enabled!");
alertDialog.setMessage("Do you want to turn on GPS?");
alertDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
mContext.startActivity(intent);
}
});
alertDialog.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
alertDialog.show();
}
public void stopListener() {
if (locationManager != null) {
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
locationManager.removeUpdates(LocationTrack.this);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onLocationChanged(Location location) {
}
@Override
public void onStatusChanged(String s, int i, Bundle bundle) {
}
@Override
public void onProviderEnabled(String s) {
}
@Override
public void onProviderDisabled(String s) {
}
}
Một vài điểm đáng chú ý từ đoạn code trên:
- Phương thức
isProviderEnabled(String provider)
được gọi trên đối tượnglocationManager
và được dùng để kiểm tra xem GPS/Network Provider đã được bật hay chưa. - Nếu các Providers chưa được bật, chúng ta gọi phương thức
showSettingsAlert()
để hiển thị lời nhắc bật GPS. - Phương thức
requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener)
của classLocationManager
được sử dụng để đăng ký activity hiện tại để được nhà cung cấp thông báo định kỳ. onLocationChanged
được gọi định kỳ dựa trênminTime
vàminDistance
, tùy điều kiện nào đến trước.- Class
Location
chứa các giá trị vĩ độ (latitude
) và kinh độ (longitude
). Để lấy vị trí hiện tại, đoạn code sau được sử dụng:Location loc = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
Trên đối tượng
Location
ở trên, các phương thức getter được gọi để lưu trữ các giá trịdouble
của vĩ độ và kinh độ. Các giá trị này sau đó được hiển thị dưới dạng một tin nhắnToast
trên màn hình. - Để dừng các cập nhật vị trí, phương thức
removeUpdates
được gọi trên instance củaLocationManager
.
Giờ đây, chúng ta đã nắm được cách triển khai, hãy cùng xem kết quả thực tế và những lưu ý quan trọng.
Kết quả của ứng dụng trên trình giả lập:
Trình giả lập của chúng ta không thể lấy được vị trí thực tế, do đó nó trả về 0.0 cho lat/lng. Bạn có thể kết nối điện thoại thông minh của mình và chạy ứng dụng ở chế độ gỡ lỗi (debug mode) để kiểm tra vị trí hiện tại của mình. Để mô phỏng vị trí GPS trong trình giả lập, chúng ta có thể truyền các giá trị Vĩ độ và Kinh độ cố định từ Android Studio. Bên cạnh cửa sổ trình giả lập, bạn có thể thấy một danh sách các tùy chọn. Tùy chọn ở dưới cùng (có ba dấu chấm) là tùy chọn Kiểm soát mở rộng (Extended Controls). Mở nó ra và gửi một vị trí giả. Ứng dụng của bạn sẽ trông như thế này:
Kết luận
Như vậy, chúng ta đã cùng nhau tìm hiểu sâu về cách triển khai tính năng định vị trên Android bằng cách sử dụng Android Location API cốt lõi, đặc biệt là interface LocationListener
.
Khả năng định vị người dùng là một yếu tố then chốt trong rất nhiều loại ứng dụng hiện đại, từ bản đồ, du lịch, thể thao, đến các dịch vụ giao hàng hay an toàn cá nhân. Việc thành thạo API này sẽ mở ra nhiều cơ hội để bạn phát triển những ứng dụng sáng tạo và hữu ích.