Android 地図アプリの修正 その2

先日、AndroidのバージョンをAndroid11にしたら、地図アプリの軌跡が表示されないという不具合を発見した。
alasixosaka.hatenablog.com
エミュレータの問題も一応解決したので、不具合を解消した。エミュレータの方はまだなんとなく不安定ですが、とりあえず動いているのでまあ良しとすることに。
alasixosaka.hatenablog.com

さて、今回の不具合は2カ所。
・Android11では軌跡が表示されない。
地磁気センサーの取得間隔がおかしくなって、地図の回転が速すぎて見ずらい。
例によって、色々試行錯誤したので、何が正解なのかわからないところもありますが、とりえず備忘録として書き留めておくことにします。

Android11で軌跡を表示する

まずは1点目から。Android11ではバックグラウンドでのGPSの使用に厳しい制限がかかっているらしいということは前回書きました。

Android9以前は、フォアグラウンドでGPSの位置情報取得に許可を与えると、自動的にバックグラウンドでも許可が与えられていました。
Android11では明確にその部分が区分けされたようです。
blog.ch3cooh.jp
上記のサイトを見ると、そもそもバックグラウンドでの位置情報の更新頻度はとても低く、地図アプリで通過してきたところを軌跡で表示するようには向いていないようです。
結論から言うと、それでも、Android11では明確にバックグラウンドでの許可がいるようでした。
始めは、バックグラウンドは関係ないと思い、マニュフェストに以下の文を追記してみました。

<service
    android:name="XXXXXXX"
    android:foregroundServiceType="location" ... >
    <!-- Any inner elements would go here. -->
</service>

XXXXXはアプリの名前です。その下に、android:foregroundServiceType="location" を追記するだけです。AndroidデベロッパーサイトにはAndroid10からこの記述が必要となっていましたが、手持ちのAQUOS Sense Plus(Android10)では、この一文が無くてもちゃんと動いていました。Android11のRedMe Note 10Proでも軌跡が表示されないこと以外はまともに動いていたので謎ですが。
developer.android.com
とりあえず、小さなアプリで動くかどうか試してみました。
akira-watson.com
上記のサイトのアプリをそのままエミュレータで動かしてみました。
すると、foregroundServiceType="location"の一文を入れなかったときは、バックグラウンドでの位置が更新されなかったのが、更新されるようになって改善されているようです。

ところで、この修正は曲者で、Android10以降で有効になるようなのです。つまり、それ以前のOSがターゲットだとエラーになるらしく、ググって、下記のスタックオーバーフローの書き込みを見つけました。
stackoverflow.com
解決策その2の、tools:node="replace"というのは何のことやらさっぱりわからんので、ターゲットをAndroid10以上に設定し直してコンパイルするとOKになりました。それ以外にも、小さなエラーはいくつかあったのですが、エラーメッセージをググって解決しました。どうも、Android Studioのバージョンを上げたおかげで構文のチェックが厳密になったようで、以前なら問題なかった記述がいくつかエラーで引っかかってしまいました。
さて、これで問題なかろうと、地図アプリの方もマニュフェストを修正して、実機で試してみました。
ところが、どうもこの方法ではまずいらしく、画面をオフにしたり、バックグラウンドに持っていくと軌跡が表示されませんでした。

やっぱりバックグラウンドは必要か?

ちなみに、バックグラウンドでGPSを使う場合には、同じサイトに記載されているように、マニュフェストにuses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"を追加して許可を取る必要があります。
そうしておいて、MainActibityのパーミッションのところに、下記のように if(Build.VERSION.SDK_INT>=30){... を追記して、Android11以上で、バックグラウンドパーミッションを取りに行くようにしました。

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)  {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION,},
                    1001);
            //return;
        }
        if (Build.VERSION.SDK_INT>=30) {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION,}, PERMISSION_BACKGROUND);
            }
        }

        final int permission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
        if (permission != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
        } else {
            clickWait();
        }

すると、位置情報の許可の画面に、位置情報設定に誘導するような表示が追加されて、そこをクリックして、位置情報設定に進んで、常に許可というのを選択すると良いようです。
スクリーンショットを取り忘れたので、ネットから適当に拾ってきた画像を貼ります。ほぼ同じ画面が表示されました。

f:id:alasixOsaka:20210725152830j:plain
位置情報取得の許可を設定する画面。

左の画面で、設定をタップすると、右の画面が表示される。ここで、常に許可を選択する。そうするとバックグラウンドでの許可が与えられるようです。
それで試してみると、家の中をウロウロした限りでは軌跡が表示されるようになりました。ところが、外に持ち出して、しばらく歩いてみると、途中で軌跡が途切れて、画面をオンにしたところに飛んでしまいました。どうも途中で省電力が働いたようです。
がっかりしたのですが、また、いろいろ調べると、結局これはアプリの問題でなく、端末の省電力設定の問題だとわかりました。

端末側で設定する。

