51歳からのプログラミング　備忘　初心者が作る移動管理アプリ　ベースプログラム

android端末の移動状態を管理するシステムの、ベースとなるプログラムを作ってみました。セキュリティなど問題は多いと思うんだけれど、今の僕に作れる（初心者が作る）簡単シンプルなプログラムを目標に作ってみました。車両管理や営業管理など、事業場外活動の状況を確認する目的で作成してます。個人利用なのでこれをカスタマイズして必要な機能を盛り込めば十分かな？

なんか問題ありそうならアドバイス頂けますと幸いです！
ここまで来るのにめっちゃ時間がかかった！

#Over View
このシステムは、androidアプリとwebアプリで構築してます。androidは常駐アプリにし、１分毎に位置情報を取得し、おおよそ10分毎に発信し、webアプリでデータベース登録し、移動状況を表示するという感じです。このシステムはベースプログラムなので、複数端末には対応してませんし、位置情報の利用もMap表示だけです。なお送信はGET送信としてます。

androidアプリはapi28以上を想定してます。
webアプリはLaravelです。

#ファイル構成


**android**

|ファイル名|説明|
|---|---|
|MainActivity.java|FusedLocationProviderClientを使うためにPermissionチェックをします
|MyService.java|アプリを常駐させるためにForegroundServiceでNotificationを発行し、FusedLocationProviderClientを呼びます|
|MyLocation.java|FusedLocationProviderClientで定期的に位置情報を取得します|
|MySendDataAsyncTask.java|位置情報をWEBサーバーにGET送信します|

**Laravel**

|ファイル名|説明|
|----|---|
|Route.php|下記2つのルートを作成します<br>・　サーバーから位置情報を取得して、位置情報を確認するMAP表示のルート<br>・　androidからの位置情報を受信してサーバーに記録するルート|
|Controller.php|下記2つの関数を作成します<br>・　サーバー情報を取得してViewを表示させる関数<br>・　androidから情報を取得してサーバーに記録する関数|
|top.blade.php|Map表示します|


#android
##MainActivity.java

```java:MainActivity

public class MainActivity extends AppCompatActivity {
    // MainActivityを起動すると、自動的に位置情報を送信し始めます
    // STOPボタンでアプリが停止した後、再開させる時のためにSTARTボタンを作成
    // 位置情報を取得することは、ユーザーデンジャラスな機能なので許可確認が必要となります
    // MainActivityで,許可確認して、ForegroundService->位置情報取得->非同期通信で送信とします
    // このシステムで使用する機能は、すべて過去の記事で詳しい説明をしてます

    static TextView     textView;
    static TextView     textScroll;
    static MainActivity activity;
           Intent       intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView   = findViewById(R.id.textView);
        textSroll  = findViewById(R.id.textScroll);
        activity   = MainActivity.this;
        Button btnStart = findViewById(R.id.btnStart);
        Button btnStop  = findViewById(R.id.btnStop);

        intent = new Intent(this,MyService.class);
        myCheckPermission();

        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            if(MyService.state == false && Build.VERSION.SDK_INT >= 26){
                myCheckPermission();
            }
            }
        });

        btnStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                stopService(intent);
            }
        });
    }

    // FusedLocationProviderClientはユーザーデンジャラスなので許可の確認が必要
    // このアプリでは、MainActivityで許可確認してからアプリを開始します
    // 詳細は、過去の記事「FusedLocationProviderClientをForegorundServiceで常駐させる」で説明してます
    private void myCheckPermission(){
        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.ACCESS_FINE_LOCATION},100);
        }else{
            checkProviderEnable();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(requestCode == 100){
            if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
                checkProviderEnable();
            }else{
                textView.setText("no permission");
            }
        }
    }

    private void checkProviderEnable(){
        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        if(locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            if (Build.VERSION.SDK_INT >= 26) {
                startForegroundService(intent);
            } else {
                startService(intent);
            }
        }else{
            textView.setText("Please LocationInfomation ON!");
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.ACCESS_FINE_LOCATION},100);
        }
    }
}
```

##MyService.java

