Androidアプリ内でGoogle Maps Platformを使う

Androidアプリにて、Google Maps Platformを動かすようにするまでで少し詰まったので、まとめておきます。

# はじめに

[Google Maps Platform](https://cloud.google.com/maps-platform/?hl=ja)は

- マップ
- ルート
- プレイス

サービスがあって、それぞれ

- Directions API
- Distance Matrix API
- Elevation API
- Geocoding API
- Maps Static API
- Places API
- Roads API
- Time Zone API

のAPIの種類があります。このAPIはWebのURLを通して叩くと、JSONファイルとして取得できます。例えば[公式を参考に](https://developers.google.com/maps/documentation/geocoding/start)、緯度・経度が35.684064, 139.774517の場所を知りたければ、`API_KEY`が登録されているキーだとして、WebブラウザのURLに

```
https://maps.googleapis.com/maps/api/geocode/json?latlng=35.684064,139.774517&key=YOUR_API_KEY
```
とアクセスすると

```json
{
   "plus_code" : {
      "compound_code" : "MQMF+JR 日本、東京都東京",
      "global_code" : "8Q7XMQMF+JR"
   },
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "日本橋",
               "short_name" : "日本橋",
               "types" : [ "establishment", "point_of_interest" ]
            },
            {
               "long_name" : "１ 国道1号",
               "short_name" : "１ 国道1号",
               "types" : [ "premise" ]
            },
            {
               "long_name" : "８",
               "short_name" : "８",
               "types" : [ "political", "sublocality", "sublocality_level_4" ]
            },
            {
               "long_name" : "１丁目",
               "short_name" : "１丁目",
               "types" : [ "political", "sublocality", "sublocality_level_3" ]
            },
            {
               "long_name" : "日本橋室町",
               "short_name" : "日本橋室町",
               "types" : [ "political", "sublocality", "sublocality_level_2" ]
            },
            {
               "long_name" : "中央区",
               "short_name" : "中央区",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "東京都",
               "short_name" : "東京都",
               "types" : [ "administrative_area_level_1", "political" ]
            },
            {
               "long_name" : "日本",
               "short_name" : "JP",
               "types" : [ "country", "political" ]
            },
            {
               "long_name" : "103-0022",
               "short_name" : "103-0022",
               "types" : [ "postal_code" ]
            }
         ],
...
```
と、先程の緯度経度に対応する日本橋の住所や情報が出力されます。

このAPI、このようにWebページから叩けるのですが、NativeなJava, Python, Go, Node.js用にクライアントライブラリが公式で用意されています[Google Maps (GitHub)](https://github.com/googlemaps)。

今回は、その中でのJava用のツールを用いて、Androidアプリ内で使用出来るまでの設定を行います。

# 前提

- 既にGoogle Cloud PlatformにてGoogle MapsのAPI Keyは取得しています
- Android studio 3.5.3
- Android Gradle Plugin 3.5.3
- Gradle Version 5.4.1

# 実装

## Activityを持ったProjectを作る

## クライアントライブラリをインポートする

 Google Maps Platform Javaクライアントライブラリを, **アプリの方の** (プロジェクトの方ではありません) `build.gladle`に以下をスコープの外に追記します。

```build.gradle(app)
repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'com.google.maps:google-maps-services:0.10.0'
    implementation 'org.slf4j:slf4j-simple:1.7.26'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
    implementation 'com.squareup.okhttp3:okhttp:3.4.1'
}
```

google-maps-servicesのバージョンは最新版が0.10.1ですが、ここを0.10.1にすると動きませんでしたので、バージョンを落として0.10.0にしています。また、JSONを取得するのに`gson`, HTTP通信を行うのに必要なモジュール類も入れています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255755/e3fed87d-5019-2012-5e59-6716c66505c2.png)

書き終わりましたら、右上にある`Sync Now`をクリックします。

## Maps APIを準備する

`strings.xml`に書いても良いかもしれませんが、今回は`app/res/values/google_maps_api.xml`ファイルを作って、そちらに書きます。

```app/res/values/google_maps_api.xml
<resources>
    <string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">API_KEY</string>
</resources>
```


## MainActivityのレイアウトを作る

出力形式をJSONにするので、`ScrollView`で`TextView`をくくります。

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

   <ScrollView
       android:layout_width="match_parent"
       android:layout_height="match_parent">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="TextView"
        app:layout_constraintTop_toTopOf="parent"
        tools:layout_editor_absoluteX="179dp" />
   </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
```

## MainActivityを書く

Activityの中身を書いていきます。今回は勉強中のKotlinで書いてみました。

```MainActivity.kt
package com.example.googlemaps_test_kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.google.gson.GsonBuilder
import com.google.maps.GeoApiContext
import com.google.maps.GeocodingApi
import com.google.maps.model.LatLng
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val context = GeoApiContext.Builder()
            .apiKey(getString(R.string.google_maps_key)) //API KEYを取得
            .build()

        val latLng = LatLng(35.684064, 139.774517)
        val results = GeocodingApi.reverseGeocode(context, latLng).language("ja").awaitIgnoreError()
        val gson = GsonBuilder().setPrettyPrinting().create()

        textView.apply {
            text = gson.toJson(results[0].addressComponents)
        }
    }
}
```

(例外処理などはとりあえずなしで書いてしまいました)
Kotlinでも何一つ迷わずにJavaのライブラリを利用出来ました。

## インターネット接続の権限をマニフェストに追加する

これを忘れていてずっと悩んでいた…
HTTPでAPIを叩くので、必要な権限です。

```AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
```
を追加

# 結果
![Screenshot_1575884138.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255755/4b998378-2bf8-5691-61b8-faede50140c5.png)

URLからAPIを叩いたのと同じ結果になりました!
あとは、緯度経度をGPSで取得したり、他のAPIを使ったり、その結果をGSONで煮たり焼いたりすれば、色々と楽しいことができそうです!

# 2019/12/18 追記

Java clientからAPIを直接叩くのは危険だと分かりました。
API Keyには何らかの制限をかけて無断で使われることを保護します。
今回、このテストを行ったときはIPアドレスでフィルタリングしてAPI Keyを保護していましたが、途中からAndroidアプリのパッケージ名とFingerprintで保護する形式に変えたところ、Java client経由でAPIがたたけなくなってしまいました。
[Roads APIのサンプル](https://github.com/googlemaps/roads-api-samples/blob/master/app/src/debug/res/values/api_keys.xml)に

> The code supporting this sample has been provided as a single Android app for illustrative purposes. In practice you should not distribute your server-side API keys in an Android app as your key cannot be secured against unauthorised access from a third party. Instead, to secure your keys you should deploy the API-facing code as a server-side proxy and have your Android app send requests via the proxy, ensuring requests are authorized.

とあります。
アプリでAPIを使用するときはプロキシサーバーを立てて、そこ経由でリクエストするなどの対策が必要です。


# 参考

google maps platform java client library(READMEにサンプルコードが載っています)
https://github.com/googlemaps/google-maps-services-java
Gradleについて(TIPS \# 2とか)
http://gihyo.jp/dev/serial/01/android_studio/0006?page=1
Android studio 3から`compile` -> `implementation`に変わった
http://tech.hikware.com/article/20180330a.html
Buildのエラー対処
https://stackoverflow.com/questions/39545629/noclassdeffounderror-failed-resolution-of-lokhttp3-internal-platform
https://stackoverflow.com/questions/17360924/securityexception-permission-denied-missing-internet-permission