vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Android】インテントからLocationを有効にする(和歌山トイレマップで遊んでみる 4)

はじめに

一回アドベントカレンダーのネタをはさみましたが、前回の続きです。

Google Map上には、現在位置を地図の中心に表示するボタンを表示しているのですが、GPSがONになっていない場合は特に何も表示されず、地図の移動もない、という状態でした。
これではなぜボタンが動作しないかが分からないため、GPSがONになっていない場合はインテントを表示して変更できるようにしたいと思います。

f:id:mslGt:20151229003858p:plain

ボタン押下のイベントを取得する

まずはボタンを押した時のイベントを取得して、GPSの状態をチェックする処理が実行できるようにします。

LocationAccesser.java

~省略~
public void getGoogleMap(final MainActivity mainActivity, final SupportMapFragment mapFragment){
    // GoogleMapのインスタンス取得.
    if (mMap != null) {
        return;
    }
    // マップの表示.
    mapFragment.getMapAsync(
        new OnMapReadyCallback() {
            @Override
            public void onMapReady(GoogleMap gMap) {
                mMap = gMap;
                mMap.setMyLocationEnabled(true);
                // 和歌山県庁に移動.
                mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(34.22501, 135.1678), 9));
                // Map上の現在位置を中央に表示するボタンが押された時のイベントが取得できるようにする.
                mMap.setOnMyLocationButtonClickListener(new GoogleMap.OnMyLocationButtonClickListener() {
                    @Override
                    public boolean onMyLocationButtonClick() {
                        // GPSがONになっているかを確認する.
                        sLocationAccesser.moveToMyLocation(mainActivity);
                        // falseを返すことで現在位置をMapの中央に表示するデフォルトの操作が実行される.
                        return false;
                    }
                });
~省略~

onMyLocationButtonClickをオーバーライドしたsetOnMyLocationButtonClickListenerをセットすることで、ボタンを押した時に任意の処理を実行できるようになります。

簡単な解決方法

GPSをONにするためのインテント表示は、以下が一番シンプルな方法かと思います。

Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivityForResult(intent, 2);

ただ、上記は設定画面を開くだけなので、元のアプリに戻って云々という操作が発生するという問題がありました。

実現したい内容は、Bluetoothの時のように、別のアプリや画面に遷移せず、直接GPSをONにする、つまり以下の画像のような状態にしたかったのです。

f:id:mslGt:20151229004019p:plain

LocationRequestを使う

これを解決するためには、LocationRequest、SettingsApi、GoogleApiClientを使って以下のように記述すれば良いようです。
※この内容についてはまだぼんやりとしか理解できていないため、後で追記するかもしれません。

~省略~
public void moveToMyLocation(final FragmentActivity activity){
    // LocationServicesAPIを利用するための準備.
    // LocationRequest.
    LocationRequest locationRequest = LocationRequest.create();
    locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
    locationRequest.setInterval(3000L);
    locationRequest.setFastestInterval(500L);
    // SettingsApi.
    LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
            .addLocationRequest(locationRequest);
    builder.setAlwaysShow(true);
    // GoogleApiClient.
    if(mGoogleApiClient == null) {
        mGoogleApiClient = new GoogleApiClient
                .Builder(activity.getApplicationContext())
                .enableAutoManage(activity, this)
                .addApi(LocationServices.API)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();
    }
    // 非同期でGPSをONにするIntentを表示する.
    PendingResult result =
            LocationServices.SettingsApi.checkLocationSettings(mGoogleApiClient, builder.build());
    result.setResultCallback(new ResultCallback() {
        @Override
        public void onResult(LocationSettingsResult result) {
            final Status status = result.getStatus();
            switch (status.getStatusCode()) {
                case LocationSettingsStatusCodes.SUCCESS:
                    // GPSがOnなら無視.
                    break;
                case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                    try {
                        // GPSがOffならIntent表示. onActivityResultで結果取得.
                        status.startResolutionForResult(
                                activity, R.string.request_enable_location);
                    } catch (IntentSender.SendIntentException e) {
                        // TODO; 例外処理.
                    }
                    break;
                case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                    // Locationが無効なら無視.
                    break;
            }
        }
    });
}
~省略~

これでGPSがONにできる状態で、かつOFFになっている場合にIntentが表示され、ボタンが押されればonActivityResultでその結果を受取ることができるようになります。

MainActivity.java

~省略~
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode){
    case R.string.request_enable_location:
        if(resultCode == RESULT_OK){
            // GPSがONに変更された時の処理.
        }
        break;
    }
}
~省略~

