Rails + Grapeを使って超単純なapiを作ってみる
初心者の勉強記録です。
「〜らしい」や「っぽい」などの語尾が多くなっています。
手順を書き残すため、主題とは外れた内容も含みます。
間違ったり知識が最新でなかったりすると思いますのでツッコミ大歓迎でございます。
Railsとgrapeを組み合わせる利点はあるのか
いい感じの形式を強制されるので、一度導入すれば後で拡張と保守が楽になりそう
Grape公式サイト
monterail.com
本家のサンプルリポジトリ
rails-grape-example/app at master · monterail/rails-grape-example · GitHub
参考サイト
grapeメモ - Qiita ← ここのサイトさまを見ながら作りますm(_ _)m
[Ruby on Rails]Grapeを使ってWeb APIを作成する | DevelopersIO
http://qiita.com/magaya0403/items/f9cd1340960ab997cf63
http://qiita.com/takusemba/items/a86796aa3c207155c579
公式サイトを拾い読みメモ
・APIバージョンをパスに含める構造を取る
・APIに関するコードベースをAPIモジュール配下に設置する
・format は:jsonが推奨されているらしく、xmlは
# We don't like xml anymore とか言われているが使用不可かどうかまでは調べてない
Controllerの定義は次のような形式になる
# app/controllers/api/v1/hussars.rb module API module V1 class Hussars < Grape::API version 'v1' # path-based versioning by default format :json # We don't like xml anymore resource :hussars do desc "Return list of hussars" get do Hussar.all # obviously you never want to call #all here end end end end end
このコードだとクライアントから呼び出すパスは/v1/hussars.jsonで終わる
/v1/wings.json で終わるパスであればクラスの定義は↓になる。
API::V1::Wings → app/controllers/api/v1/wings.rb
/v2/hussars.json なら
API::V2::Hussars → app/controllers/api/v2/hussars.rb
各APIバージョンについて、すべてのリソースをマウントする集約クラスが必要。
# app/controllers/api/v1/base.rb module API module V1 class Base < Grape::API mount API::V1::Hussars mount API::V1::Wings end end end
v2だと app/controllers/api/v2/base.rb.
すべてのAPIバージョンを集約するクラスが1つ必要。
# app/controllers/api/base.rb module API class Base < Grape::API mount API::V1::Base mount API::V2::Base end end
最後に集約クラスをroutes.rbに記述する
# config/routes.rb Monterail::Application.routes.draw do # ... mount API::Base => '/api' # ... end
config/application.rbを編集
config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
APIクラスを設置する(リソースを集約したクラスでもある)
class API < Grape::API prefix 'api' version 'v1', using: :path format :json helpers do def dummy_code { code: 1 } end def err401 error!('401 Unauthorized', 401) end end resource :dummy_api do get :status do dummy_code end get :secret do err401 end end end
rake routesと実行すると次のように表示される。Controllerのルーティングと違う。
$ rake routes Prefix Verb URI Pattern Controller#Action api / API
このサイト様によると
・「desc〜」で機能の説明を記述できる
・「get」「post」「put」「delete」と、HTTPのメソッドに対応した処理を定義できる
・「params〜」でパラメータを定義し、「require」で必須かを定義している
プライベートメソッドはhelpers内で行うことになっているらしい
検証
ローカルサーバーを立ち上げる
$ rails s
curlコマンドで確認する
$ curl localhost:3000/api/v1/dummy_api/status
{"code":1}
目論見どおり文字列が返ってくる
サーバーにデプロイする
herokuのアカウントを持っているのでそこにデプロイ
$ heroku login
$ heroku create sakurabird1-grape-example
Creating ⬢ sakurabird1-grape-example... done
https://sakurabird1-grape-example.herokuapp.com/ | https://git.heroku.com/sakurabird1-grape-example.git
$ git push heroku master
このようなエラーメッセージが出るので
remote: Make sure that `gem install sqlite3 -v '1.3.12'` succeeds before bundling. remote: ! remote: ! Failed to install gems via Bundler. remote: ! remote: ! Detected sqlite3 gem which is not supported on Heroku. remote: ! https://devcenter.heroku.com/articles/sqlite3 remote: ! remote: ! Push rejected, failed to compile Ruby app.
group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' gem 'sqlite3' end group :production do gem 'pg’ end
$ bundle install ←記述後に実行しておく
変更をcommitした後、最新のコードをHerokuにpush
$ git push heroku master
デプロイ成功
remote: https://sakurabird1-grape-example.herokuapp.com/ deployed to Heroku
URLはこうなる
https://sakurabird1-grape-example.herokuapp.com/api/v1/dummy_api/status
検証
$ curl https://sakurabird1-grape-example.herokuapp.com/api/v1/dummy_api/status
と実行すると
{"code":1}
とレスポンスが返ってくるので成功
代々木ドッツへの行き方(シェアオフィス、コワーキングスペースConnecting The Dots Yoyogi)
※当エントリーはもくもく会に来てくれる方に情報を提供するために書いたもので、Connecting The Dotsを運営されているインクルードさまとは関係ありません。
2017/1/14の情報なので、古くなっている可能性があります。ご注意下さい。
引用やリンクはご自由にどうぞ( ´∀`)
公式サイト
JR代々木駅からの行き方
サマリー
- JR代々木駅の西口から出ます
- 降りてすぐ前にある交番の後ろに赤茶っぽいビルがあります。代々木教会 国際英語学校と書いてあります。
- ビルの前の信号を渡ります。
- 入ってすぐのガラスの扉の内側にエレベーターがあります。
- エレベーターで4階に行きます。
- 降りてすぐガラス扉があります。左側にタッチスクリーンがあり、「TOUCH TO CALL」と書いてあります。
- 「TOUCH TO CALL」を押して開けてもらいます。鍵がかかってなければ自分で開けて入る場合もあります。
写真
- JR代々木駅の西口から出ます
- 降りてすぐ前にある交番の後ろに赤茶っぽいビルがあります。代々木教会 国際英語学校と書いてあります。
- ビルの前の信号を渡ります。
- ふと後ろを振り返ると南新宿のNTTドコモ代々木ビルが見えます。
- ビルの入口です。
- 入ってすぐのガラスの扉の内側にエレベーターがあります。奥には三菱東京UFJ銀行のATMがあります。
- エレベーターで4階に行きます。
- 降りてすぐガラス扉があります。左側にタッチスクリーンがあり、「TOUCH TO CALL」と書いてあります。
- 「TOUCH TO CALL」を押して開けてもらいます。鍵がかかってなければ自分で開けて入る場合もあります。
- 入ってすぐのエリアがフリーエリアです。
- 奥の蜂の巣状のエリアは月額会員さんの使うエリアです。
- トイレは入ってすぐ右にあります。青い目印があるドアは男子トイレ、赤い目印があるドアは女子トイレの入り口です。
以上です。
LocationManagerはもう古い!Google Service の Location APIを使って現在位置を取得する
しばらく使っていない分野のAPIっていつの間にか非推奨になってたりしますよね。
変化の激しいAndroid開発で全ての変化に付いていくのは至難の業だと思います。
恥ずかしいんですが、この度久しぶりに位置情報の取得方法を調べましたら結構前に時代が変わっていました。
かつてAndroidで位置情報を使ったコードを実装すると言えばandroid.location 以下のAPIを使用するということでした。
LocationManagerインスタンスを作ってrequestLocationUpdatesのメソッドを使って位置情報を取得し、LocationListenerでコールバックを受け取って画面などの値を更新などとやっていましたね。
現在デベロッパーサイトのandroid.locationにアクセスすると、以下のように注意書きがあります。
(デベロッパーサイトは英語ですが和訳を載せます)
このAPIは、Androidの場所にアクセスするための推奨方法ではありません。
Google Playサービスの一部であるGoogle Location Services APIは、アプリに位置情報を追加するための好ましい方法です。これは、よりシンプルなAPI、より高い精度、低消費電力のジオフェンシングなどを提供します。現在android.location APIを使用している場合は、できるだけ早くGoogle Location Services APIに切り替えることを強くおすすめします。
Google Play serviceの location APIは数年前からありましたが、Androidのフレームワークの方はdeprecatedにはなっていないもののLocation APIの方を強くおすすめされるようになってます。
2013年のGoogle I/Oで発表があったようなので少なくとも2年以上は時代に遅れていたようです。
Google I/Oは一応チェックしているので、当時大いに首を縦に振りながら納得したかもしれないのですが、
3歩歩くと忘れるので忘れました(´-﹏-`;)
参考
Google Play Service Analysis (4) – Choice between Google Play Location Service and Android Location Serviceantoniohongkr.wordpress.com
本エントリではLocation APIについての調査と導入方法、基本的な使用方法について記述していきます。
ランタイムパーミッションやライブラリや設定の事前チェックの実装まで含めるとなかなか複雑です。
公式サイト
Google APIs for Androidのリファレンス
com.google.android.gms.location | Google APIs for Android | Google Developers
Location APIだと何がいいのか
- 位置が正確
- ほとんどの場合、バッテリーのパフォーマンスと適切な精度が向上する
- Fused Location Providerという仕組みが導入され、精度のパラメータにもとづいてロケーションソースを適切に使用する
- Geofencing APIが追加され、ユーザーが指定の範囲に出入りした時にアプリに通知することができる
- Activity Recognition APIが追加され、ユーザーの行動を認識できる(徒歩なのか、車移動なのか、など)
位置情報の取得にはアプリの要件によって条件が変わってきます。
正確さ命!5秒毎に位置取得!という要件では電力消費とトレードオフとなります。
Location APIには要件に合わせた最適化をやってくれるので複雑なコードを書かなくてもよいです(๑•̀ㅂ•́)و✧
導入
このクラスはGoogle Play Serviceのクライアントライブラリを使用します。
セットアップはこちらの公式サイトもご覧ください。
Android 2.3以上で使用できます。
Set Up Google Play Services | Google APIs for Android | Google Developers
Google Play Serviceは65,536メソッドの制限を考えると、必要な機能だけ導入するほうがよいでしょう。
Google Location and Activity Recognition APIはplay-servicesの後ろに「-location」と指定します。
appモジュールのbuild.gradleに次のように記述します。10.0.0となっているところは最新のバージョン番号を書きましょう。
dependencies { compile 'com.google.android.gms:play-services-location:10.0.0' }
Google Play Service APKがユーザーの端末にはいっているかチェックする
Google Play Service APKは(日本で発売されている一般的なAndroidスマートフォンには入っていますが)全ての端末に入っているかは確実でなく、アップデートが必要な場合でもアップデートされていない場合があります。
GoogleApiClientクラスのインスタンスを作成する時に OnConnectionFailedListener をセットしましょう。
デバイスが適切なGoogle Play Serviceライブラリを持っていない場合 onConnectionFailed()にエラー結果となって返されます。
GoogleApiAvailability クラスのisGooglePlayServicesAvailable()メソッドでチェックするのもよいです。
GoogleApiAvailability | Google APIs for Android | Google Developers
例えば次のように記述すると、ユーザーの端末のGoogle Play Service apkが有効かどうかチェックして、エラー内容に合わせたダイアログを表示してくれます。
int resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); if (resultCode != ConnectionResult.SUCCESS) { GoogleApiAvailability.getInstance().getErrorDialog((Activity) context, resultCode, SOME_REQUEST_CODE).show(); }
パーミッション宣言
AndroidManifest.xmlに位置情報のパーミッションを宣言します。
ACCESS_FINE_LOCATIONはGPS_PROVIDERとNETWORK_PROVIDERを使用する場合に指定します。
ACCESS_COARSE_LOCATIONはNETWORK_PROVIDERのみを使用する場合に指定します。
ACCESS_FINE_LOCATIONを指定したら、ACCESS_COARSE_LOCATIONの指定は必要ありません。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Location APIを使って位置情報を取得する
Google Play Servicesに接続する
まずonCreate()あたりのタイミングでGoogleApiClientを生成します。この時にLocationServices.APIを追加します。
GoogleApiClient.ConnectionCallbacksとGoogleApiClient.OnConnectionFailedListener を実装します。
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { (中略) protected synchronized void buildGoogleApiClient() { Log.i(TAG, "Building GoogleApiClient"); mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(LocationServices.API) .build(); createLocationRequest(); } @Override public void onConnected(@Nullable Bundle bundle) { // 接続時の処理を記述します } @Override public void onConnectionSuspended(int i) { // 何らかの理由で接続が無くなった場合の処理を記述します mGoogleApiClient.connect(); } @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { // 接続が失敗した場合の処理を記述します。ConnectionResult のエラーコードから失敗理由がわかります。 Log.i(TAG, "Connection failed: ConnectionResult.getErrorCode() = " + connectionResult.getErrorCode()); } (中略)
接続と接続解除をonStart(),onStop()あたりのタイミングで行います。
protected void onStart() { mGoogleApiClient.connect(); super.onStart(); } protected void onStop() { mGoogleApiClient.disconnect(); super.onStop(); }
LocationRequestをセットアップする
位置情報取得の際の正確さのレベル、更新間隔など要件に合わせたパラメータをセットします。
公式サイトで全てのオプションをご確認ください。
LocationRequest | Google APIs for Android | Google Developers
Priorityの指定では電力消費と正確さのオプションが4つありますのでご紹介します。
Priority | 特徴 |
PRIORITY_BALANCED_POWER_ACCURACY | 精度は100メートル。精度は粗い。消費電力はより少ない。ネットワークの位置情報を使う可能性が高い。ロケーションプロバイダの選択は、使用可能なソースなど、他の多くの要因に依存する。 |
PRIORITY_HIGH_ACCURACY | 精度は最も正確。消費電力は多い。GPSを使用して位置を特定する可能性が高くなる。 |
PRIORITY_LOW_POWER | 精度は10km。都市レベル。消費電力は少ない。 |
PRIORITY_NO_POWER | 消費電力はごくわずか。利用可能な場合は場所の更新を受信する必要がある。この設定では、アプリは場所の更新をトリガーするのではなく、他のアプリによってトリガーされた場所を受け取る。 |
実装例
mLocationRequest = new LocationRequest();
mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
端末の位置情報アクセスが有効になっているかチェックする
端末の設定で位置情報が無効にされている場合、事前にチェックして有効にしてもらう必要があります。
GPSやWi-Fiスキャンなどの適切なシステム設定を有効にする必要があります。デバイスのGPSなどのサービスを直接有効にするのではなく、必要な精度/消費電力と更新間隔を指定して、デバイスが自動的にシステム設定を適切に変更します。これらの設定は、LocationRequestデータオブジェクトによって定義されます。
上記の引用は微妙によくわからない説明なのですが、次のようなコードを書くと位置情報設定の状態を取得→有効にする必要があればダイアログを表示するという処理を事前に行うことが出来ます
参考 Googleサンプルのgitリポジトリ(位置情報設定)
https://github.com/googlesamples/android-play-location/tree/master/LocationSettings
1. LocationSettingsRequest.Builderのインスタンスを作成しLocationRequestを追加します
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
.addLocationRequest(mLocationRequest);
2. 設定の状態を取得します
PendingResult<LocationSettingsResult> result = LocationServices.SettingsApi.checkLocationSettings(mGoogleClient, builder.build());
3. 取得した状態によって処理を進めます
result.setResultCallback(new ResultCallback<LocationSettingsResult>() { @Override public void onResult(@NonNull LocationSettingsResult locationSettingsResult) { final Status status = locationSettingsResult.getStatus(); switch (status.getStatusCode()) { case LocationSettingsStatusCodes.SUCCESS: // 設定が有効になっているので現在位置を取得する if (ContextCompat.checkSelfPermission( MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, MainActivity.this); } break; case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: // 設定が有効になっていないのでダイアログを表示する try { // Show the dialog by calling startResolutionForResult(), // and check the result in onActivityResult(). status.startResolutionForResult(MainActivity.this, REQUEST_CHECK_SETTINGS); } catch (IntentSender.SendIntentException e) { // Ignore the error. } break; case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: // Location settings are not satisfied. However, we have no way // to fix the settings so we won't show the dialog. break; } } }); @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CHECK_SETTINGS: switch (resultCode) { case Activity.RESULT_OK: startLocationUpdates(); break; case Activity.RESULT_CANCELED: break; } break; } }
設定画面の位置情報をOffにすると、次のようなダイアログを表示してくれます。
OKを押すと設定がOnになります。(その後上記のコードでは位置情報を取得開始するようにしています。)
現在位置を取得する
最新の位置を取得する場合LocationServices.FusedLocationApi.requestLocationUpdatesメソッドを使用します。
位置情報のコールバックには、com.google.android.gms.location.LocationListenerを実装します。
既に取得済みの最新の位置を取得するにはLocationServices.FusedLocationApi.getLastLocationメソッドを使用します。
LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, MainActivity.this); @Override public void onLocationChanged(Location location) { Log.i(TAG, "onLocationChanged"); // 取得した位置情報を使用して処理を記述します mCurrentLocation = location; mLastUpdateTime = DateFormat.getTimeInstance().format(new Date()); updateUI(); Toast.makeText(this, getResources().getString(R.string.location_updated_message), Toast.LENGTH_SHORT).show(); }
サンプルでは10秒おきに位置情報を取得しているのですが、消費電力を考慮しonPause()で取得処理をストップし、onResume()で再開しています。
@Override public void onResume() { super.onResume(); if (mGoogleApiClient.isConnected() && mRequestingLocationUpdates) { startLocationUpdates(); } } @Override protected void onPause() { super.onPause(); // Stop location updates to save battery, but don't disconnect the GoogleApiClient object. if (mGoogleApiClient.isConnected()) { stopLocationUpdates(); } }
サンプルコード
長くなりましたが、これらのサンプルはまとめると以下のようになります。
当エントリーではGoogleのサンプルを元にnullチェックやパーミッションチェックなどを独自に加えています。
ボタンを押すと10秒間隔で位置情報を更新します。
Githubにもプロジェクト全体をアップロードしてあります。
github.com
画面イメージはこのようになります。
package com.sakurafish.exam.location.api; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager; import android.databinding.DataBindingUtil; import android.location.Location; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.Toast; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import com.google.android.gms.location.LocationSettingsRequest; import com.google.android.gms.location.LocationSettingsResult; import com.google.android.gms.location.LocationSettingsStatusCodes; import com.sakurafish.exam.location.api.databinding.ActivityMainBinding; import java.text.DateFormat; import java.util.Date; /** * Retrieve current location using Google Play Services Location API * Based on "https://github.com/googlesamples/android-play-location/tree/master/LocationUpdates" */ public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { protected static final String TAG = "location-updates-sample"; /** * 10秒間隔で位置情報を更新。実際には多少頻度が多くなるかもしれない。 */ public static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000; /** * 最速の更新間隔。この値より頻繁に更新されることはない。 */ public static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = UPDATE_INTERVAL_IN_MILLISECONDS / 2; private final static String REQUESTING_LOCATION_UPDATES_KEY = "requesting-location-updates-key"; private final static String LOCATION_KEY = "location-key"; private final static String LAST_UPDATED_TIME_STRING_KEY = "last-updated-time-string-key"; private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1; private static final int REQUEST_CHECK_SETTINGS = 10; private ActivityMainBinding mBinding; private GoogleApiClient mGoogleApiClient; private LocationRequest mLocationRequest; private Location mCurrentLocation; private Boolean mRequestingLocationUpdates; private String mLastUpdateTime; private String mLatitudeLabel; private String mLongitudeLabel; private String mLastUpdateTimeLabel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); mLatitudeLabel = getResources().getString(R.string.latitude_label); mLongitudeLabel = getResources().getString(R.string.longitude_label); mLastUpdateTimeLabel = getResources().getString(R.string.last_update_time_label); mRequestingLocationUpdates = false; mLastUpdateTime = ""; updateValuesFromBundle(savedInstanceState); buildGoogleApiClient(); } private void updateValuesFromBundle(Bundle savedInstanceState) { Log.i(TAG, "Updating values from bundle"); if (savedInstanceState != null) { if (savedInstanceState.keySet().contains(REQUESTING_LOCATION_UPDATES_KEY)) { mRequestingLocationUpdates = savedInstanceState.getBoolean( REQUESTING_LOCATION_UPDATES_KEY); setButtonsEnabledState(); } if (savedInstanceState.keySet().contains(LOCATION_KEY)) { mCurrentLocation = savedInstanceState.getParcelable(LOCATION_KEY); } if (savedInstanceState.keySet().contains(LAST_UPDATED_TIME_STRING_KEY)) { mLastUpdateTime = savedInstanceState.getString(LAST_UPDATED_TIME_STRING_KEY); } updateUI(); } } protected synchronized void buildGoogleApiClient() { Log.i(TAG, "Building GoogleApiClient"); mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(LocationServices.API) .build(); createLocationRequest(); } protected void createLocationRequest() { mLocationRequest = new LocationRequest(); mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS); mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS); mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); } public void startUpdatesButtonHandler(View view) { clearUI(); if (!isPlayServicesAvailable(this)) return; if (!mRequestingLocationUpdates) { mRequestingLocationUpdates = true; } else { return; } if (Build.VERSION.SDK_INT < 23) { setButtonsEnabledState(); startLocationUpdates(); return; } if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { setButtonsEnabledState(); startLocationUpdates(); } else { if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.ACCESS_FINE_LOCATION)) { showRationaleDialog(); } else { ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION); } } } public void stopUpdatesButtonHandler(View view) { if (mRequestingLocationUpdates) { mRequestingLocationUpdates = false; setButtonsEnabledState(); stopLocationUpdates(); } } private void startLocationUpdates() { Log.i(TAG, "startLocationUpdates"); LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() .addLocationRequest(mLocationRequest); // 現在位置の取得の前に位置情報の設定が有効になっているか確認する PendingResult<LocationSettingsResult> result = LocationServices.SettingsApi.checkLocationSettings(mGoogleApiClient, builder.build()); result.setResultCallback(new ResultCallback<LocationSettingsResult>() { @Override public void onResult(@NonNull LocationSettingsResult locationSettingsResult) { final Status status = locationSettingsResult.getStatus(); switch (status.getStatusCode()) { case LocationSettingsStatusCodes.SUCCESS: // 設定が有効になっているので現在位置を取得する if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, MainActivity.this); } break; case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: // 設定が有効になっていないのでダイアログを表示する try { status.startResolutionForResult(MainActivity.this, REQUEST_CHECK_SETTINGS); } catch (IntentSender.SendIntentException e) { // Ignore the error. } break; case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: // Location settings are not satisfied. However, we have no way // to fix the settings so we won't show the dialog. break; } } }); } private void setButtonsEnabledState() { if (mRequestingLocationUpdates) { mBinding.startUpdatesButton.setEnabled(false); mBinding.stopUpdatesButton.setEnabled(true); } else { mBinding.startUpdatesButton.setEnabled(true); mBinding.stopUpdatesButton.setEnabled(false); } } private void clearUI() { mBinding.latitudeText.setText(""); mBinding.longitudeText.setText(""); mBinding.lastUpdateTimeText.setText(""); } private void updateUI() { if (mCurrentLocation == null) return; mBinding.latitudeText.setText(String.format("%s: %f", mLatitudeLabel, mCurrentLocation.getLatitude())); mBinding.longitudeText.setText(String.format("%s: %f", mLongitudeLabel, mCurrentLocation.getLongitude())); mBinding.lastUpdateTimeText.setText(String.format("%s: %s", mLastUpdateTimeLabel, mLastUpdateTime)); } protected void stopLocationUpdates() { Log.i(TAG, "stopLocationUpdates"); // The final argument to {@code requestLocationUpdates()} is a LocationListener // (http://developer.android.com/reference/com/google/android/gms/location/LocationListener.html). LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { setButtonsEnabledState(); startLocationUpdates(); } else { if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { mRequestingLocationUpdates = false; Toast.makeText(MainActivity.this, "このアプリの機能を有効にするには端末の設定画面からアプリの位置情報パーミッションを有効にして下さい。", Toast.LENGTH_SHORT).show(); } else { showRationaleDialog(); } } break; } } } private void showRationaleDialog() { new AlertDialog.Builder(this) .setPositiveButton("許可する", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION); } }) .setNegativeButton("しない", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Toast.makeText(MainActivity.this, "位置情報パーミッションが許可されませんでした。", Toast.LENGTH_SHORT).show(); mRequestingLocationUpdates = false; } }) .setCancelable(false) .setMessage("このアプリは位置情報の利用を許可する必要があります。") .show(); } public static boolean isPlayServicesAvailable(Context context) { // Google Play Service APKが有効かどうかチェックする int resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); if (resultCode != ConnectionResult.SUCCESS) { GoogleApiAvailability.getInstance().getErrorDialog((Activity) context, resultCode, 2).show(); return false; } return true; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CHECK_SETTINGS: switch (resultCode) { case Activity.RESULT_OK: startLocationUpdates(); break; case Activity.RESULT_CANCELED: break; } break; } } @Override protected void onStart() { super.onStart(); mGoogleApiClient.connect(); } @Override public void onResume() { super.onResume(); isPlayServicesAvailable(this); // Within {@code onPause()}, we pause location updates, but leave the // connection to GoogleApiClient intact. Here, we resume receiving // location updates if the user has requested them. if (mGoogleApiClient.isConnected() && mRequestingLocationUpdates) { startLocationUpdates(); } } @Override protected void onPause() { super.onPause(); // Stop location updates to save battery, but don't disconnect the GoogleApiClient object. if (mGoogleApiClient.isConnected()) { stopLocationUpdates(); } } @Override protected void onStop() { stopLocationUpdates(); mGoogleApiClient.disconnect(); super.onStop(); } @Override protected void onDestroy() { super.onDestroy(); } @Override public void onConnected(@Nullable Bundle bundle) { Log.i(TAG, "onConnected"); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } if (mCurrentLocation == null) { mCurrentLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); mLastUpdateTime = DateFormat.getTimeInstance().format(new Date()); updateUI(); } if (mRequestingLocationUpdates) { startLocationUpdates(); } } @Override public void onLocationChanged(Location location) { Log.i(TAG, "onLocationChanged"); mCurrentLocation = location; mLastUpdateTime = DateFormat.getTimeInstance().format(new Date()); updateUI(); Toast.makeText(this, getResources().getString(R.string.location_updated_message), Toast.LENGTH_SHORT).show(); } @Override public void onConnectionSuspended(int i) { // The connection to Google Play services was lost for some reason. We call connect() to // attempt to re-establish the connection. Log.i(TAG, "Connection suspended"); mGoogleApiClient.connect(); } @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { // Refer to the javadoc for ConnectionResult to see what error codes might be returned in // onConnectionFailed. Log.i(TAG, "Connection failed: ConnectionResult.getErrorCode() = " + connectionResult.getErrorCode()); } public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putBoolean(REQUESTING_LOCATION_UPDATES_KEY, mRequestingLocationUpdates); savedInstanceState.putParcelable(LOCATION_KEY, mCurrentLocation); savedInstanceState.putString(LAST_UPDATED_TIME_STRING_KEY, mLastUpdateTime); super.onSaveInstanceState(savedInstanceState); } }
何かお気づきの点がありましたらお気軽にお知らせ下さい。