読者です 読者をやめる 読者になる 読者になる

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

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

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

ruby on rails

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


何をやりたいのか

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 2号店)への行き方

勉強会 その他

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

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:20161129202453j:plain

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

f:id:sakura_bird1:20161126110430j:plain

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

f:id:sakura_bird1:20161126110531j:plain

  1. ビルの入口です。

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


以上です。







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

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

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

Android

しばらく使っていない分野の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以上で使用できます。

Setting 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リポジトリ(位置情報設定)
android-play-location/LocationSettings at master · googlesamples/android-play-location · GitHub


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

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







Webセキュリティサービス「SiteLock」






ネコ活@東京キャットガーディアンで一日ボランティア体験してきました

その他 Save Animal ねこ

こんにちは。さくらです。
私は現在飼っていないのですが、猫に対する愛は常に心の底に流れています!
といってもまた猫を迎えるという気になれず、ささやかな寄付やお友達のお宅の可愛いにゃんこさんと遊ぶ程度しか猫の世界に接していません。
このまま猫の世界と交差することなく遠くで眺めているだけなのかなーと思うと、それも寂しかったりします。
猫の世界に対する方針(なんだそれ笑)が決定せず、デモデモダッテとグズグズしている状態が現状です。
猫を飼うということは、あと20年生きるという決意をすることなんですよね。猫の寿命は長いと20年近くなりますから。
前のにゃんこ3匹は拾われて、うちに連れてこられて、考える暇がなかったですからね。
今の私にとっては重い決断です。

f:id:sakura_bird1:20161122172619j:plain


そんなこととは別にネットの海をぶらぶらしてたら見つけました。

tcg.ldblog.jp


一日だけ気軽にボランティア出来るし、猫さん達も見られるらしい、といい感じではありませんか!
さっそくブログ記事にあるメールアドレス宛にメールを送って参加登録しました。
返信で当日用意することなど、わかりやすくご案内下さいました。


このネコ活を開催している「東京キャットガーディアン」という団体は一体何でしょうか。
実は過去こちらに寄付した記事を書いています。

sakura-bird1.hatenablog.com


殺処分ゼロを目指して、多くの猫を譲渡する活動を中心として保護猫カフェやペット用品リサイクル販売他
様々な活動をされています。
詳しくはこちらで

東京キャットガーディアンについて - NPO法人 東京キャットガーディアン〜子猫の里親募集〜


当日は猫の毛などが付いてもよいような服装で行ってきました。
10人ぐらいは他にもボランティア参加の方が来ていました。
みんな女性でした(*‘ω‘ *)

私が参加した11月22日は火曜日なのでスカイシェルターという猫カフェスペースはおやすみなのですが、
今日だけ特別に、とネコ活の私達をお招きいただきました。
ちなみにお休みの日はまだ人に慣れていない子を出したり、色々とあるそうです。

ボランティアの作業内容は、掃除に使用する古タオルを小さくカットすることでした。
猫さんが足元をウロウロしてるし、他の方の話を聞いてたりしたらあっという間でした。

f:id:sakura_bird1:20161122172834j:plain

上の写真はゴミではありません。
カットしたタオルの一部です。この袋3つ分ぐらいは出来たのではないかな。
それにしても全員猫好きであるという空間はいいですね。
楽しかったです。
もっとお手伝いしたくなりました。
また行きます。今度行くときは早く行って猫カフェで猫とまったり戯れたいです。

今月の寄付はシェルターで寄付した2000円です。いつもより少ない(;´∀`)
ちょっと収入減りますんで、今後寄附金額少なくなるかもです( ;∀;)

f:id:sakura_bird1:20161122172548j:plain


実際は写真に写っていないところにいっぱい猫がいます。




DataBindingを使おうとしてバインディングクラスがcannot resolve などとエラーになった時に確認すること

Android

DataBindingを使う時に、プロジェクト生成時に作成したactivity_main.xmlとMainActivity.javaバインディングしようとしただけで
「Cannot resolve symbol 'ActivityMainBinding'」とか言われたはことないでしょうか。
私は2回あるので、初歩的ではありますが簡単にメモっておきます。
本エントリーでは本当に最初の定義の部分の確認のため、データクラスのインスタンスとレイアウトのバインディングのことまで触れておりませんのでよろしくお願いいたします。

developer.android.com

前提事項

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;   // ← ここがエラーになる

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    }
}

確認事項

DataBinding設定

appモジュールのbuild.gradleにDataBindingが正しく指定されているか

android {
 中略
    dataBinding {
        enabled = true
    }
 中略
}

レイアウトxml

レイアウトxmlのルートタグがlayoutになっているか

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

バインディングクラスのクラス名は正しく定義したか

バインディングクラスは、レイアウトxmlの名前のパスカルケース+"Binding"というルールで自動生成されます。
R.layout.activity_main からは ActivityMainBinding というクラスが生成され、
R.layout.fragment_settings からは FragmentSettingsBinding というクラスが生成されます。
自動生成された後ならAndroid Studioで補完が効くのであまり間違わないと思います。

上記を確認した後に「Cannot resolve symbol 'ActivityMainBinding'などとエラーになった場合

Android Studioを起動し直す

これで上手く行ったりするんですよねぇ(;´∀`)