```java:MyService

public class MyService extends Service {
    // アプリを常駐させるためにNotificationを使ってForegroundServiceにします
    // Notificationは、過去の記事「Notification IntentService Service　コード備忘まとめ」で説明してます
    // 通知が完了したらMyLocation.classを呼びます

    static boolean state = false;
    Notification notification;
    FusedLocationProviderClient client;

    @Override
    public void onCreate() {
        super.onCreate();
        state = true;
        client = LocationServices.getFusedLocationProviderClient(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Intent pIntent = new Intent(this,MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,pIntent,0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this,"id");
        notification = builder.setSmallIcon(R.drawable.notification_icon)
                              .setContentIntent(pendingIntent).build();
        if(Build.VERSION.SDK_INT >= 26){
            int importance = NotificationManager.IMPORTANCE_LOW;
            NotificationChannel channel = new NotificationChannel("id","name",importance);
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
            startForeground(1,notification);
        }else{
            notification = builder.setPriority(NotificationCompat.PRIORITY_LOW).build();
            NotificationManagerCompat managerCompat = NotificationManagerCompat.from(this);
            managerCompat.notify(1,notification);
        }

        new MyLocation().MyLocation();
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        state = false;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
```
##MyLocation.java

```java:MyLocation

public class MyLocation {
    // 位置情報を取得するクラス
    // MyService.class から呼ばれます
    // 位置情報取得は、FusedLocationProviderClientを使います
    // 位置情報を取得したら、AsyncTaskで送信処理します
    // 詳細は、過去の記事「FusedLocationProviderClientをForegorundServiceで常駐させる」で説明してます

    // 変数やオブジェクトを宣言
    MainActivity activity;
    TextView textView;
    FusedLocationProviderClient client;
    LocationRequest request;
    MyLocationCallback callback;

    int    counter  = 0;
    String sendData = null;
    String myDate;
    String lati;
    String longi;
    String myLocation;

    public void MyLocation(){
        activity = MainActivity.activity;
        textView = MainActivity.textView;
        callback = new MyLocationCallback();

        client   = LocationServices.getFusedLocationProviderClient(activity);
        request  = LocationRequest.create();
                   request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
                   // 1分ごとに計測
                   request.setInterval(1000*60*1);

        client.requestLocationUpdates(request,callback,null);
    }

    private class MyLocationCallback extends LocationCallback{
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);

            // 1分ごとに計測した位置情報を10個毎にまとめます。
            long myDate;
            if(counter == 10){
                new MySendDataAsyncTask().execute(sendData);
                counter  = 0;
                sendData = "";
            }

            myDate      = Calendar.getInstance().getTimeInMillis();
            lati        = String.valueOf(locationResult.getLastLocation().getLatitude());
            longi       = String.valueOf(locationResult.getLastLocation().getLongitude());
            myLocation  = "D" + myDate + "_" + lati + "_" + longi;
            if(sendData == null){
                sendData = myLocation;
            }else{
                sendData = sendData + myLocation;
            }
            counter++;
        }

        @Override
        public void onLocationAvailability(LocationAvailability locationAvailability) {
            super.onLocationAvailability(locationAvailability);
        }
    }
}
```

##MySendDateAsyncTask.java

