android端末の移動状態を管理するシステムの、ベースとなるプログラムを作ってみました。セキュリティなど問題は多いと思うんだけれど、今の僕に作れる(初心者が作る)簡単シンプルなプログラムを目標に作ってみました。車両管理や営業管理など、事業場外活動の状況を確認する目的で作成してます。個人利用なのでこれをカスタマイズして必要な機能を盛り込めば十分かな?
なんか問題ありそうならアドバイス頂けますと幸いです!
ここまで来るのにめっちゃ時間がかかった!
Over View
このシステムは、androidアプリとwebアプリで構築してます。androidは常駐アプリにし、1分毎に位置情報を取得し、おおよそ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つのルートを作成します ・ サーバーから位置情報を取得して、位置情報を確認するMAP表示のルート ・ androidからの位置情報を受信してサーバーに記録するルート |
Controller.php | 下記2つの関数を作成します ・ サーバー情報を取得してViewを表示させる関数 ・ androidから情報を取得してサーバーに記録する関数 |
top.blade.php | Map表示します |
android
MainActivity.java
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
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
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
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);
}
}
connection.setDoInput() と connection.getResponceCode()を同時に使うと、InputStreamReader()でAlready connected というエラーがでる。同じようなことは、getResponseMessage()、getInputStream()、connect()でも起こりえる。InputStreamReader()を使うときには注意かな。参考サイト様
Laravel
Route
Route::get ('/','Laravel01Controller@getIndex');
Route::get ('/getData','Laravel01Controller@getData');
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
<!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出力
最低この程度の機能があれば事業場外勤務の管理に使えるようになるかな?
後記
やっぱり楽しい。僕は独学なので、書籍とネットで学んでいます。ネットで情報を提供してくださる皆様は僕の先生。ありがとうございます。亀の一歩を続けていて、本当に進むのが遅いのですけれど、楽しいからやめられない。
自分で作ったコードって、至らないところも多いのでしょうけれど、愛おしい。作ったコードが愛おしくって何度も見てしまう。こんな愛おしく感じるのだから、プログラミングって楽しいんだね!