Huaweiの端末では、以前からバックグラウンドで動いているアプリが停止するのが問題になっていたようです。
2ndart.hatenablog.com
このサイトを見て、もしやと思い、自分の端末でも設定をチェックしてみました。
すると、案の定バッテリーセーバーが機能していました。これを解除することで軌跡が正しく表示されるようになりました。

f:id:alasixOsaka:20210725160024j:plain
端末の設定を開いて、アプリを選択する
f:id:alasixOsaka:20210725160148j:plain
アプリの管理を選択
f:id:alasixOsaka:20210725160232j:plain
地図アプリを選択
f:id:alasixOsaka:20210725160315j:plain
バッテリーセーバーを選択
f:id:alasixOsaka:20210725160358j:plain
当初は、バッテリーセーバー(推奨)が選択されていた。制限なしに変更する

アプリを動かしていると、アプリがバックグラウンドで位置情報にアクセスしていますといったメッセージが表示されるようになった。
これでとりあえず、問題は解決。

センサーの取得間隔について

結局こっちのほうが謎だらけで、結局力業で解決することになりました。
センサーの取得間隔については、一部のセンサーを除いて、間隔を指定することができるのですが、どんなセンサーが間隔指定できるのかは下記のサイトに詳しいです。
akihito104.hatenablog.com
スマホの向きを知るために、加速度センサーと地磁気センサーを使っているのですが、地磁気センサーの方は間隔指定が有効のはずですが、上記のサイトを参考に簡単なアプリでテストしたところ、とっても妙なことがわかりました。

AQUOS Sense Plusでは間隔は約0.5秒が最長。

まず、AQUOSでテスト。取得間隔は、センサーマネージャーの引数にマイクロ秒単位で指定できる。1秒に設定する場合は下記のようになる。
manager.registerListener(sensorEventListener, sensor, (int) 1e6, handler);
ところが、AQUOSではこの数値で取得間隔チェックしたところ何故か0.5秒程度で値が更新されてしまう。しかも値はあまり安定していない。いくら数値を大きくしても同じだった。

RedMe Note 10Proはもっと謎な挙動

今度はRedMe Noteでやってみた。こちらは、取得間隔はほぼ正確に1秒単位であった。ちゃんと設定が効いているじゃないかと思って地図アプリの方を確認すると、当たり前のように画面がふらふらと動いて、どう見ても1秒間隔ではない。おかしいと思って、色々いじくった結果、どうもGPSがOnになっていると設定が無視されてしまうようで、メチャメチャ短い間隔で更新されることが分かった、正確に覚えていないがだいたい200mSくらい。これは、数値指定でなく、コマンド指定でDELAY_NORMALを選んだ時と同じ。どうも数値指定の設定が無視されてしまうみたいだ。そりゃ地図がふらふら動くはずだ。
でも、プログラムを見てもGPSの処理と、加速度センサー、地磁気センサーの処理は別々にやっていて互いに影響するようには到底思えず、結局原因がわからないまま、タイムスタンプを確認して、1秒以上たっていたらセンサーの値を更新するようにして解決した。具体的にはonSenserChangedのところで、地磁気センサーの値が更新された場合に、まず long now = event.timestamp;でタイムスタンプを取得し、元々、if(fAccell!=null) { と、加速度センサーの値も更新されていた場合にセンサー値を書き換えていたものを、if ( (fAccell!=null)& ( (now-post)>1000000000)) { というように、1秒以上たった場合の条件を追加し、センサー値の更新かつ1秒以上たった場合に値を更新するように書き換えた。この修正は、RotateMapViewer.javaとRotateMapViewer2.javaの2カ所に必要です。前者は高低図を表示しない時のルーティンで、後者は高低図を表示したときのルーティンです。

 public void onSensorChanged(SensorEvent event) {
                switch (event.sensor.getType()) {
                    case Sensor.TYPE_ACCELEROMETER:
                        fAccell = event.values.clone();
                        LowPassFilter(fAccell);
                        break;
                    case Sensor.TYPE_MAGNETIC_FIELD:
                        fMagnetic = event.values.clone();
                        LowPassFilter2(fMagnetic);
                        long now = event.timestamp; // nano sec.
                        Log.d("1", "sensor: " + event.sensor.getType() +
                                ", interval: " + (now - post) / 1000 /* micro sec. */);
                        if((fAccell!=null)&((now-post)>1000000000)) {
                            post = now;
                            float[] inR = new float[9];
                            SensorManager.getRotationMatrix(inR, null, fAccell, fMagnetic);
                            float[] outR = new float[9];
                            SensorManager.remapCoordinateSystem(inR, SensorManager.AXIS_X, SensorManager.AXIS_Y, outR);
                            float[] fAttitude = new float[3];
                            SensorManager.getOrientation(outR, fAttitude);

かなり強引な力業だがこれしか解決法が思いつかないので仕方ない。とりあえず、これでしばらく運用することにする。