Androidはワンツーパンチ 三歩進んで二歩下がる

Android卵プログラマーの記録ブログ

ちよだニャンとなる会&千代田区共催「ちよだ猫まつり2017」に行ってきました

以前のエントリでご紹介したイベントに行ってきました。

chiyoda-nekofes.jp

sakura-bird1.hatenablog.com


直前まで別の用事があったのですが、幸い千代田区役所に近い場所にいたので終了1時間前に駆け込みました。
千鳥ヶ淵を歩いていたら少しだけ桜が咲いていました。

立派な千代田区役所
f:id:sakura_bird1:20170219155611j:plain

f:id:sakura_bird1:20170219155702j:plain

私が行った時はもう終了間近だったんですが、とても盛況でした。
公式のFacebookでも盛り上がっている様子がわかります。
小池都知事も来てたんですね〜。
愛犬家らしいしちょっと好感度あがっちゃう。

公式Facebookより
https://www.facebook.com/chiyodanekofes/


イベントは、猫グッズなどの販売・展示物・講演・譲渡会・企業のブースなどで成り立っており、収益が飼い主のいない猫の医療費などに使われるようになっています。
写真でどんな雰囲気だったかご紹介します。
猫グッズはかわいいものがたくさんあって見ているだけで楽しかったです。

戦利品
f:id:sakura_bird1:20170219231134j:plain


ネコロジーコーナー
f:id:sakura_bird1:20170219155707j:plain

