Android地図アプリにログ機能を搭載する

Androidのアプリで外部ストレージにファイルを書き込む方法について調べて、簡単なアプリを作ってテストをしました。
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
今回は、いよいよ自作の地図アプリにログを取らせてファイルに保存する機能を搭載します。
方法としては、大きく2通りあり、一つは、配列にログデータをためておいて、活動を終了するときに一気にファイルに書き出す方法(前々回にやった方法)。もう一つは、ログをGPSの位置が更新されるたびにファイルに書き出す方法(前回にやった方法)です。
一長一短があるのでどちらにするか悩ましいところです。前者の方法は、ストレージにアクセスする回数が最後の1回だけなので、SDカードへの負担が減り、SDカードがエラーで死んでしまうというリスクは減らせます。ただし、内部メモリーを沢山使うことになるので、長時間の活動を記録するときにメモリーが足りるかどうかが心配です。後者の方は、メモリーオーバーのリスクはないですが、頻繁にSDカードをアクセスすることになるのでSDカードの寿命が心配です。Androidデベロッパーサイトにも、頻繁なアクセスは推奨されていないようなのでまずは、前者の方法を試すことにしました。
作った地図アプリは、いろいろなファイルの集合体になっていて複雑怪奇な構造になっていて、自分でも触るのが嫌になるくらいなのですが、地図の表示部分は全部、OvarlayMapViewer.javaというActivityが持っているのでその中身だけをいじくることにします。まずは、ログを記録する部分です。これは、地図上に現在地の丸印を表示する部分にログを記録する処理を追加するだけです。地図上に現在地を表示するルーチンは、drawUserPositionMarker()というところにあります。まずは、経度と緯度だけを取得していたのを、高度と時刻も取得するようにします。高度の取得は、location.getAltitude()で行えます。時刻の取得も同様にGPSのデータを用いてlocation.getTime()で行うことができるのですが、これだと1970の1月1日を基準とした相対時刻(しかもUTC)が取得されるので、あとで計算をして日本の標準時に直す必要があります。まあ、どうせGPXファイルに直すのに何らかの方法で変換が必要になるので後処理は必要なんですが、ファイルネームも時刻を基準に設定しておこうと思っているので、前回までにやった方法と同じく、アンドロイドの端末の内部時刻を取得するようにしています。
また、ログを記録する部分ですが、配列(ArrayList)としてLogdataというのを作っておいて、そこに、緯度、経度、高度、現在時刻を順番に追記していきます。
この配列は倍精度浮動小数点になっています。理由は単純で緯度、経度、高度などは倍精度浮動小数点で値が返ってくるからです。また、この方が文字列に直すよりもメモリの消費量が少ないのでこうしています。ただし、現在時刻だけは、現在時刻を取得するルーチンが文字列を返すために、文字列を倍精度浮動小数点の値に変換しています。現在時刻を取得するルーチンは前回と同様のルーチンで、getNowTime()で現在時刻を文字列で返します。

    @RequiresApi(api = Build.VERSION_CODES.N)
    private void drawUserPositionMarker(Location location){
        if (globals.setGPS==true) {
            Latitude = location.getLatitude();
            Longitude = location.getLongitude();
     追加した部分~
            Height = location.getAltitude();
            nowTime = Double.parseDouble(getNowDate());
    ~追加した部分(ここまで)
            latLong15 = new LatLong(Latitude,Longitude);
         追加した部分~  
            Logdata.add(Latitude);
            Logdata.add(Longitude);
            Logdata.add(Height);
            Logdata.add(nowTime);
    ~追加した部分(ここまで)

次に、配列にため込んだログデータをファイルに書き出す部分です。試しに作ったアプリではSAFを使ってファイルを書き出したのですが、これのやり方はIntentを発行して、ファイル選択画面を出して、ファイル名を確定してから書き込むという方法をとっています。この時、onActivityResultを呼び出すのですが、このアプリの場合、onActivityResultがMainActivityに書かれていて、そこに飛んでいくようになっています。そうすると、OvarlayMapViewer内で使っていた変数が読めなくなります。解決方法の一つは、必要な変数をグローバル変数にしてしまうという方法です。また、Intentを介してデータを受け渡しするという方法もあるみたいです、ただし、どちらも結構記述が面倒なので(特に配列は)、今回はOvarlayMapViewerの中で解決できるように、一旦アプリ固有の外部ストレージにファイルを保存し、それを最終的に外から読める領域にコピーするということをしています。なので、前回2回でやったことのハイブリッドのような処理になってしまいました。onDestroy()の記述は下記のようになります。

    @RequiresApi(api = Build.VERSION_CODES.N)
    public void onDestroy() {


        try {
            if (locationUpdateReceiver != null) {
                unregisterReceiver(locationUpdateReceiver);
            }


        } catch (IllegalArgumentException ex) {
            ex.printStackTrace();
        }
        Intent intent = new Intent(getApplication(), LocationService.class);
        this.getApplication().unbindService(serviceConnection);
        stopService(intent);

        NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.cancelAll();
        追加した部分~
        Ntime=getNowDate();
        String fileName = Ntime + ".log";
        Context context = getApplicationContext();
        file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName);
        if(isExternalStorageWritable()){
           
            {
                
                FileOutputStream output = new FileOutputStream(file,true);
                //OutputStreamWriter writer = new OutputStreamWriter(output);
                for (int i=0;i<Logdata.size();i++) {
                    String str = Logdata.get(i).toString();
                    output.write(str.getBytes(StandardCharsets.UTF_8));
                    output.write(",".getBytes(StandardCharsets.UTF_8));
                }

                output.flush();
                output.close();
                Logdata.clear();
               
            } catch (IOException e) {
                //e.printStackTrace();
                //textView.setText(R.string.error);
            }
            //String nfile = "storage/emulated/0/documents/"+fileName;
            String nfile = Path+"/"+fileName;
            java.nio.file.Path p1 = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                p1 = Paths.get(String.valueOf(file));
            }
            Path p2 = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                p2 = Paths.get(nfile);
            }

            try{
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    Files.copy(p1, p2);
                }
                file.delete();
            }catch(IOException e){
                System.out.println(e);
            }

        }
  ~追加した部分(ここまで)
        super.onDestroy();

    }

