Location API & Google Map API

York
15 min readJun 8, 2018

--

本章要講解如何使用Location API取得手機最後位置,用Map API加入Google地圖到App。

要求取得手機位置的權限

自從Android 6.0開始,若使用到App以外的資源,除了在AndroidManifest宣告權限外,還要在App執行期間跟使用者要求權限。接下來要介紹如何在run time時跳出訊息窗進一步處理取得權限與拒絕後的情況。

首先一樣要在AndroidManifest.xml中設定要取得的權限,例如App要取得手機的精準位置:

<uses-permission  android:name=”android.permission.ACCESS_FINE_LOCATION” />

再來要跟使用者要求我們想要的權限。Android已經內建代表權限的字串變數了,因此可以直接使用,但要注意的是requestPermissions(String[], int)要求放入的是字串陣列,所以必須把代表權限的String包在陣列中。例如我們這次要取得讀取手機的位置,就直接新增一個字串陣列變數:

val PERMISSIONS_LOCATION = arrayOf(Manifest.permmission.ACCESS_FINE_LOCATION)

requestPermissions()的第二個參數要放入request code,因此我們也自己定義一個request code:

val REQUEST_LOCATION = 123  // any number you want

接著來看負責驗證權限的方法 verifyStoragePermission()

RequiresApi(Build.VERSION_CODES.O)override fun verifyStoragePermission() {   // Check if we have write permission   val permission = ActivityCompat.checkSelfPermission(activity,Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) { // We don't have permission so prompt the user requestPermissions(PERMISSIONS_LOCATION,REQUEST_EXTERNAL_STORAGE) } else { // Already have permission, do actions ... }}

呼叫requestPermissions()會跳出允許存取權限的視窗。當按下允許或取消後,會再呼叫Callback method`onRequestPermissionsResult()`,並傳入requestCode、permissions、grantResults。requestCode就是呼叫requestPermissions()時傳入的第二個參數

@RequiresApi(Build.VERSION_CODES.O)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {

}
}

最後記得在onCreate()或onStart()時呼叫verifyStoragePermission(),開始取得權限的流程。

@RequiresApi(Build.VERSION_CODES.O)
override fun onStart() {
super.onStart()
verifyStoragePermission()
}

取得裝置的最後位置座標

接下來我們要使用 Google Play services client library 取得手機最後位置的座標。

The Google Play services APK contains the individual Google services and runs as a background service in the Android OS. You interact with the background service through the client library and the service carries out the actions on your behalf.

依照官方的說明,我們知道這個library是與在Android手機背景執行的Google service互動,藉此得到位置資訊。利用client library提供的類別,我們就可以得到相關的物件。

使用前記得先在buid.gradle(app)加入library:

implementation 'com.google.android.gms:play-services-location:15.0.1'

要取得裝置最後位置的座標,需使用到FusedLocationProviderClient object,這個物件會提供較精確的最後位置資訊。呼叫LocationServices.getFusedLocationProviderClient(Context)

得到location provider client後呼叫它的 getLastLocation(),這個方法會回傳 Task object,Task是專門處理非同步工作的類別,它的作用是只要長時間的工作一旦完成,就會呼叫OnSuccessListener的onSuccess(),保證使用者能夠在此方法內得到工作回傳的結果。

因此我們需要先建個OnSuccessListener object,並實作onSuccess()方法。onSuccess()會在取得最後位置的工作完成後被呼叫,並取得location object。所以我們要在這個方法內實作取得locationc後要做的動作。

建立完在Task.addOnSuccessListener(OnSuccessListener)方法中傳入OnSuccessListener object。

fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)fusedLocationClient.lastLocation?.addOnSuccessListener { 
location: Location? ->
lastLocation = location // get Location object
}

利用座標取得實際地址

當取得擁有座標屬性的Location object後,我們要藉由 IntentService 執行取得實際地址的工作,並將結果傳回Activity。

首先建立一個新類別繼承 ResultReceiver ,這個類別負責接收由背景service傳來的查詢地址結果。

internal inner class AddressResultReceiver(handler: Handler) : ResultReceiver(handler) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
super.onReceiveResult(resultCode, resultData)
Timber.d("onReceiveResult: resultCode: ${resultCode} resultData: ${resultData}")
locationAddress = resultData?.getString(Constants.RESULT_DATA_KEY) ?: "" Timber.d("locationAddress: ${locationAddress}")
// show message if an address was found
if (resultCode == Constants.SUCCESS_RESULT) {
Toast.makeText(this@MainActivity, "定位完成", Toast.LENGTH_SHORT).show()
}
}
}

