Java tutorial
/* * This file is part of the NoiseCapture application and OnoMap system. * * The 'OnoMaP' system is led by Lab-STICC and Ifsttar and generates noise maps via * citizen-contributed noise data. * * This application is co-funded by the ENERGIC-OD Project (European Network for * Redistributing Geospatial Information to user Communities - Open Data). ENERGIC-OD * (http://www.energic-od.eu/) is partially funded under the ICT Policy Support Programme (ICT * PSP) as part of the Competitiveness and Innovation Framework Programme by the European * Community. The application work is also supported by the French geographic portal GEOPAL of the * Pays de la Loire region (http://www.geopal.org). * * Copyright (C) IFSTTAR - LAE and Lab-STICC CNRS UMR 6285 Equipe DECIDE Vannes * * NoiseCapture is a free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of * the License, or(at your option) any later version. NoiseCapture is distributed in the hope that * it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details.You should have received a copy of the GNU General Public License along with this * program; if not, write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA or see For more information, write to Ifsttar, * 14-20 Boulevard Newton Cite Descartes, Champs sur Marne F-77447 Marne la Vallee Cedex 2 FRANCE * or write to scientific.computing@ifsttar.fr */ package org.noise_planet.noisecapture; import android.Manifest; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.GpsStatus; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.media.AudioManager; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.support.v4.content.ContextCompat; import org.orbisgis.sos.LeqStats; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * Fetch the most precise location from different location services. */ public class MeasurementService extends Service { private enum LISTENER { GPS, NETWORK, PASSIVE } private LocationManager gpsLocationManager; private LocationManager passiveLocationManager; private LocationManager networkLocationManager; private CommonLocationListener gpsLocationListener; private CommonLocationListener networkLocationListener; private CommonLocationListener passiveLocationListener; // New measurement record sent to the database Event object is Storage.Leq public static final String PROP_NEW_MEASUREMENT = "PROP_NEW_MEASUREMENT"; private long minTimeDelay = 1000; private static final long MAXIMUM_LOCATION_HISTORY = 50; private AudioProcess audioProcess; private AtomicBoolean isRecording = new AtomicBoolean(false); // Is microphone activated private AtomicBoolean isPaused = new AtomicBoolean(false); // Recording is temporary paused private AtomicBoolean isStorageActivated = new AtomicBoolean(false); // Is leq are stored into database private AtomicBoolean canceled = new AtomicBoolean(false); // 1s leq recorded in db private AtomicInteger leqAdded = new AtomicInteger(0); private MeasurementManager measurementManager; private DoProcessing doProcessing = new DoProcessing(this); // This measurement identifier in the long term storage private int recordId = -1; // Keep the measurement only if the count of leq is equal or greater than minimalLeqCount private int minimalLeqCount = 0; // Seconds to delete when pause is activated private int deletedLeqOnPause = 0; private double dBGain = 0; private PropertyChangeSupport listeners = new PropertyChangeSupport(this); private static final Logger LOGGER = LoggerFactory.getLogger(MeasurementService.class); private NavigableMap<Long, Location> timeLocation = new TreeMap<Long, Location>(); private LeqStats leqStats = new LeqStats(); private LeqStats leqStatsFast = new LeqStats(); private NotificationManager mNM; // Unique Identification Number for the Notification. // We use it on Notification start, and to cancel it. private int NOTIFICATION = R.string.local_service_started; private Notification.Builder notification; private Notification notificationInstance; /** * Class for clients to access. Because we know this service always * runs in the same process as its clients, we don't need to deal with * IPC. */ public class LocalBinder extends Binder { MeasurementService getService() { return MeasurementService.this; } } /** * @param dBGain Gain in dB */ public void setdBGain(double dBGain) { this.dBGain = dBGain; if (audioProcess != null && Double.compare(0, dBGain) != 0) { audioProcess.setGain((float) Math.pow(10, dBGain / 20)); } } public LeqStats getLeqStats() { return leqStats; } public LeqStats getFastLeqStats() { return leqStatsFast; } public int getRecordId() { return recordId; } public void cancel() { canceled.set(true); isRecording.set(false); stopLocalisationServices(); } public boolean isCanceled() { return canceled.get(); } public int getLeqAdded() { return leqAdded.get(); } /** * @return AudioProcess or null if recording has not been started */ public AudioProcess getAudioProcess() { return audioProcess; } @Override public void onCreate() { mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); this.measurementManager = new MeasurementManager(getApplicationContext()); // Display a notification about us starting. We put an icon in the status bar. showNotification(); // Mute NoiseCapture while measuring (do not capture android sounds) try { AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mgr.setStreamMute(AudioManager.STREAM_SYSTEM, true); } catch (SecurityException ex) { // Ignore } } /** * Keep the measurement only if the count of leq is equal or greater than minimalLeqCount * @param minimalLeqCount Minimal seconds */ public void setMinimalLeqCount(int minimalLeqCount) { this.minimalLeqCount = minimalLeqCount; } @Override public int onStartCommand(Intent intent, int flags, int startId) { return START_NOT_STICKY; } @Override public void onDestroy() { // Hide notification mNM.cancel(NOTIFICATION); // Stop record if (isRecording()) { cancel(); } try { AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mgr.setStreamMute(AudioManager.STREAM_SYSTEM, false); } catch (SecurityException ex) { // Ignore } } /*** * @return Get last precision in meters. Null if no available location */ public Location getLastLocation() { return timeLocation.isEmpty() ? null : timeLocation.lastEntry().getValue(); } @Override public IBinder onBind(Intent intent) { return mBinder; } public void startRecording() { canceled.set(false); initLocalisationServices(); isRecording.set(true); this.audioProcess = new AudioProcess(isRecording, canceled); if (Double.compare(0, dBGain) != 0) { audioProcess.setGain((float) Math.pow(10, dBGain / 20)); } audioProcess.getListeners().addPropertyChangeListener(doProcessing); // Start measurement new Thread(audioProcess).start(); // Change notification icon message showNotification(); } public boolean isPaused() { return isPaused.get(); } public void stopRecording() { isRecording.set(false); } // This is the object that receives interactions from clients. See // RemoteService for a more complete example. private final IBinder mBinder = new LocalBinder(); public static int getNotificationIcon() { boolean useWhiteIcon = (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP); return useWhiteIcon ? R.drawable.ic_measure_notification : R.mipmap.ic_launcher; } /** * Show a notification while this service is running. */ private void showNotification() { // Do not stack notifications mNM.cancelAll(); // Text for the ticker CharSequence text = isStoring() ? getString(R.string.notification_record_content, audioProcess.getLeq()) : getText(R.string.record_message); // Set the info for the views that show in the notification panel. if (notification == null) { // The PendingIntent to launch our activity if the user selects this notification PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, MeasurementActivity.class), 0); notification = new Notification.Builder(this).setSmallIcon(getNotificationIcon()) // the status icon .setWhen(System.currentTimeMillis()).setTicker(text) // the status text .setWhen(System.currentTimeMillis()) // the time stamp .setContentTitle(getString(R.string.title_service_measurement)) // the label // of the // entry .setContentText(text) // the contents of the entry .setContentIntent(contentIntent) // The intent to send when the entry is clicked ; } else { notification.setContentText(text); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { notification.setUsesChronometer(true); } // Send the notification. notificationInstance = notification.getNotification(); mNM.notify(NOTIFICATION, notificationInstance); } private void initLocalisationServices() { initPassive(); initGPS(); initNetworkLocation(); } private void stopLocalisationServices() { stopPassive(); stopGPS(); stopNetworkLocation(); } private void restartLocalisationServices() { LOGGER.info("Restart localisation services"); stopLocalisationServices(); initLocalisationServices(); } private void initPassive() { if (passiveLocationListener == null) { passiveLocationListener = new CommonLocationListener(this, LISTENER.PASSIVE); } passiveLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && passiveLocationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)) { passiveLocationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, minTimeDelay, 0, passiveLocationListener); } } private void initNetworkLocation() { if (networkLocationListener == null) { networkLocationListener = new CommonLocationListener(this, LISTENER.NETWORK); } networkLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && networkLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { networkLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, minTimeDelay, 0, networkLocationListener); } } private void stopGPS() { if (gpsLocationListener == null || gpsLocationManager == null) { return; } if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && passiveLocationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)) { gpsLocationManager.removeUpdates(gpsLocationListener); } } private void stopPassive() { if (passiveLocationListener == null || passiveLocationManager == null) { return; } passiveLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && passiveLocationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)) { passiveLocationManager.removeUpdates(passiveLocationListener); } } private void stopNetworkLocation() { if (networkLocationListener == null || networkLocationManager == null) { return; } networkLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && networkLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { networkLocationManager.removeUpdates(networkLocationListener); } } private void initGPS() { if (gpsLocationListener == null) { gpsLocationListener = new CommonLocationListener(this, LISTENER.GPS); } gpsLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && passiveLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { gpsLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTimeDelay, 0, gpsLocationListener); } } public void addPropertyChangeListener(PropertyChangeListener propertyChangeListener) { listeners.addPropertyChangeListener(propertyChangeListener); } public void removePropertyChangeListener(PropertyChangeListener propertyChangeListener) { listeners.removePropertyChangeListener(propertyChangeListener); } public void setPause(boolean newState) { isPaused.set(newState); LOGGER.info("Measurement pause = " + String.valueOf(newState)); audioProcess.setDoFastLeq(!newState); audioProcess.setDoOneSecondLeq(!newState); if (newState && deletedLeqOnPause > 0 && recordId > -1) { // Delete last recorded leq int deletedLeq = measurementManager.deleteLastLeqs(recordId, System.currentTimeMillis() - (deletedLeqOnPause * 1000)); leqAdded.set(Math.max(0, leqAdded.get() - deletedLeq)); // Recompute LeqStats altered by the removed leq LeqStats newLeqStats = new LeqStats(); // Query database List<Integer> frequencies = new ArrayList<Integer>(); List<Float[]> leqValues = new ArrayList<Float[]>(); measurementManager.getRecordLeqs(recordId, frequencies, leqValues, null); // parse each leq window time for (Float[] leqFreqs : leqValues) { double rms = 0; for (float leqValue : leqFreqs) { rms += Math.pow(10, leqValue / 10); } newLeqStats.addLeq(10 * Math.log10(rms)); } leqStats = newLeqStats; leqStatsFast = new LeqStats(newLeqStats); } else if (newState && recordId > -1) { leqStatsFast = new LeqStats(); } } /** * @param deletedLeqOnPause Number of leq to delete on pause */ public void setDeletedLeqOnPause(int deletedLeqOnPause) { this.deletedLeqOnPause = Math.max(0, deletedLeqOnPause); } /** * @return Deleted leq triggered by a pause */ public int getDeletedLeqOnPause() { return deletedLeqOnPause; } public void addLocation(Location location) { // Check if the previous location is inside the precision range of the new location // Keep the new location only if the new location is 60% chance away from previous location // and if the new location precision is at least with better accuracy and at most worst // than two times of old location // see https://developer.android.com/guide/topics/location/strategies.html Location previousLocation = timeLocation.isEmpty() ? null : timeLocation.lastEntry().getValue(); if (previousLocation == null || (location.getProvider().equals(LocationManager.GPS_PROVIDER) || previousLocation.getProvider().equals(LocationManager.NETWORK_PROVIDER) || previousLocation.getProvider().equals(LocationManager.PASSIVE_PROVIDER))) { timeLocation.put(System.currentTimeMillis(), location); if (timeLocation.size() > MAXIMUM_LOCATION_HISTORY) { // Clean old entry timeLocation.remove(timeLocation.firstKey()); } } } /** * Fetch the nearest location acquired during the provided utc time. * @param utcTime UTC time * @return Location or null if not found. */ public Location fetchLocation(Long utcTime) { Map.Entry<Long, Location> low = timeLocation.floorEntry(utcTime); Map.Entry<Long, Location> high = timeLocation.ceilingEntry(utcTime); Location res = null; long key = 0; if (low != null && high != null) { // Got two results, find nearest res = Math.abs(utcTime - low.getKey()) < Math.abs(utcTime - high.getKey()) ? low.getValue() : high.getValue(); key = Math.abs(utcTime - low.getKey()) < Math.abs(utcTime - high.getKey()) ? low.getKey() : high.getKey(); } else if (low != null || high != null) { // Just one range bound, search the good one res = low != null ? low.getValue() : high.getValue(); key = low != null ? low.getKey() : high.getKey(); } if (BuildConfig.DEBUG) { System.out.println("Fetch time offset " + (utcTime - key) + " ms"); } return res; } private static class CommonLocationListener implements LocationListener, GpsStatus.Listener, GpsStatus.NmeaListener { private MeasurementService measurementService; private LISTENER listenerId; public CommonLocationListener(MeasurementService measurementService, LISTENER listenerId) { this.measurementService = measurementService; this.listenerId = listenerId; } @Override public void onGpsStatusChanged(int event) { } @Override public void onLocationChanged(Location location) { measurementService.addLocation(location); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { measurementService.restartLocalisationServices(); } @Override public void onProviderDisabled(String provider) { measurementService.restartLocalisationServices(); } private int nmeaChecksum(String s) { int c = 0; for (char ch : s.toCharArray()) { c ^= ch; } return c; } @Override public void onNmeaReceived(long timestamp, String nmea) { if (nmea == null || !nmea.startsWith("$")) { return; } StringTokenizer stringTokenizer = new StringTokenizer(nmea, ","); //TODO read NMEA // Used by bluetooth GPS receivers } } private static class DoProcessing implements PropertyChangeListener { private MeasurementService measurementService; public DoProcessing(MeasurementService measurementService) { this.measurementService = measurementService; } @Override public void propertyChange(PropertyChangeEvent event) { // Skip event if we do not record or if the pause is active if (AudioProcess.PROP_DELAYED_STANDART_PROCESSING.equals(event.getPropertyName())) { if (measurementService.isStoring() && !measurementService.isPaused.get()) { // Delayed audio processing AudioProcess.AudioMeasureResult measure = (AudioProcess.AudioMeasureResult) event.getNewValue(); Location location = measurementService.fetchLocation(measure.getBeginRecordTime()); Storage.Leq leq; if (location == null) { leq = new Storage.Leq(measurementService.recordId, -1, measure.getBeginRecordTime(), 0, 0, null, null, null, 0.f, 0); } else { leq = new Storage.Leq(measurementService.recordId, -1, measure.getBeginRecordTime(), location.getLatitude(), location.getLongitude(), location.hasAltitude() ? location.getAltitude() : null, location.hasSpeed() ? location.getSpeed() : null, location.hasBearing() ? location.getBearing() : null, location.getAccuracy(), location.getTime()); } double[] freqValues = measurementService.audioProcess.getDelayedCenterFrequency(); final float[] leqs = measure.getLeqs(); // Add leqs to stats measurementService.leqStats.addLeq(measure.getGlobaldBaValue()); // Update notification measurementService.showNotification(); List<Storage.LeqValue> leqValueList = new ArrayList<>(leqs.length); for (int idFreq = 0; idFreq < leqs.length; idFreq++) { leqValueList.add(new Storage.LeqValue(-1, (int) freqValues[idFreq], leqs[idFreq])); } measurementService.measurementManager .addLeqBatch(new MeasurementManager.LeqBatch(leq, leqValueList)); measurementService.leqAdded.addAndGet(1); measurementService.listeners.firePropertyChange(PROP_NEW_MEASUREMENT, null, new MeasurementEventObject(measure, leq)); } } else if (AudioProcess.PROP_MOVING_SPECTRUM.equals(event.getPropertyName())) { if (measurementService.isStoring() && !measurementService.isPaused.get()) { AudioProcess.AudioMeasureResult measure = (AudioProcess.AudioMeasureResult) event.getNewValue(); measurementService.leqStatsFast.addLeq(measure.getGlobaldBaValue()); } } else if (AudioProcess.PROP_STATE_CHANGED.equals(event.getPropertyName())) { if (AudioProcess.STATE.CLOSED.equals(event.getNewValue())) { if (measurementService.recordId > -1) { // Recording and processing of audio has been closed // Cancel the persistent notification. if (measurementService.canceled.get() || measurementService.leqAdded.get() < measurementService.minimalLeqCount) { // Canceled or has not the minimal leq count // Destroy record measurementService.measurementManager.deleteRecord(measurementService.recordId); } else { // Update record measurementService.measurementManager.updateRecordFinal(measurementService.recordId, (float) measurementService.leqStats.getLeqMean(), measurementService.leqAdded.get(), (float) measurementService.dBGain); } } measurementService.isRecording.set(false); measurementService.stopLocalisationServices(); // Stop task measurementService.stopForeground(true); measurementService.stopSelf(); } } measurementService.listeners.firePropertyChange(event); } } /** * @return True if microphone is enabled */ public boolean isRecording() { return isRecording.get(); } /** * @return True if storage of records are activated */ public boolean isStoring() { return isStorageActivated.get(); } /** * Start the storage of leq in database */ public void startStorage() { if (!isRecording()) { startRecording(); } recordId = measurementManager.addRecord(); leqAdded.set(0); isStorageActivated.set(true); showNotification(); // Set is foreground in order to let this service running without stopping startForeground(NOTIFICATION, notificationInstance); } public static final class MeasurementEventObject { public final Storage.Leq leq; public final AudioProcess.AudioMeasureResult measure; public MeasurementEventObject(AudioProcess.AudioMeasureResult measure, Storage.Leq leq) { this.measure = measure; this.leq = leq; } } }