```java:MySendDataAsyncTask

public class MySendDataAsyncTask extends AsyncTask<String,Void,String>{
    // MyLocation.classから受信した位置情報をwebアプリに送信します。
    // 詳しい説明は、過去の記事「AndroidからLaravelにHttpURLConnection接続」でどうぞ

    TextView   textScroll;

    public MySendDataAsyncTask() {
        super();
        textScroll = MainActivity.textScroll;
    }

    @Override
    protected String doInBackground(String... sendData) {
        StringBuffer buffer = new StringBuffer();
        String senddata = sendData[0];
        try {
            HttpURLConnection connection = (HttpURLConnection) new URL("http://192.1xx.xxx.xxx:8000/getData").openConnection();
            connection.addRequestProperty("senddata",senddata);
            connection.addRequestProperty("id","123");
            connection.setRequestMethod("GET");
            // connection.setDoInput(true);

            // 確認のためのコード
            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            while(reader.readLine() != null){
                buffer.append(reader.readLine());
            }
            reader.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(buffer);
    }

    @Override
    protected void onPostExecute(String s) {
        super.onPostExecute(s);
        textScroll.setText(s);
    }
}
```
<font color="#f55">connection.setDoInput() と connection.getResponceCode()を同時に使うと、InputStreamReader()でAlready connected というエラーがでる。同じようなことは、getResponseMessage（）、getInputStream（）、connect（）でも起こりえる。InputStreamReader()を使うときには注意かな。</font>[参考サイト様](https://codeday.me/jp/qa/20190325/476302.html)


#Laravel
##Route

```php:Route
Route::get  ('/','Laravel01Controller@getIndex');
Route::get  ('/getData','Laravel01Controller@getData');
```
##Controller

```php:Controller

class Laravel01Controller extends Controller
{
    public function getIndex(Request $request){
      // 必要なデータをJsonにして送信
      $sample = Sample::get(['datetime','latitude','longitude'])->toJson();
      return view('top',['sample'=>$sample]);
    }

    // androidから送信された情報をデータベースに登録する
    public function getData(Request $request){

      // $receiver     androidから送られてくる情報
      // $id           android端末識別番号
      // $sampleArray  androidから送られてくる情報を、日時,緯度,経度を一要素とした配列で格納
      // $splitArray   $sampleArrayを、データベース登録用に,日時,緯度,経度を分けて配列にして格納

      // androidから情報を取得
      $receiver = $request->server->get('HTTP_SENDDATA');
      if(isset($receiver)){
        $id       = $request->server->get('HTTP_ID');
        
        // 日時,緯度,経度を一要素として配列に格納 ['D15469674_35.125456_139.123654','...'...]
        $sampleArray = explode("D",$receiver);
        $sampleArray = array_filter($sampleArray,"strlen");
        $sampleArray = array_values($sampleArray);
        
        // $sampleArrayの要素を、日付、緯度、経度に分けて配列にする ['15469674','35.125456','139.123654'],...
        foreach($sampleArray as $value){
          $splitArray = explode("_",$value);
          $splitArray = array_filter($splitArray,"strlen");
          $splitArray = array_values($splitArray);
          
          $splitArray[1] = (double)  $splitArray[1];
          $splitArray[2] = (double)  $splitArray[2];
          
          $saveData   = [
            'name'       =>(int) $id,
            'datetime' =>$splitArray[0],
            'latitude' =>$splitArray[1],
            'longitude'=>$splitArray[2],
          ];
          
          $sample = new Sample;
          $sample->fill($saveData)->save();
        }
      }
    }
}
```
##top.blade.php

```php:top.blade.php
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="initial-scale=0.5">
    <meta charset="utf-8">

    <style>
      #map      {height:80%;width:80%;margin:10px}
      html,body {height:100%;margin:0;padding:0;}
    </style>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

<script>
      function initMap(){

        // 変数宣言します
        var uluru;
        var uluruLast;
        var color='#4169e1';
        // Controllerから受信した連想配列を変数に格納
        var sampleArray = JSON.parse('<?php echo $sample; ?>');
        uluru ={lat:35,lng:139};

        // Mapインスタンスを作ります
        var map = new google.maps.Map(document.getElementById('map'),{zoom:13,center:uluru});
        $.each(sampleArray,function(index,value){
          uluru = {lat:value['latitude'],lng:value['longitude']};
          
          // マップに位置情報を表示するための円を作成
          for(i=0;i<=30;i++){
            var circle = new google.maps.Circle({
                                    strokeColor:'',
                                    strokeOpacity:0,
                                    strokeWeight:0,
                                    fillColor:color,
                                    fillOpacity:0.05,
                                    map:map,
                                    center:uluru,
                                    radius:i,
            });
            
            // 移動経路を線で表示します
            if(uluruLast){
              var drivePath = new google.maps.Polyline({
                path:[uluruLast,uluru],geodesic:true,
                                       strokeColor:color,
                                       strokeOpacity:0.2,
                                       strokeWeight:0.1,
              });
              drivePath.setMap(map);
            }
          }
          // 移動経路を線で表示するため、古い位置情報を別の変数に代入します
          uluruLast = uluru;
        });
      }
    </script>

    <!-- 
    geolocation は javascriptではキーが必要。入手して変更してください -->
    <script src="https://maps.googleapis.com/maps/api/js?key=userKey&callback=initMap" async defer>
    </script>

</head>
<body>

DriveManager<br>
<div id="map"></div>

</body>
</html>
```

これをベースに今後はゆっくり以下を実装できたらと考えてます。
・　諸条件の設定変更（位置情報取得感覚、表示色の変更その他）
・　複数端末に対応
・　端末側のアプリが未起動の場合に起動するようメッセージ
・　端末ごとの移動効率など集計し、端末とwebで確認
・　5分以上位置が停止している場合の、場所と時間の吹き出し表示
・　データーベースの自動バックアップ
・　データーベースのCSV出力

最低この程度の機能があれば事業場外勤務の管理に使えるようになるかな？

##後記
やっぱり楽しい。僕は独学なので、書籍とネットで学んでいます。ネットで情報を提供してくださる皆様は僕の先生。ありがとうございます。亀の一歩を続けていて、本当に進むのが遅いのですけれど、楽しいからやめられない。

自分で作ったコードって、至らないところも多いのでしょうけれど、愛おしい。作ったコードが愛おしくって何度も見てしまう。こんな愛おしく感じるのだから、プログラミングって楽しいんだね！