/* * Copyright (C) 2010, 2011, 2012 Herbert von Broeuschmeul * Copyright (C) 2010, 2011, 2012 BluetoothGPS4Droid Project * * This file is part of BluetoothGPS4Droid. * * BluetoothGPS4Droid is 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. * * BluetoothGPS4Droid 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 BluetoothGPS4Droid. If not, see . */ package org.broeuschmeul.android.gps.bluetooth.provider; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.broeuschmeul.android.gps.nmea.util.NmeaParser; import org.broeuschmeul.android.gps.sirf.util.SirfUtils; import android.app.AppOpsManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.content.SharedPreferences; import android.content.Intent; import android.location.LocationManager; import android.location.GpsStatus.NmeaListener; import android.preference.PreferenceManager; import android.provider.Settings; import android.os.SystemClock; import android.util.Log; /** * This class is used to establish and manage the connection with the bluetooth GPS. * * @author Herbert von Broeuschmeul * */ public class BlueetoothGpsManager { /** * Tag used for log messages */ private static final String LOG_TAG = "BlueGPS"; /** * A utility class used to manage the communication with the bluetooth GPS whn the connection has been established. * It is used to read NMEA data from the GPS or to send SIRF III binary commands or SIRF III NMEA commands to the GPS. * You should run the main read loop in one thread and send the commands in a separate one. * * @author Herbert von Broeuschmeul * */ private class ConnectedGps extends Thread { /** * GPS bluetooth socket used for communication. */ private final BluetoothSocket socket; /** * GPS InputStream from which we read data. */ private final InputStream in; /** * GPS output stream to which we send data (SIRF III binary commands). */ private final OutputStream out; /** * GPS output stream to which we send data (SIRF III NMEA commands). */ private final PrintStream out2; /** * A boolean which indicates if the GPS is ready to receive data. * In fact we consider that the GPS is ready when it begins to sends data... */ private boolean ready = false; public ConnectedGps(BluetoothSocket socket) { this.socket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; PrintStream tmpOut2 = null; try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); if (tmpOut != null){ tmpOut2 = new PrintStream(tmpOut, false, "US-ASCII"); } } catch (IOException e) { Log.e(LOG_TAG, "error while getting socket streams", e); } in = tmpIn; out = tmpOut; out2 = tmpOut2; } public boolean isReady(){ return ready; } public void run() { try { BufferedReader reader = new BufferedReader(new InputStreamReader(in,"US-ASCII")); String s; long now = SystemClock.uptimeMillis(); long lastRead = now; while((enabled) && (now < lastRead+5000 )){ if (reader.ready()){ s = reader.readLine(); Log.v(LOG_TAG, "data: "+System.currentTimeMillis()+" "+s); notifyNmeaSentence(s+"\r\n"); ready = true; lastRead = SystemClock.uptimeMillis(); } else { Log.d(LOG_TAG, "data: not ready "+System.currentTimeMillis()); SystemClock.sleep(500); } now = SystemClock.uptimeMillis(); } } catch (IOException e) { Log.e(LOG_TAG, "error while getting data", e); setMockLocationProviderOutOfService(); } finally { // cleanly closing everything... this.close(); disableIfNeeded(); } } /** * Write to the connected OutStream. * @param buffer The bytes to write */ public void write(byte[] buffer) { try { do { Thread.sleep(100); } while ((enabled) && (! ready)); if ((enabled) && (ready)){ out.write(buffer); out.flush(); } } catch (IOException e) { Log.e(LOG_TAG, "Exception during write", e); } catch (InterruptedException e) { Log.e(LOG_TAG, "Exception during write", e); } } /** * Write to the connected OutStream. * @param buffer The data to write */ public void write(String buffer) { try { do { Thread.sleep(100); } while ((enabled) && (! ready)); if ((enabled) && (ready)){ out2.print(buffer); out2.flush(); } } catch (InterruptedException e) { Log.e(LOG_TAG, "Exception during write", e); } } public void close(){ ready = false; try { Log.d(LOG_TAG, "closing Bluetooth GPS output sream"); in.close(); } catch (IOException e) { Log.e(LOG_TAG, "error while closing GPS NMEA output stream", e); } finally { try { Log.d(LOG_TAG, "closing Bluetooth GPS input streams"); out2.close(); out.close(); } catch (IOException e) { Log.e(LOG_TAG, "error while closing GPS input streams", e); } finally { try { Log.d(LOG_TAG, "closing Bluetooth GPS socket"); socket.close(); } catch (IOException e) { Log.e(LOG_TAG, "error while closing GPS socket", e); } } } } } private Service callingService; private BluetoothSocket gpsSocket; private String gpsDeviceAddress; private NmeaParser parser = new NmeaParser(10f); private boolean enabled = false; private ExecutorService notificationPool; private ScheduledExecutorService connectionAndReadingPool; private List nmeaListeners = Collections.synchronizedList(new LinkedList()); private LocationManager locationManager; private SharedPreferences sharedPreferences; private ConnectedGps connectedGps; private int disableReason = 0; private Notification connectionProblemNotification; private Notification serviceStoppedNotification; private Context appContext; private NotificationManager notificationManager; private int maxConnectionRetries; private int nbRetriesRemaining; private boolean connected = false; /** * @param callingService * @param deviceAddress * @param maxRetries */ public BlueetoothGpsManager(Service callingService, String deviceAddress, int maxRetries) { this.gpsDeviceAddress = deviceAddress; this.callingService = callingService; this.maxConnectionRetries = maxRetries; this.nbRetriesRemaining = 1+maxRetries; this.appContext = callingService.getApplicationContext(); locationManager = (LocationManager)callingService.getSystemService(Context.LOCATION_SERVICE); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(callingService); notificationManager = (NotificationManager)callingService.getSystemService(Context.NOTIFICATION_SERVICE); parser.setLocationManager(locationManager); connectionProblemNotification = new Notification(); connectionProblemNotification.icon = R.drawable.ic_stat_notify; Intent stopIntent = new Intent(BluetoothGpsProviderService.ACTION_STOP_GPS_PROVIDER); // PendingIntent stopPendingIntent = PendingIntent.getService(appContext, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT); PendingIntent stopPendingIntent = PendingIntent.getService(appContext, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT); connectionProblemNotification.contentIntent = stopPendingIntent; serviceStoppedNotification = new Notification(); serviceStoppedNotification.icon=R.drawable.ic_stat_notify; Intent restartIntent = new Intent(BluetoothGpsProviderService.ACTION_START_GPS_PROVIDER); PendingIntent restartPendingIntent = PendingIntent.getService(appContext, 0, restartIntent, PendingIntent.FLAG_CANCEL_CURRENT); serviceStoppedNotification.setLatestEventInfo(appContext, appContext.getString(R.string.service_closed_because_connection_problem_notification_title), appContext.getString(R.string.service_closed_because_connection_problem_notification), restartPendingIntent); } private void setDisableReason(int reasonId){ disableReason = reasonId; } /** * @return */ public int getDisableReason(){ return disableReason; } /** * @return true if the bluetooth GPS is enabled */ public synchronized boolean isEnabled() { return enabled; } /** * @return true if mock locations are enabled, respectively if BlueGps is set as the mock location provider */ public boolean isMockLocationProviderEnabled() { boolean isMockLocation = false; try { //if marshmallow if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { AppOpsManager opsManager = (AppOpsManager) appContext.getSystemService(Context.APP_OPS_SERVICE); isMockLocation = (opsManager.checkOp(AppOpsManager.OPSTR_MOCK_LOCATION, android.os.Process.myUid(), "org.broeuschmeul.android.gps.bluetooth.provider")== AppOpsManager.MODE_ALLOWED); } else { // in marshmallow this will always return true isMockLocation = !android.provider.Settings.Secure.getString(appContext.getContentResolver(), "mock_location").equals("0"); } } catch (Exception e) { return isMockLocation; } return isMockLocation; } /** * Enables the bluetooth GPS Provider. * @return */ public synchronized boolean enable() { notificationManager.cancel(R.string.service_closed_because_connection_problem_notification_title); if (! enabled){ Log.d(LOG_TAG, "enabling Bluetooth GPS manager"); final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { // Device does not support Bluetooth Log.e(LOG_TAG, "Device does not support Bluetooth"); disable(R.string.msg_bluetooth_unsupported); } else if (!bluetoothAdapter.isEnabled()) { // Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); // startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); Log.e(LOG_TAG, "Bluetooth is not enabled"); disable(R.string.msg_bluetooth_disabled); } else if (!isMockLocationProviderEnabled()){ Log.e(LOG_TAG, "Mock location provider OFF"); disable(R.string.msg_mock_location_disabled); // } else if ( (! locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) // && (sharedPreferences.getBoolean(BluetoothGpsProviderService.PREF_REPLACE_STD_GPS, true)) // ) { // Log.e(LOG_TAG, "GPS location provider OFF"); // disable(R.string.msg_gps_provider_disabled); } else { final BluetoothDevice gpsDevice = bluetoothAdapter.getRemoteDevice(gpsDeviceAddress); if (gpsDevice == null){ Log.e(LOG_TAG, "GPS device not found"); disable(R.string.msg_bluetooth_gps_unavaible); } else { Log.e(LOG_TAG, "current device: "+gpsDevice.getName() + " -- " + gpsDevice.getAddress()); try { gpsSocket = gpsDevice.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")); } catch (IOException e) { Log.e(LOG_TAG, "Error during connection", e); gpsSocket = null; } if (gpsSocket == null){ Log.e(LOG_TAG, "Error while establishing connection: no socket"); disable(R.string.msg_bluetooth_gps_unavaible); } else { Runnable connectThread = new Runnable() { @Override public void run() { try { connected = false; Log.v(LOG_TAG, "current device: "+gpsDevice.getName() + " -- " + gpsDevice.getAddress()); if ((bluetoothAdapter.isEnabled()) && (nbRetriesRemaining > 0 )){ try { if (connectedGps != null){ connectedGps.close(); } if ((gpsSocket != null) && ((connectedGps == null) || (connectedGps.socket != gpsSocket))){ Log.d(LOG_TAG, "trying to close old socket"); gpsSocket.close(); } } catch (IOException e) { Log.e(LOG_TAG, "Error during disconnection", e); } try { gpsSocket = gpsDevice.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")); } catch (IOException e) { Log.e(LOG_TAG, "Error during connection", e); gpsSocket = null; } if (gpsSocket == null){ Log.e(LOG_TAG, "Error while establishing connection: no socket"); disable(R.string.msg_bluetooth_gps_unavaible); } else { // Cancel discovery because it will slow down the connection bluetoothAdapter.cancelDiscovery(); // we increment the number of connection tries // Connect the device through the socket. This will block // until it succeeds or throws an exception Log.v(LOG_TAG, "connecting to socket"); gpsSocket.connect(); Log.d(LOG_TAG, "connected to socket"); connected = true; // reset eventual disabling cause // setDisableReason(0); // connection obtained so reset the number of connection try nbRetriesRemaining = 1+maxConnectionRetries ; notificationManager.cancel(R.string.connection_problem_notification_title); Log.v(LOG_TAG, "starting socket reading task"); connectedGps = new ConnectedGps(gpsSocket); connectionAndReadingPool.execute(connectedGps); Log.v(LOG_TAG, "socket reading thread started"); } // } else if (! bluetoothAdapter.isEnabled()) { // setDisableReason(R.string.msg_bluetooth_disabled); } } catch (IOException connectException) { // Unable to connect Log.e(LOG_TAG, "error while connecting to socket", connectException); // disable(R.string.msg_bluetooth_gps_unavaible); } finally { nbRetriesRemaining--; if (! connected) { disableIfNeeded(); } } } }; this.enabled = true; Log.d(LOG_TAG, "Bluetooth GPS manager enabled"); Log.v(LOG_TAG, "starting notification thread"); notificationPool = Executors.newSingleThreadExecutor(); Log.v(LOG_TAG, "starting connection and reading thread"); connectionAndReadingPool = Executors.newSingleThreadScheduledExecutor(); Log.v(LOG_TAG, "starting connection to socket task"); connectionAndReadingPool.scheduleWithFixedDelay(connectThread, 5000, 60000, TimeUnit.MILLISECONDS); } } } } return this.enabled; } /** * Disables the bluetooth GPS Provider if the maximal number of connection retries is exceeded. * This is used when there are possibly non fatal connection problems. * In these cases the provider will try to reconnect with the bluetooth device * and only after a given retries number will give up and shutdown the service. */ private synchronized void disableIfNeeded(){ if (enabled){ if (nbRetriesRemaining > 0){ // Unable to connect Log.e(LOG_TAG, "Unable to establish connection"); connectionProblemNotification.when = System.currentTimeMillis(); String pbMessage = appContext.getResources().getQuantityString(R.plurals.connection_problem_notification, nbRetriesRemaining, nbRetriesRemaining); connectionProblemNotification.setLatestEventInfo(appContext, appContext.getString(R.string.connection_problem_notification_title), pbMessage, connectionProblemNotification.contentIntent); connectionProblemNotification.number = 1 + maxConnectionRetries - nbRetriesRemaining; notificationManager.notify(R.string.connection_problem_notification_title, connectionProblemNotification); } else { // notificationManager.cancel(R.string.connection_problem_notification_title); // serviceStoppedNotification.when = System.currentTimeMillis(); // notificationManager.notify(R.string.service_closed_because_connection_problem_notification_title, serviceStoppedNotification); disable(R.string.msg_two_many_connection_problems); } } } /** * Disables the bluetooth GPS provider. * * It will: *
    *
  • close the connection with the bluetooth device
  • *
  • disable the Mock Location Provider used for the bluetooth GPS
  • *
  • stop the BlueGPS4Droid service
  • *
* The reasonId parameter indicates the reason to close the bluetooth provider. * If its value is zero, it's a normal shutdown (normally, initiated by the user). * If it's non-zero this value should correspond a valid localized string id (res/values..../...) * which will be used to display a notification. * * @param reasonId the reason to close the bluetooth provider. */ public synchronized void disable(int reasonId) { Log.d(LOG_TAG, "disabling Bluetooth GPS manager reason: "+callingService.getString(reasonId)); setDisableReason(reasonId); disable(); } /** * Disables the bluetooth GPS provider. * * It will: *
    *
  • close the connection with the bluetooth device
  • *
  • disable the Mock Location Provider used for the bluetooth GPS
  • *
  • stop the BlueGPS4Droid service
  • *
* If the bluetooth provider is closed because of a problem, a notification is displayed. */ public synchronized void disable() { notificationManager.cancel(R.string.connection_problem_notification_title); if (getDisableReason() != 0){ serviceStoppedNotification.when = System.currentTimeMillis(); serviceStoppedNotification.setLatestEventInfo(appContext, appContext.getString(R.string.service_closed_because_connection_problem_notification_title), appContext.getString(R.string.service_closed_because_connection_problem_notification, appContext.getString(getDisableReason())), serviceStoppedNotification.contentIntent); notificationManager.notify(R.string.service_closed_because_connection_problem_notification_title, serviceStoppedNotification); } if (enabled){ Log.d(LOG_TAG, "disabling Bluetooth GPS manager"); enabled = false; connectionAndReadingPool.shutdown(); Runnable closeAndShutdown = new Runnable() { @Override public void run(){ try { connectionAndReadingPool.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } if (!connectionAndReadingPool.isTerminated()){ connectionAndReadingPool.shutdownNow(); if (connectedGps != null){ connectedGps.close(); } if ((gpsSocket != null) && ((connectedGps == null) || (connectedGps.socket != gpsSocket))){ try { Log.d(LOG_TAG, "closing Bluetooth GPS socket"); gpsSocket.close(); } catch (IOException closeException) { Log.e(LOG_TAG, "error while closing socket", closeException); } } } } }; notificationPool.execute(closeAndShutdown); nmeaListeners.clear(); disableMockLocationProvider(); notificationPool.shutdown(); // connectionAndReadingPool.shutdown(); callingService.stopSelf(); Log.d(LOG_TAG, "Bluetooth GPS manager disabled"); } } /** * Enables the Mock GPS Location Provider used for the bluetooth GPS. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#enableMockLocationProvider(java.lang.String) * @param gpsName the name of the Location Provider to use for the bluetooth GPS * @param force true if we want to force auto-activation of the mock location provider (and bypass user preference). */ public void enableMockLocationProvider(String gpsName, boolean force){ if (parser != null){ Log.d(LOG_TAG, "enabling mock locations provider: "+gpsName); parser.enableMockLocationProvider(gpsName, force); } } /** * Enables the Mock GPS Location Provider used for the bluetooth GPS. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#enableMockLocationProvider(java.lang.String) * @param gpsName the name of the Location Provider to use for the bluetooth GPS */ public void enableMockLocationProvider(String gpsName){ if (parser != null){ Log.d(LOG_TAG, "enabling mock locations provider: "+gpsName); boolean force = sharedPreferences.getBoolean(BluetoothGpsProviderService.PREF_FORCE_ENABLE_PROVIDER, false); parser.enableMockLocationProvider(gpsName, force); } } /** * Disables the current Mock GPS Location Provider used for the bluetooth GPS. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#disableMockLocationProvider() */ public void disableMockLocationProvider(){ if (parser != null){ Log.d(LOG_TAG, "disabling mock locations provider"); parser.disableMockLocationProvider(); } } /** * Getter use to know if the Mock GPS Listener used for the bluetooth GPS is enabled or not. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#isMockGpsEnabled() * * @return true if the Mock GPS Listener used for the bluetooth GPS is enabled. */ public boolean isMockGpsEnabled() { boolean mockGpsEnabled = false; if (parser != null){ mockGpsEnabled = parser.isMockGpsEnabled(); } return mockGpsEnabled; } /** * Getter for the name of the current Mock Location Provider in use. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#getMockLocationProvider() * * @return the Mock Location Provider name used for the bluetooth GPS */ public String getMockLocationProvider() { String mockLocationProvider = null; if (parser != null){ mockLocationProvider = parser.getMockLocationProvider(); } return mockLocationProvider; } /** * Indicates that the bluetooth GPS Provider is out of service. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#setMockLocationProviderOutOfService() */ private void setMockLocationProviderOutOfService(){ if (parser != null){ parser.setMockLocationProviderOutOfService(); } } /** * Adds an NMEA listener. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#addNmeaListener(NmeaListener) * @param listener a {@link NmeaListener} object to register * @return true if the listener was successfully added */ public boolean addNmeaListener(NmeaListener listener){ if (!nmeaListeners.contains(listener)){ Log.d(LOG_TAG, "adding new NMEA listener"); nmeaListeners.add(listener); } return true; } /** * Removes an NMEA listener. * In fact, it delegates to the NMEA parser. * * @see NmeaParser#removeNmeaListener(NmeaListener) * @param listener a {@link NmeaListener} object to remove */ public void removeNmeaListener(NmeaListener listener){ Log.d(LOG_TAG, "removing NMEA listener"); nmeaListeners.remove(listener); } /** * Notifies the reception of a NMEA sentence from the bluetooth GPS to registered NMEA listeners. * * @param nmeaSentence the complete NMEA sentence received from the bluetooth GPS (i.e. $....*XY where XY is the checksum) */ private void notifyNmeaSentence(final String nmeaSentence){ if (enabled){ Log.v(LOG_TAG, "parsing and notifying NMEA sentence: "+nmeaSentence); String sentence = null; try { sentence = parser.parseNmeaSentence(nmeaSentence); } catch (SecurityException e){ Log.e(LOG_TAG, "error while parsing NMEA sentence: "+nmeaSentence, e); // a priori Mock Location is disabled sentence = null; disable(R.string.msg_mock_location_disabled); } final String recognizedSentence = sentence; final long timestamp = System.currentTimeMillis(); if (recognizedSentence != null){ Log.v(LOG_TAG, "notifying NMEA sentence: "+recognizedSentence); synchronized(nmeaListeners) { for(final NmeaListener listener : nmeaListeners){ notificationPool.execute(new Runnable(){ @Override public void run() { listener.onNmeaReceived(timestamp, recognizedSentence); } }); } } } } } /** * Sends a NMEA sentence to the bluetooth GPS. * * @param command the complete NMEA sentence (i.e. $....*XY where XY is the checksum). */ public void sendPackagedNmeaCommand(final String command){ Log.d(LOG_TAG, "sending NMEA sentence: "+command); if (isEnabled()){ notificationPool.execute( new Runnable() { @Override public void run() { while ((enabled) && ((!connected) || (connectedGps == null) || (!connectedGps.isReady()))){ Log.v(LOG_TAG, "writing thread is not ready"); SystemClock.sleep(500); } if (isEnabled() && (connectedGps != null)){ connectedGps.write(command); Log.d(LOG_TAG, "sent NMEA sentence: "+command); } } }); } } /** * Sends a SIRF III binary command to the bluetooth GPS. * * @param commandHexa an hexadecimal string representing a complete binary command * (i.e. with the Start Sequence, Payload Length, Payload, Message Checksum and End Sequence). */ public void sendPackagedSirfCommand(final String commandHexa){ Log.d(LOG_TAG, "sending SIRF sentence: "+commandHexa); if (isEnabled()){ final byte[] command = SirfUtils.genSirfCommand(commandHexa); notificationPool.execute( new Runnable() { @Override public void run() { while ((enabled) && ((!connected) || (connectedGps == null) || (!connectedGps.isReady()))){ Log.v(LOG_TAG, "writing thread is not ready"); SystemClock.sleep(500); } if (isEnabled() && (connectedGps != null)){ connectedGps.write(command); Log.d(LOG_TAG, "sent SIRF sentence: "+commandHexa); } } }); } } /** * Sends a NMEA sentence to the bluetooth GPS. * * @param sentence the NMEA sentence without the first "$", the last "*" and the checksum. */ public void sendNmeaCommand(String sentence){ String command = String.format((Locale)null,"$%s*%X\r\n", sentence, parser.computeChecksum(sentence)); sendPackagedNmeaCommand(command); } /** * Sends a SIRF III binary command to the bluetooth GPS. * * @param payload an hexadecimal string representing the payload of the binary command * (i.e. without Start Sequence, Payload Length, Message Checksum and End Sequence). */ public void sendSirfCommand(String payload){ String command = SirfUtils.createSirfCommandFromPayload(payload); sendPackagedSirfCommand(command); } }