# BGM アプリ 仕様 ```[java] package com.example.bgmwithlight; //必要なライブラリのインストール import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import android.Manifest; import android.content.pm.PackageManager; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.media.AudioManager; import android.media.MediaPlayer; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageButton; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import java.util.HashMap; import java.util.Map; import java.util.Random; public class MainActivity extends AppCompatActivity implements SensorEventListener, LocationListener { //クラス内変数の準備 private SensorManager sensorManager; private LocationManager mLocationManager; private String mediaPlayerState = "Idle"; private String[] soundFilePool; private String[] spinnerItems = {"曲を選択してください"}; private String[] areaTexts = { "登録エリア一覧", "エリア1(雪が谷大塚駅付近-旗の台駅)", "エリア2(旗の台駅-あざみ野駅)", "エリア3(あざみ野駅-中川駅)" }; float illuminance=0; private boolean isDark = false; private String bestProvider; private double light[] = {0.0, 0.0, 0.0, 0.0, 0.0}; private MediaPlayer mediaPlayer; Map<String, String> map = new HashMap<String, String>() { { put("サンライズ", "earth_song"); put("ハロー", "hello_summer"); put("幕開け", "ikusanomakuake"); put("おもてなし", "kareinarubansan"); put("航海", "koukai"); put("女神", "serene"); put("ゴール", "into_the_future"); put("羽ばたき", "monshirochou"); put("渚", "nagisa"); } }; //アプリが起動して最初の動作 @Override protected void onCreate(Bundle savedInstanceState) { /* アプリの初期設定 */ //元からあるアプリ起動時の動作 super.onCreate(savedInstanceState); //ボタンの配置などを行うページの指定 setContentView(R.layout.activity_main); /*スピナーの初期設定*/ //スピナーに紐づける配列の設定(エリア一覧) Spinner areaspinner = findViewById(R.id.AreaSpinner); // ArrayAdapter ArrayAdapter<String> areaadapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, areaTexts); areaadapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // spinner に adapter(指定した配列のデータ) をセット areaspinner.setAdapter(areaadapter); //スピナーに紐づける配列の設定(曲名) Spinner spinner = findViewById(R.id.songname_spinner); // ArrayAdapter ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, spinnerItems); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // spinner に adapter(指定した配列のデータ) をセット spinner.setAdapter(adapter); // 曲名のみイベントリスナー(選ばれた時の動作)を登録 spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { // アイテムが選択された時 @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { Spinner spinner = (Spinner) parent; String selectedItem = (String) spinner.getSelectedItem(); if (selectedItem.equals("全曲再生する")) { int i; String[] soundFileNames = new String[spinnerItems.length - 2]; for (i = 1; i < spinnerItems.length - 1; i++) { soundFileNames[i - 1] = map.get(spinnerItems[i]); } soundFilePool = soundFileNames; } else if (!selectedItem.equals("曲を選択してください")) { soundFilePool = new String[]{ map.get(selectedItem)}; } if (mediaPlayer != null) {//前の音楽がなっている場合はこのタイミングで消す // 音楽停止 audioStop(); } } // アイテムが選択されなかった public void onNothingSelected(AdapterView<?> parent) { } }); /* GPS、センサを使うための準備*/ initLocationManager();//GPSの機能の初期化 sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);//加速度(を含むセンサ群)を使うための宣言 /*ボタン(再生ボタンなど)が押された時の動作(イベントリスナー)の設定*/ ImageButton buttonPause = findViewById(R.id.pauseBtn); buttonPause.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mediaPlayer != null) { // 音楽停止 audioPause(); } } }); // 音楽開始ボタンへアクセスし、機能を割り振る ImageButton buttonStart = findViewById(R.id.playBtn); buttonStart.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 音楽再生 audioPlay(); } }); // 音楽停止ボタンへアクセスし機能を割り振る ImageButton buttonStop = findViewById(R.id.stopBtn); buttonStop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mediaPlayer != null) { // 音楽停止 audioStop(); } } }); } /*Activityが開始した時(onCreateが終わった後すぐのイメージ)*/ @Override protected void onStart() { //元からあったonstartの動作 super.onStart(); //GPSの機能の動作開始 locationStart(); } /*画面が表示され次第*/ @Override protected void onResume() { //元からあったonstartの動作 super.onResume(); /* 照度Listenerの登録(センサの測定開始)*/ Sensor light = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); if(light != null){//照度センサがアプリから使えるかのチェック sensorManager.registerListener(this, light, SensorManager.SENSOR_DELAY_UI); //照度の情報に関してのリスナーの登録 //SensorManager.SENSOR_DELAY_UI : 60msおきに値の更新(イベント)を発生させる } else{ Toast.makeText(getApplication(), "light sensor is not support", Toast.LENGTH_SHORT).show(); } } // 解除するコードも入れる! @Override protected void onPause() {//画面がバックグラウンドに移動する直前 // 元からある一時停止の動作 super.onPause(); /*照度せんさのていし*/ sensorManager.unregisterListener(this);// Listenerを解除 } //Activityが表示もされないまま長時間使われない時(タスクが落とされる直前のイメージ) @Override protected void onStop() { // 元からあるアプリ終了時の動作 super.onStop(); /*GPSのていし*/ locationStop(); } private void initLocationManager() {//GPSの機能の初期化 // インスタンス生成(GPSの機能を取りまとめる変数の宣言) mLocationManager = (LocationManager) getSystemService(LOCATION_SERVICE); bestProvider = LocationManager.NETWORK_PROVIDER; } private void checkPermission() {//PackageManager.PERMISSION_GRANTED //許可されていることを示す定数 if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // パーミッションの許可を取得する ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, 1000); } } private void locationStart() {//GPSのデータを取得し始める checkPermission(); //更新頻度の設定(1秒に一回は更新,3m動いてたら更新) mLocationManager.requestLocationUpdates(bestProvider, 1000, 3, this); } private void locationStop() {//GPSのデータの取得をやめる mLocationManager.removeUpdates(this); } @Override public void onLocationChanged(Location location) {//GPSのデータが更新された時 //GPSで得られた緯度と経度 double latitude = location.getLatitude(); double longitude = location.getLongitude(); if (!mediaPlayerState.equals("Started")) { int nowArea = getNowArea(latitude, longitude); updateplaylist(nowArea); } else { Toast.makeText(getApplication(), "It was changed only text label.", Toast.LENGTH_SHORT).show(); } //labelの更新 TextView latitude_label = findViewById(R.id.latitude_label); TextView longitude_label = findViewById(R.id.longitude_label); latitude_label.setText(String.format("緯度:%s", location.getLatitude())); longitude_label.setText(String.format("経度:%s", location.getLongitude())); } private double calcDistance(Double lat1, Double lon1, Double lat2, Double lon2) { //緯度と経度を与えた時の距離の計算は複雑なので、今回は地球を半径 6370 kmの球体に近似した時の距離で比較する。 //参考:三浦 英俊 緯度経度を用いた3つの距離計算方法 (http://www.orsj.or.jp/archive2/or60-12/or60_12_701.pdf) //(そんなに距離もないし、緯度と経度を単純なxyグラフのように比較でも問題はないと思います。ただ、どれくらい影響があるかを計算する方がめんどくさそうなので今回は球体近似にします) return 6370 * Math.acos(Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2)); } private int getNowArea(Double lat, Double lon) { //家から学校までの通学経路の緯度と経度の情報 //自宅、雪が谷大塚駅,(石川台駅),旗の台駅,(大岡山駅),二子玉川駅,(宮崎台駅),(たまプラーザ駅),あざみ野駅,(交差点),中川駅,都市大横浜キャンパスの順 //どの区間にいるかの判定精度をあげるために、()の駅の地点とも比較 double[] points_latitude = {35.5908322, 35.5969326, 35.5969326, 35.6048821, 35.607486, 35.6115718, 35.5872174, 35.5754248, 35.5685796, 35.565656, 35.5628313, 35.5614436}; double[] points_longitude = {139.6841728, 139.6788131, 139.6830777, 139.7005319, 139.6834543, 139.6243976, 139.5890125, 139.5571483, 139.5512665, 139.563103, 139.5677896, 139.5735238}; double distance; int i; double miniDistance = 1e10;//最小値 int miniNum = 0;//最小の値を取った地点 for (i = 0; i < points_latitude.length - 1; i++) { distance = calcDistance(lat, lon, points_latitude[i], points_longitude[i]) + calcDistance(lat, lon, points_latitude[i + 1], points_longitude[i + 1]); if (miniDistance > distance) { miniNum = i; miniDistance = distance; } } return miniNum; } @Override public void onProviderDisabled(String provider) { Log.d("DEBUG", "called onProviderDisabled"); } @Override public void onProviderEnabled(String provider) { Log.d("DEBUG", "called onProviderEnabled"); } @Override public void onSensorChanged(SensorEvent event) {//加速度などのセンサーのデータが更新されたとき double meanIlluminance; if (event.sensor.getType() == Sensor.TYPE_LIGHT){ illuminance = event.values[0];//照度センサの値を取得 int i; meanIlluminance=illuminance; for (i = 0; i < 4; i++) { light[i] = light[i + 1]; meanIlluminance += light[i + 1]; } light[4] = illuminance; meanIlluminance /= 5; if (illuminance<15){ HandlerThread handlerThread = new HandlerThread("foo"); handlerThread.start(); new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { // ここに3秒後に実行したい処理 if (light[4]<15 && !isDark) { String[] songs = { "kinenju", "hatsuyuki", "white_snow","harunoaoiro" }; Random r = new Random(); soundFilePool = new String[]{songs[r.nextInt(songs.length)]}; audioStop(); isDark = true; Toast.makeText(getApplication(), "It's dark here.", Toast.LENGTH_SHORT).show(); audioPlay(); /*テストおn*/ TextView playlistTextView = findViewById(R.id.playlist_textview); playlistTextView.setText("再生中の曲 : 眠くなる曲"); } } }, 3000);//3秒後に曲チェンジ } if (meanIlluminance<15 && !isDark) { audioStop(); isDark = true; String[] songs = { "kinenju", "hatsuyuki", "white_snow","harunoaoiro" }; Random r = new Random(); soundFilePool = new String[]{songs[r.nextInt(songs.length)]}; Toast.makeText(getApplication(), "It's dark here.", Toast.LENGTH_SHORT).show(); TextView playlistTextView = findViewById(R.id.playlist_textview); playlistTextView.setText("再生中の曲 : 眠くなる曲"); audioPlay(); } String strTmp = "照度センサー :" + illuminance +"\n"; TextView textView = findViewById(R.id.text_view); textView.setText(strTmp); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } //音楽の再生に関する関数群 private boolean audioSetup() { if (mediaPlayerState.equals("pause")) { return true; } if (soundFilePool == null) { return false; } int viewId = getResources().getIdentifier(soundFilePool[0], "raw", getPackageName()); mediaPlayer = MediaPlayer.create(this, viewId); if (soundFilePool.length == 1) { soundFilePool = null; } else { String[] fileNameArr = new String[soundFilePool.length - 1]; int i; for (i = 0; i < soundFilePool.length - 1; i++) { fileNameArr[i] = soundFilePool[i + 1]; } soundFilePool = fileNameArr; } // 音量調整を端末のボタンに任せる setVolumeControlStream(AudioManager.STREAM_MUSIC); return true; } private void audioPlay() { if (mediaPlayerState.equals("Paused")) { // 再生する mediaPlayer.start(); mediaPlayerState = "Started"; } else { Log.d("DEBUG", String.format("%s", mediaPlayer)); if (mediaPlayer == null) { // audio ファイルを読出し if (audioSetup()) { Toast.makeText(getApplication(), "Read audio file", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getApplication(), "Error: read audio file", Toast.LENGTH_SHORT).show(); return; } } else { // 繰り返し再生する場合 mediaPlayer.stop(); mediaPlayer.reset(); // リソースの解放 mediaPlayer.release(); } // 再生する mediaPlayer.start(); mediaPlayerState = "Started"; // 終了を検知するリスナー mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mediaPlayerState = "PlayBackCompleted"; audioStop(); if (soundFilePool == null) { Toast.makeText(getApplication(), "finish", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getApplication(), "next", Toast.LENGTH_SHORT).show(); audioPlay(); } } }); } } private void audioStop() { if (mediaPlayerState.equals("Started") || mediaPlayerState.equals("Paused") || mediaPlayerState.equals("PlayBackCompleted")) { // 再生終了 mediaPlayer.stop(); mediaPlayerState = "Stopped"; // リセット mediaPlayer.reset(); mediaPlayerState = "Idle"; // リソースの解放 mediaPlayer.release(); mediaPlayerState = "End"; mediaPlayer = null; if (soundFilePool == null) { Spinner spinner = findViewById(R.id.songname_spinner); spinner.setSelection(0); locationStart(); } isDark = false; } } private void audioPause() { // 再生終了 if (mediaPlayerState.equals("Started")) { mediaPlayer.pause(); mediaPlayerState = "Paused"; } } void updateplaylist(int nowArea){ int[] areaNumber = { 0, 0, 1, 1, 1, 1, 1, 2, 2}; String playlistName; if (nowArea < 2) { playlistName = "プレイリスト1"; //spinnerItems = new String[]{"曲を選択してください", "branchtime","okusamahabargengaosuki","ookinaugokufurudokei", "全曲再生する"}; spinnerItems = new String[]{"曲を選択してください", "サンライズ","ハロー","幕開け", "全曲再生する"}; } else if (nowArea < 7) { playlistName = "プレイリスト2"; spinnerItems = new String[]{"曲を選択してください", "女神" ,"航海","おもてなし", "全曲再生する"}; } else { playlistName = "プレイリスト3"; spinnerItems = new String[]{"曲を選択してください", "羽ばたき", "渚","ゴール","全曲再生する"}; } /*else { playlistName = "プレイリスト4"; }*/ Spinner spinner = findViewById(R.id.songname_spinner); // ArrayAdapter ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, spinnerItems); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // spinner に adapter をセット spinner.setAdapter(adapter); TextView areaTextView = findViewById(R.id.area_textview); TextView playlistTextView = findViewById(R.id.playlist_textview); areaTextView.setText(String.format("現在のエリア : %s", areaTexts[1 + areaNumber[nowArea]])); playlistTextView.setText(String.format("プレイリスト : %s", playlistName)); } } ``` ## 参考文献 - 基礎&応用力をしっかり育成! Androidアプリ開発の教科書 なんちゃって開発者にならないための実践ハンズオン - https://www.amazon.co.jp/dp/B078X8H61T/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1 - 著者 斎藤新三 - 監修 山田祥寛 - 発行所  株式会社 翔泳社 - 2018年6月20日 初版第2刷 - MediaPlayer| Androidデベロッパー - Google社 - https://developer.android.com/reference/android/media/MediaPlayer?hl=ja - アクティビティのライフサイクルについて - Google社 - https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ja - 緯度経度を用いた3つの距離計算方法 - (http://www.orsj.or.jp/archive2/or60-12/or60_12_701.pdf) - 三浦 英俊 - オペレーションズ・リサーチ - Volume: 60 Issue: 12 Page: 701-705 Publication year: Dec. 01, 2015 - sensor | Androidデベロッパー - Google社 - https://developer.android.com/reference/android/hardware/Sensor?hl=ja#TYPE_LIGHT ### ノウハウ的に参考にしたけど誰が書いたか分からないので参考文献に載せていいものか怪しいやつ - nyanのブログ https://akira-watson.com/ - 照度せんさ https://seesaawiki.jp/w/moonlight_aska/d/%BE%C8%C5%D9%A5%BB%A5%F3%A5%B5%A1%BC%A4%CE%C3%CD%A4%F2%BC%E8%C6%C0%A4%B9%A4%EB - メディアプレイヤー https://qiita.com/PP_/items/94c704522ac6a6111797 - 加速度センサ https://akira-watson.com/android/accelerometer.html ## 処理の流れ ### 初期設定(アプリ起動後上から順に3つの関数が動作) - クラス内変数の準備(41~77) - OnCreate **(80-147)** - アプリとしての初期設定(84~89) - デフォルトのcreate - ボタンなどを追加できるような - スピナーの設定(92~147) - (エリア用)(94~104) - スピナーの内容を決める - (再生リスト用) (106~) - スピナーの内容を決める (106~116) - スピナーから選ばれた時のイベント(119~147) - GPS、センサの初期化 (150~153) - GPSのしょきか(152) - センサの準備(153) - ボタンが押された時のイベント(156~191) - 一時停止ボタン(158~167) - 再生ボタン(170~179) - 停止ボタン(182~191) - OnStart(196~205) - アプリとしての初期動作(201) - GPSの計測開始(204) - OnResume(207~229) - アプリとしての初期動作(213) - センサの初期設定(218~226) - センサのイベント開始(220) ### 終了するときに呼ばれる関数 - onpause(234~245) - アプリとしての中断動作(238) - センサの測定の停止(243) - onStop(247~254) - アプリとしての停止動作(251) - GPSの機能の停止(253) ### アプリの動作に関する関数 - GPSに関する関数 - initLocationManger(258~262) - 監視員と測定に使うデータを決める - locationStart(273~277) - GPSの測定開始(監視員に頼む) - checkpermission(264~271) - GPSの機能を使っていいかの確認 - onLocationChanged(283~303) - 緯度、経度の取得(288~289) - どこの場所に近いか確認(292) - プレイリストの決定(293) - 画面表示の更新(298~302) - locationStop(279~281) - GPSの測定終了 - onProviderDisabled(341~343) - (定義だけ必要だが、実際はログを出すだけで動かない) - onProviderEnabled(346~349) - (定義だけ必要だが、実際はログを出すだけで動かない) - センサの値が更新されたときの関数 - onSensorChanged(352~414) - 明るさの取得(359) - 直近の明るさでの判断(368~394) - 音楽の割り込み(369~384) - 画面表示の更新(曲名)(387~388) - 平均的な明るさでの判断(396~411) - 音楽の割り込み(398~405) - 画面表示の更新(曲名)(408~409) - 画面表示の更新(センサの値)(412~414) - onAccurayChanged(421~424) - (定義だけ必要だが、実際は動かない - 音楽の再生に関する関数 - AudioPlay(457~501) - 一時停止であれば続きから再生 - これからかける曲を決めて、再生 - AudioSetup(428~455) - 音楽ファイルの選択 - 流し終わった時のイベントの設定 - mediaPlayer.setOnCompletionListener - AudioStop(503~527) - 今かかっている曲を終了。 - メディアプレイヤーの過去の情報もリセット - 画面上のデータの更新 - 最新の位置情報を取得 - AudioPause(529~535) - 今かかっている曲の一時停止 - GPS周りで細かい計算、処理に使っている関数 - getNowArea(315~339) - 指定したチェックポイントと比較し現在位置のエリアを取得 - calcDistance (306~313) - 地球上の距離を求める - updateplaylist(536~576) - 近くのチェックポイントからプレイリストを変更(537~568) - 画面表示の更新(曲名など)(571~574) 幾つかの場合 音楽の再生に関する動作