2016/11/18 追記
コメント欄にて id:kimukou_26さんからこのように教えていただきました。
きむこうさんありがとうございます( ´∀`)

再起動しなくてもgradle sync をすれば認識できますよ。
xmlのパースエラーはsys.errに表示されるみたいなので
terminal から
gradlew assembleDebugを行えば
xmlのパースエラーが出た場合には最終行あたりにエラー行が表示されます

きむこうさんがDataBindingの記事を書いていらしてるのでこちらもどうぞ( ´∀`)

exception-think.hatenablog.com




7ヶ月ぶりに公益財団法人どうぶつ基金に5000円寄付しました&近況

Save Animal その他

11月に入ってしまいましたけど、10月分のつもりです。
公益財団法人どうぶつ基金さまに5千円寄付しました。

どうぶつ基金とは – どうぶつ基金

f:id:sakura_bird1:20161103214102p:plain


どうぶつ基金さまに寄付をするのは4月以来です。前回の記事はこれですね。
sakura-bird1.hatenablog.com

当時はこの記事を書くのには抵抗感があったんだけど、蓋を開けてみれば
寄付の記事にスターを付けてくださるかたもいらして、応援していただいてると勝手に解釈しています!
こういうのってホントに励みになりますね。寄付も、記事を書くこともやっていてよかったです。

街で見かける野良猫さんで耳先カットしている猫さんがいるのを見たことがありますか?
この赤丸の部分のような↓
f:id:sakura_bird1:20161103214214p:plain

これがさくら猫というやつで、ボランティアさんが保護→不妊・去勢手術後リリースされた猫さんです。
先日猫がたくさんいるスポットでたくさん見かけました。
この活動が殺処分を減らしたり、地域の野良猫生活被害を減らしていくことにつながります(=^・^=)


話変わって最近の近況をズボラ報告です。

ポケモンGoは現在トレーナーレベル29です。
図鑑は141種類であとラッキーを捕まえれば国内のポケモンをコンプリートです。
といってもリリースから4ヶ月以上経って、ここしばらくはさすがに活動が鈍化してきました。
夜のウォーキングのお供程度です。これはこれでモチベーション維持に役に立ってます。

あとは開発ですが、年を取っても、どんな形でも続けていきたいなーって思っています。
北島マヤみたいな恐ろしい子ではありませんが(笑)

f:id:sakura_bird1:20161103221751p:plain

一般社団法人ちよだニャンとなる会さんに5千円寄付しました。Pokemon Go まだやってます。レベル28です。

Save Animal 雑感

こんにちは!
あっという間に一ヶ月経ってしまいました。
ここ2ヶ月ぐらいポケモンの世界にいたせいか、他のことが疎かになってきてるような気がしてヤバイと思っています。
開発をもっとやりたいのです。
密かに不安に感じているのですが最近物忘れもひどいので、いつまで自分は開発をやってられるのだろうか、
特にプログラミングでお金をいただくということは果たしていつまで出来るんだろう。
今が最後かもしれないのにこんなこと(ゲーム、ぐうたら、漫画、ネットを徘徊などなど)していていいのだろうか。
うわーん反省。ごめんなさい。
ちゃんとした人間になります!
って感じで一瞬大反省するもすぐ忘れてしまうということを繰り返しております(´-﹏-`;)

一応勉強はちびちびやってはいるのですがね。
↓ ここで記録しています。見えるかな?
studyplus.jp

何年か勉強の記録を付けてみて思ったのは
人間(特に自分)は弱い。だから習慣の力に頼るのだ。
ということです。
自分を責めるよりはまず15分だけ手を動かしたりすることですね。

とかなんとか言ってもとにかく理想通りにいかないですね。
私には何らかの強制力が働く方がいいのかもしれません。

ところで、今月は一般社団法人ちよだニャンとなる会さんに5千円寄付しました。
www.chiyoda-nyan.org

東京・千代田区で行政と連携・協力して「飼い主のいない猫」の問題に取り組んでいらっしゃいます。
行政もこの問題に取り組んでくれるってたまに見かけますが、嬉しくてうるっときます。
こうした取り組みが全国に全宇宙に広がっていってほしいですね。

さっき振込予約しました。
f:id:sakura_bird1:20160930214801p:plain


そして、Pokemon Goですが、トレーナーレベル28になりました。
いい運動になってますよ。

f:id:sakura_bird1:20160930215646p:plain

f:id:sakura_bird1:20160930215651p:plain


図鑑も142種類のうち138種類まで埋まりました。あと4種類。
f:id:sakura_bird1:20160930215653p:plain


最近せっかく10キロ歩いて卵を孵化してカイロスさんばかり生まれるのです。
f:id:sakura_bird1:20160930215657p:plain