再來建立一個繼承IntentService的FetchAddressIntentService class。IntentService是一種可以在背景工作的元件,與一般Service不同的地方在於它自動建立執行緒,而且當工作執行完後會自動銷毀,不需要呼叫stopSelf()或是stopService()。在Activity呼叫startService(intent)時,會開始執行service的onStartCommand(),將intent排入隊列中。每次只會有一個 intent 傳入 onHandleIntent() callback method,我們要在此方法取得Address object。

override fun onHandleIntent(intent: Intent?) {if (intent != null) {Log.d(TAG, "onHandleIntent: ${intent}")// Pass a Locale object to the Geocoder object to ensure that the resulting address is localized to the user's geographic regionreceiver = intent.getParcelableExtra(Constants.RECEIVER)val geocoder = Geocoder(this, Locale.getDefault())var errorMessage = ""// Get the location passed to this service through an extra.val location = intent.getParcelableExtra<Location>(Constants.LOCATION_DATA_EXTRA)Log.d(TAG, "onHandleIntent: location: ${location}")// Ready-only empty listvar address = emptyList<Address>()try {// Just get single addressaddress = geocoder.getFromLocation(location.latitude, location.longitude, 1)} catch (ioException: IOException) {errorMessage = getString(R.string.service_not_available)} catch (illegalArgumentException: IllegalArgumentException) {errorMessage = "Use invalid longitude and latitude"}if(address.isEmpty()) {// If address is empty, pass error message to receiver.if (errorMessage.isEmpty()) {errorMessage = "no address found"}// deliverdeliverResultReceiver(Constants.FAILURE_RESULT, errorMessage)} else {// If address isn't empty,val address = address[0]// Fetch theval addressFragment = with(address) {(0..maxAddressLineIndex).map { getAddressLine(it) }}deliverResultReceiver(Constants.SUCCESS_RESULT, addressFragment.joinToString(separator = "\n"))}}}

依照上面程式碼,取得address object後再建立一個方法叫 deliverResultReceiver(),由此方法負責傳送結果給我們前面在activity建立的AddressResultReceiver object。

// send the results back to the requesting activityprivate fun deliverResultReceiver(resultCode: Int, message:String) {val bundle = Bundle().apply { putString(Constants.RESULT_DATA_KEY, message) }Log.d(TAG, "deliverResultReceiver bundle: ${bundle}")// send() will call onReceiveResult()receiver?.send(resultCode, bundle)}

最後一個步驟是回到Activity並建立方法 startIntentService()。在這個方法內開始一個在背景執行的 service來幫我們將位置的經緯度轉換成想要的實際地址

fun startIntentService() {Log.d(TAG, "start intentService")val intent = Intent(this, FetchAddressIntentService::class.java).apply {resultReceiver = AddressResultReceiver(Handler())putExtra(Constants.RECEIVER, resultReceiver)Log.d(TAG, "start intentService lastLocation: ${lastLocation}")putExtra(Constants.LOCATION_DATA_EXTRA, lastLocation)}// Starts intent service to get address in background.startService(intent)}

內嵌Google地圖

首先在gradle.build(app)加入library:

implementation ‘com.google.android.gms:play-services-maps:15.0.0’

Google Map API已經提供了 SupportMapFragment 可以讓你直接加地圖到Activity。我們可以直接在xml加入地圖的fragment或是在程式碼的 onCreate 區塊加入。

<fragment xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/fragment_map"android:name="com.google.android.gms.maps.SupportMapFragment"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.york.android.cafemap.view.MapsActivity" />

加完之後為了取得GoogleMap object,Activity要實作OnMapReadyCallback,因此就能在 onMapReady(p0: GoogleMap?) 這個callback method得到 GoogleMap object。

啟動Google Map App來導航

依照官方文件的範例,依規定建立一個包含座標或是住址的字串並用Uri.parse(String)轉成Uri。

// Create a Uri from an intent string. Use the result to create an Intent.
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=46.414382,10.013988");

再建立intent並傳入ACTION_VIEW及Uri object 。

// Create an Intent from gmmIntentUri. Set the action to ACTION_VIEWIntent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);

設定要啟動App的package name,限制intent所能啟動的App。

// Make the Intent explicit by setting the Google Maps packagemapIntent.setPackage("com.google.android.apps.maps");

傳入startActivity(intent)。

// Attempt to start an activity that can handle the IntentstartActivity(mapIntent);

--

--