現在位置に移動させる

現在位置を地図の中心に表示するボタンを押した時にGPSがONになっている場合は正しく地図(上のカメラ)が移動しますが、OFFになっている場合は上記のIntentでGPSをONにしても自動では移動しません。

そのため、後者の場合にも地図の中央に現在位置が表示されるようにします。

LocationAccesser.java

~省略~
public void moveCurrentLocation(){
    try {
        // 現在位置を中央に表示.
        Location currentLocation = mMap.getMyLocation();

        if(currentLocation == null){
            // 現在位置の取得に失敗したら位置情報が取得できるプロバイダから現在位置を取得.
            Criteria criteria = new Criteria();
            criteria.setAccuracy(Criteria.ACCURACY_COARSE);
            currentLocation = mLocationManager.getLastKnownLocation(mLocationManager.getBestProvider(criteria, true));
        }
        if(currentLocation == null) {
            // 現在位置の取得に失敗した場合はToast表示.
            MainActivity.showToastFailedGettingLocation();
        }
        else{
            // 
            mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()), 13));
        }
    }catch(SecurityException ex){
        Log.d("SWT Error", ex.getLocalizedMessage());
    }
}
~省略~

「GoogleMap.getMyLocation()」で現在位置が取得できるはずですが、失敗した場合は最適なプロバイダから現在位置を取得しています(「GoogleMap.getMyLocation()」の方は不要かもしれませんね…)。

なお、今回は地図(上のカメラ)を移動させるだけでなく拡大もしたかったため、「CameraUpdateFactory.newLatLngZoom()」を使用しています。

位置情報が取得できない

ここまでのコードで正しく動作するかな?と思いきや、アプリの起動時にGPSをOFFにしていて上記コードでONに切り替えた場合に位置情報が取得できず、 「GoogleMap.getMyLocation()」や「mLocationManager.getLastKnownLocation()」がNullで返るという問題が発生しました。

GPSをONにしたあと、位置情報が取得できるようになるまでに多少の時間が必要になるため、ONにしたらしばらく待機して、そのあとで位置情報を取得する、という処理が必要なのでした。
Theadを止めてしまうと位置情報が取得できるようになるまでの時間も延びてしまうため、Timerを使って実現することにします。

MainActivity.java

~省略~
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data){
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode){
        case R.string.request_enable_location:
            if(resultCode == RESULT_OK){
                // GPSをONにした直後は値が取れない場合があるのでTimerで1秒待つ.
                mTmrGettingLocationTimer = new Timer();
                mTmrGettingLocationTimer.schedule(mTimeController, 1000L);
            }
            break;
        }
    }
~省略~
    private Handler executeOnUiThreadHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what){
                case R.string.handler_get_csv:
                    // 読み込んだCSVデータを元にマーカーを設置.
                    mLocationAccesser.adMarker(sMainActivity.mStrToiletName, sMainActivity.mDblLatitude, sMainActivity.mDblLongitude);
                    break;
                case R.string.handler_get_location:
                    // Locationが有効なら現在位置を取得.
                    mLocationAccesser.moveCurrentLocation();
                    break;
            }

        }
    };
    public class TimerController extends TimerTask{
        @Override
        public void run() {
            // 現在位置を取得する.
            executeOnUiThreadHandler.sendEmptyMessage(R.string.handler_get_location);
        }
    }
}

以前は繰り返し処理を実行するためTimer.scheduleの引数に次の処理までの時間も渡していましたが、今回は一回だけ処理されれば良いため変更しています。

おわりに

まだ抜けはある気がしますが、とにかくこれでネットワーク、GPSがOFFの状態にも対応できるようになりました。
次はCSVから読み込んだデータをDBに保存して、検索ができるようにしたいと思います。

参考

Location

Google Map API

Timer