入り口にでかいコーナーが。アルフィー(懐かしくて涙が出るリアルタイム世代。゚(゚´Д`゚)゚。)の坂崎幸之助さんが保護猫活動がまだまだマイナーだった頃に出版した本の新装版の販売コーナーがありました。
「理想は、世の中から不幸なノラ猫が1匹もいなくなること。猫写真とエッセイで綴る、心温まる保護猫たちとの日々。」
販売をなさっていた方から伺ったのですが、ちよだ猫まつりで販売する分は全額寄付だそうでこれは買うしか無いだろうと。
まだ読んでないんですが、Amazonのレビューを見ると非常に良い内容らしく坂崎さんの素晴らしさが伝わってきましたよ。
ハンカチ用意して読もうと思っています(笑)

↓アフィのリンクですいません。普通の商品ページへのリンクがキレイに出来ず。。。


ちよだニャンとなる会のチャリティくじ。はがきが当たりました。
f:id:sakura_bird1:20170219155746j:plain



日本羊毛アート学園佐藤法雪生徒作品展。すばらしくリアルで「おお~」と自然に声が出ます。
表情がすごい。猫を知っている人達が作ったとわかる。
f:id:sakura_bird1:20170219155833j:plain

f:id:sakura_bird1:20170219155839j:plain

f:id:sakura_bird1:20170219155841j:plain

f:id:sakura_bird1:20170219155852j:plain



イベントの目玉であるリアル猫ヘッド。ホントにリアルなんですよ!
f:id:sakura_bird1:20170219160549j:plain



里親さんの家庭で暮らす猫さんの写真展。みんな良かったねえ。
f:id:sakura_bird1:20170219160627j:plain


猫グッズなどの販売コーナーの一部。店舗数多かったです。
f:id:sakura_bird1:20170219155919j:plain


f:id:sakura_bird1:20170219155941j:plain


f:id:sakura_bird1:20170219155945j:plain


f:id:sakura_bird1:20170219160058j:plain


f:id:sakura_bird1:20170219160109j:plain


f:id:sakura_bird1:20170219160115j:plain


f:id:sakura_bird1:20170219160135j:plain


f:id:sakura_bird1:20170219160151j:plain


f:id:sakura_bird1:20170219160320j:plain


f:id:sakura_bird1:20170219160330j:plain


f:id:sakura_bird1:20170219160411j:plain


f:id:sakura_bird1:20170219160524j:plain


短い間だったけど実際に活動してる人をみると自分も何か!って思って良かったです。
最近はボランティア活動といっても明らかに優れたビジネス的なセンスをお持ちの方々を見るにつけ、外の世界を巻き込んで仕組みを作れるってすごいなあ、猫も助かるなあって感心していたところなのですがこちらでもそう感じました。
色々と自分を振り返って思うことがありますが、それはそれでまた!
主催の方々、参加者の方々、千代田区さん、本当にお疲れ様でした。

AndroidのTextUtilsのisEmptyメソッドとJavaのStringのisEmptyメソッドはわりと違う

年がら年中使っているAndroidのTextUtilsのisEmptyメソッドとJavaのStringのisEmptyメソッドですが、同じ名前で何が違うのか調べたメモです。
TextUtilsクラスのメソッドは全てstaticで定義されていて、JavaのisEmptyメソッドはインスタンス変数から使用する形になっているという違いはありますが、中身はどうでしょう。
それぞれのコードはこのようになっています。

java.lang.String

   public boolean isEmpty() {
        return count == 0;
    }

※countはfinal int の変数で、Stringオブジェクト初期化時にセットされる文字数です。

android.text. TextUtils

   public static boolean isEmpty(@Nullable CharSequence str) {
        if (str == null || str.length() == 0)
            return true;
        else
            return false;
    }

大きな違いは、TextUtilsのisEmptyメソッドはnullチェックがなされている部分です。
メソッドの引数がnullのオブジェクトが渡されてきてもjava.lang.NullPointerExceptionで落ちませんが、JavaのisEmptyメソッドではインスタンス変数がnullの場合は当然落ちます。

例えば次のコードはNullPointerExceptionが発生します。

    public void onFinish(String example) {
        example = null;
        if (example.isEmpty()) {
            Log.d("Example","exampleがemptyです");
        }

以上です。




「ちよだニャンとなる会」さまが「ちよだ猫まつり2017」を開催するそうです+寄付報告

こんにちは。さくらです。
2月って猫の月という感じがしますよね。2/22はにゃんにゃんにゃん!ということで猫の日ですし。

f:id:sakura_bird1:20161208110243j:plain

今日は4ヶ月ぶりぐらいに一般社団法人ちよだニャンとなる会に5000円寄付しました。
f:id:sakura_bird1:20170202195445p:plain


前回のエントリはこれです。
sakura-bird1.hatenablog.com

この頃はポケモンGoにはまっていたんですね。
最近は前ほどやらなくなってしまいました。
レベルは31まで上がっていますが、つい数日前やっとメタモンを捕獲したのがニュースって感じですっかり遅れを取っています。
他にも楽しんだスマホゲームあるけど、1ヶ月以上なかなか続かないですね。世の中のおばちゃんたちはどんなゲームを楽しんでいるのでしょう(・・?



ちよだニャンとなる会と千代田区の主催で「ちよだ猫まつり2017」が開催されるそうです!
同時に猫さんの譲渡会も開催するそうです。
chiyoda-nekofes.jp

千代田区|猫|一般社団法人ちよだニャンとなる会|News & Topics 【2月18日(土曜日)、19日(日曜日)に「ちよだ猫まつり2017」を開催します】

千代田区|猫|一般社団法人ちよだニャンとなる会|News & Topics 【2月19日(日曜日)「ちよだ猫まつり2017」会場にて譲渡会を開催します!】


2月18日と19日の二日間に渡って、東京都千代田区役所で開催されるそうです。
昨年は1万2千人もの来場者があったそうで、大変アツいイベントみたいですよ!
収益は、飼い主のいない猫の医療費などに活用されるそうです。

私は両日とも予定があるのですが、もしかしたら19日行けるかも。
行きたいな。こういった活動に携わっている方々に実際お目にかかりたいという気持ちがあります。
ネコ活もそうなのですが、実際に足を運んでみることで違う世界が広がったり、猫や他の動物を大切に思う気持ちが深まったり、携わっている方々へのリスペクトがガッツリ上がったりするので(●´ϖ`●)

超気軽に出来る二時間以内のボランティア、ねこ活に参加してきました - Androidはワンツーパンチ 三歩進んで二歩下がる


ところでなぜこのイベント情報を知ったかというと、以前寄付してから会報を送ってくださっているからです。
逆に言うとこういうアクションをくださらなかったら知ることはなかったので、情報収集について工夫しないとなと思いました。
あと郵送ってやっぱり強いですね。メールで送られたら見落としそう。

会報をちょっとご紹介します。
新しい家族として迎えられた猫さんのエピソードなどが載っています。

f:id:sakura_bird1:20170202143902j:plain

f:id:sakura_bird1:20170202143942j:plain


以上です。





にゃねっとCLUB 会員登録キャンペーン実施中!

歩きやすくて快適だワン!【ドッグランコーティング】

通院・災害時でもペットを思いやれるリュック【ペットキャリー GRAMP】

【Comelu. for Pet ペット向け乳酸菌サプリ】



緯度・経度から郵便番号を取得する

Geocoderクラスを使って緯度と経度から郵便番号を取得する方法です。
題名は郵便番号を取得するとなっていますが、住所も取得できます。
自分が郵便番号というキーワードで検索していたため自分用メモです。
developer.android.com


Gercoderを使用して住所(郵便番号)を使用する場合はバックエンドサービスを使用するので、インターネットに繋がっていないと取得が出来ません。
Gercoderが使用可能かどうかはisPresent()メソッドで確認します。
コードのサンプルはこんな感じです。

    private String retrievePostalCode(Location location) {
        final Geocoder geocoder = new Geocoder(getApplicationContext());
        if (geocoder.isPresent()) {
            try {
                List<Address> addresses = geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
                if (addresses != null) {
                    for (Address address : addresses) {
                        if (address.getLocality() != null && address.getPostalCode() != null) {
                            Log.i(TAG,address.getPostalCode());
                            return address.getPostalCode();
                        }
                    }
                } else
                    Log.i(TAG,"No location found..!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            Log.i(TAG,"Geocoder is not present");
        }
        return null;
    }





Rails + Grapeを使って超単純なapiを作ってみる

初心者の勉強記録です。
「〜らしい」や「っぽい」などの語尾が多くなっています。
手順を書き残すため、主題とは外れた内容も含みます。
間違ったり知識が最新でなかったりすると思いますのでツッコミ大歓迎でございます。


何をやりたいのか

Androidアプリのサンプル用に単純なAPIを作りたい→Grapeというgemを発見した
現在Railsに興味を持って勉強中なのでRailsを使いたい
サーバーにもデプロイしたい

Grapeの何がいいのか

Rest-LikeなAPIを簡単に作れるDSLらしい
きれいなコードを書けるらしい
使っている人が多そうで情報が豊富そう

Railsとgrapeを組み合わせる利点はあるのか

いい感じの形式を強制されるので、一度導入すれば後で拡張と保守が楽になりそう

サンプルで何を作るのか

固定のjson文字列を返すのみのAPIを作る

GET /api/v1/dummy_api/status
{ code: 1 }

公式サイトを拾い読みメモ

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

ルーティングはこのようになる

/api/v1/hussars.json -> API::V1::Hussars
/api/v1/wings.json -> API::V1::Wings
/api/v2/hussars.json -> API::V2::Hussars

ファイル構造はこのようになる


その他

簡単なアプリではクラスが多くなりすぎるように思えるが、すぐに元が取れる
rescue_from というのが例外のハンドリングメソッド
Swaggerと結合できる

実装を開始する

Railsアプリを新規で作成

$ rails new hogehoge

Railsにgrapeをインストール

Gemfileに記述する

gem 'grape'

$ bundle install ←記述後に実行しておく

config/application.rbを編集

config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]

app/api配下にapiを作るので、このフォルダを読み込むためにこの記述が必要

ルーティング

config/routes.rbを編集

mount 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.

deployment - Errors of pushing rails app to Heroku error occurred while installing sqlite3, and Bundler cannot continue - Stack Overflow
を参考にGemfileを直す

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

検証

$ curl https://sakurabird1-grape-example.herokuapp.com/api/v1/dummy_api/status
と実行すると
{"code":1}
とレスポンスが返ってくるので成功

このサンプル置き場

github.com




代々木ドッツへの行き方(シェアオフィス、コワーキングスペースConnecting The Dots Yoyogi)

※当エントリーはもくもく会に来てくれる方に情報を提供するために書いたもので、Connecting The Dotsを運営されているインクルードさまとは関係ありません。
2017/1/14の情報なので、古くなっている可能性があります。ご注意下さい。
引用やリンクはご自由にどうぞ( ´∀`)

f:id:sakura_bird1:20161119104703j:plain

公式サイト

dots.bz

所在地

〒151‐0053 東京都渋谷区代々木1-29-5 4F
JR線 代々木駅西口 徒歩1分
都営地下鉄大江戸線 代々木駅 徒歩1分
小田急線 南新宿駅 徒歩5分

JR代々木駅からの行き方

サマリー

  1. JR代々木駅の西口から出ます
  2. 降りてすぐ前にある交番の後ろに赤茶っぽいビルがあります。代々木教会 国際英語学校と書いてあります。
  3. ビルの前の信号を渡ります。
  4. 入ってすぐのガラスの扉の内側にエレベーターがあります。
  5. エレベーターで4階に行きます。
  6. 降りてすぐガラス扉があります。左側にタッチスクリーンがあり、「TOUCH TO CALL」と書いてあります。
  7. 「TOUCH TO CALL」を押して開けてもらいます。鍵がかかってなければ自分で開けて入る場合もあります。

写真

  1. JR代々木駅の西口から出ます

f:id:sakura_bird1:20161126110237j:plain

  1. 降りてすぐ前にある交番の後ろに赤茶っぽいビルがあります。代々木教会 国際英語学校と書いてあります。

f:id:sakura_bird1:20170114101414j:plain

  1. ビルの前の信号を渡ります。

f:id:sakura_bird1:20161126110430j:plain

  1. ふと後ろを振り返ると南新宿のNTTドコモ代々木ビルが見えます。

f:id:sakura_bird1:20161126110531j:plain

  1. ビルの入口です。

f:id:sakura_bird1:20170114101517j:plain

f:id:sakura_bird1:20161126110558j:plain

  1. 入ってすぐのガラスの扉の内側にエレベーターがあります。奥には三菱東京UFJ銀行のATMがあります。

f:id:sakura_bird1:20161129204457j:plain

  1. エレベーターで4階に行きます。

f:id:sakura_bird1:20161126110625j:plain

  1. 降りてすぐガラス扉があります。左側にタッチスクリーンがあり、「TOUCH TO CALL」と書いてあります。

f:id:sakura_bird1:20161126110702j:plain

  1. 「TOUCH TO CALL」を押して開けてもらいます。鍵がかかってなければ自分で開けて入る場合もあります。

f:id:sakura_bird1:20161126110708j:plain

  1. 入ってすぐのエリアがフリーエリアです。

f:id:sakura_bird1:20170114101930j:plain

f:id:sakura_bird1:20170114101927j:plain

  1. 奥の蜂の巣状のエリアは月額会員さんの使うエリアです。

f:id:sakura_bird1:20170114101938j:plain

  1. トイレは入ってすぐ右にあります。青い目印があるドアは男子トイレ、赤い目印があるドアは女子トイレの入り口です。

f:id:sakura_bird1:20161119104707j:plain

以上です。







ご不要なお酒、高く売れるって知ってました?

今夜、お泊まり女子会できます。【バリアン女子会】

LocationManagerはもう古い!Google Service の Location APIを使って現在位置を取得する

しばらく使っていない分野のAPIっていつの間にか非推奨になってたりしますよね。
変化の激しいAndroid開発で全ての変化に付いていくのは至難の業だと思います。
恥ずかしいんですが、この度久しぶりに位置情報の取得方法を調べましたら結構前に時代が変わっていました。

f:id:sakura_bird1:20161124215822p:plain

かつて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&nbsp;Serviceantoniohongkr.wordpress.com



本エントリではLocation APIについての調査と導入方法、基本的な使用方法について記述していきます。
ランタイムパーミッションやライブラリや設定の事前チェックの実装まで含めるとなかなか複雑です。

公式サイト

Android Developers の位置情報実装のトレーニングページトップ

Building Apps with Location & Maps | Android Developers

Android Developers の現在位置を取得するトレーニングページ

[Receiving Location Updates | Android Developers

Location APIだと何がいいのか

  1. 位置が正確
  2. ほとんどの場合、バッテリーのパフォーマンスと適切な精度が向上する
  3. Fused Location Providerという仕組みが導入され、精度のパラメータにもとづいてロケーションソースを適切に使用する
  4. Geofencing APIが追加され、ユーザーが指定の範囲に出入りした時にアプリに通知することができる
  5. 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();
        }

f:id:sakura_bird1:20161126165423p:plain

パーミッション宣言

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" />

ビルドバージョンがAndroid6.0(SDK 23)以上の場合実行時にユーザーからパーミッションの許可を得る

Android6.0以上はRuntime Permissionが導入されています。実行時にユーザーからパーミッションの許可を得ないとSecurityExceptionが発生してクラッシュします。サンプルコードにはパーミッションの許諾のコードが入っていますのでご参考にどうぞ。

developer.android.com

PermissionsDispatcherという素晴らしいOSSライブラリもあるので、使ってみると幸せになれるかもです。
github.com

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);

端末の位置情報アクセスが有効になっているかチェックする

端末の設定で位置情報が無効にされている場合、事前にチェックして有効にしてもらう必要があります。

GPSWi-Fiスキャンなどの適切なシステム設定を有効にする必要があります。デバイスGPSなどのサービスを直接有効にするのではなく、必要な精度/消費電力と更新間隔を指定して、デバイスが自動的にシステム設定を適切に変更します。これらの設定は、LocationRequestデータオブジェクトによって定義されます。

ここよりGoogle翻訳です。

上記の引用は微妙によくわからない説明なのですが、次のようなコードを書くと位置情報設定の状態を取得→有効にする必要があればダイアログを表示するという処理を事前に行うことが出来ます
参考 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になります。(その後上記のコードでは位置情報を取得開始するようにしています。)
f:id:sakura_bird1:20161127121846p:plain

現在位置を取得する

最新の位置を取得する場合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

画面イメージはこのようになります。
f:id:sakura_bird1:20161127213628p:plain

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);
    }
}

何かお気づきの点がありましたらお気軽にお知らせ下さい。