最初の部分は現在時刻を取得してファイル名を決めることをしています。
次のif(isExternalStorageWritable()){ 以下がファイルを書き出している部分で、ArrayListから順番にデータを読みだしてそれをアプリ固有の外部ストレージのDOCUMENTホルダーに現在時刻をファイル名にして書き込んでいくだけです。処理の方法は、前回テストした方法と同じです。違うのは最後に一括してファイルに書き出しているという点だけです。
ファイルを一旦アプリ固有の外部ストレージに書き込んだら、そのファイルを別の場所にコピーしています。処理は前回と基本的に同じですが、コピー先のフォルダ指定を前回同様のstorage/emulated/0/documents/としたところ、実機(Xiami RedMi Note11)では、内部ストレージの方に書き込まれてしまいました。ですので、マップファイルやGPXファイルを置いてある場所がPathという変数に入っているのでここに書き込むようにしています。
各処理にいちいちif(Build_VERSION_SDK_INT>=....)と書かれているのはAndroid Studioがエラーを吐いてサゼスチョン通りにするとこうなってしまっただけです。非常に見栄えが悪いのですが面倒なのでそのままにしてあります。
最後に、現在時刻を取得するルーチンと、アプリ固有の外部ストレージが書き込み可能か判定するルーチンを追記します。これららは前回と全く同じ処理です。

    @RequiresApi(api = Build.VERSION_CODES.N)
    public static String getNowDate() {
        final DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
        Date date = new Date();
        return df.format(date);
    }
    public boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        return (Environment.MEDIA_MOUNTED.equals(state));
    }

一応これでログが取れるようになりました。しばらくこれでテストしてみることにします。
また、このログファイルはただのデータの羅列なので、後から地図に表示させようとするとGPXなどの形式に変換する必要があります。その辺はPCを使ってPythonでやろうと思っています。