Search code examples
androidgeolocationbluetooth-lowenergy

Adroid - I get location only on battery saving GPS mode (and why I am getting it twice?)


I want my app when receiving data from a Bluetooth LE device to add a date&time and a location prefix to the incoming data. What is really strange is that the location is calculated only if I am using the battery saving GPS option (that calculates only from WiFi and network signal). If I use the GPS-only or the high precision option (GPS and network) it doesn't show the location.

On the manifest file I use the permission:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature android:name="android.hardware.location.network" />

Moreover, on that battery saving mode always, the latitude and longitude for some unknown reason get also printed after the bluetooth message (something that I have not implied)! If someone can also assist me to why that is happening I would be grateful.

The corresponding code follows:

public class Chat extends Activity {
private final static String TAG = Chat.class.getSimpleName();

public static final String EXTRAS_DEVICE = "EXTRAS_DEVICE";
private TextView tv = null;
private EditText et = null;
private Button btn = null;
private String mDeviceName;
private String mDeviceAddress;
private RBLService mBluetoothLeService;
private Map<UUID, BluetoothGattCharacteristic> map = new HashMap<UUID, BluetoothGattCharacteristic>();
private FusedLocationProviderClient mFusedLocationClient;

private final ServiceConnection mServiceConnection = new ServiceConnection() {

    @Override
    public void onServiceConnected(ComponentName componentName,
            IBinder service) {
        mBluetoothLeService = ((RBLService.LocalBinder) service)
                .getService();
        if (!mBluetoothLeService.initialize()) {
            Log.e(TAG, "Unable to initialize Bluetooth");
            finish();
        }
        // Automatically connects to the device upon successful start-up
        // initialization.
        mBluetoothLeService.connect(mDeviceAddress);
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        mBluetoothLeService = null;
    }
};

private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();

        if (RBLService.ACTION_GATT_DISCONNECTED.equals(action)) {
        } else if (RBLService.ACTION_GATT_SERVICES_DISCOVERED
                .equals(action)) {
            getGattService(mBluetoothLeService.getSupportedGattService());
        } else if (RBLService.ACTION_DATA_AVAILABLE.equals(action)) {
            displayData(intent.getByteArrayExtra(RBLService.EXTRA_DATA));
        }
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setContentView(R.layout.second);
    mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);

    tv = (TextView) findViewById(R.id.textView);
    tv.setMovementMethod(ScrollingMovementMethod.getInstance());
    et = (EditText) findViewById(R.id.editText);
    btn = (Button) findViewById(R.id.send);
    btn.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
            BluetoothGattCharacteristic characteristic = map
                    .get(RBLService.UUID_BLE_SHIELD_TX);

            String str = et.getText().toString();
            byte b = 0x00;
            byte[] tmp = str.getBytes();
            byte[] tx = new byte[tmp.length + 1];
            tx[0] = b;
            for (int i = 1; i < tmp.length + 1; i++) {
                tx[i] = tmp[i - 1];
            }

            characteristic.setValue(tx);
            mBluetoothLeService.writeCharacteristic(characteristic);

            et.setText("");
        }
    });

    Intent intent = getIntent();

    mDeviceAddress = intent.getStringExtra(Device.EXTRA_DEVICE_ADDRESS);
    mDeviceName = intent.getStringExtra(Device.EXTRA_DEVICE_NAME);

    getActionBar().setTitle(mDeviceName);
    getActionBar().setDisplayHomeAsUpEnabled(true);

    Intent gattServiceIntent = new Intent(this, RBLService.class);
    bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);

    AlertDialog.Builder builder1 = new AlertDialog.Builder(this);
    builder1.setMessage("Παρακαλώ ενεργοποιήστε το gps σε υψηλή ακρίβεια για καταγραφή της τοποθεσίας των μετρήσεων.");
    builder1.setCancelable(true);

    builder1.setPositiveButton(
            "Οκ, έγινε",
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {
                    dialog.cancel();
                }
            });

    AlertDialog alert11 = builder1.create();
    alert11.show();

}


@Override
protected void onResume() {
    super.onResume();

    registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == android.R.id.home) {
        mBluetoothLeService.disconnect();
        mBluetoothLeService.close();

        System.exit(0);
    }

    return super.onOptionsItemSelected(item);
}

@Override
protected void onStop() {
    super.onStop();

    unregisterReceiver(mGattUpdateReceiver);
}

@Override
protected void onDestroy() {
    super.onDestroy();

    mBluetoothLeService.disconnect();
    mBluetoothLeService.close();

    System.exit(0);
}

private void displayData(byte[] byteArray) {
    if (byteArray != null) {
        getLocation();
        getDate();
        tv.append("\n");
        String data = new String(byteArray);
        tv.append(data);
        tv.append("\n");
        // find the amount we need to scroll. This works by
        // asking the TextView's internal layout for the position
        // of the final line and then subtracting the TextView's height
        final int scrollAmount = tv.getLayout().getLineTop(
                tv.getLineCount())
                - tv.getHeight();
        // if there is no need to scroll, scrollAmount will be <=0
        if (scrollAmount > 0)
            tv.scrollTo(0, scrollAmount);
        else
            tv.scrollTo(0, 0);
    }
}

private void getDate() {
    Calendar c = Calendar.getInstance();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd-HH:mm:ss");
    String strDate = sdf.format(c.getTime());
    tv.append(strDate);
}

private void getLocation() {
    try {

        mFusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, new OnSuccessListener<Location>() {
                    @Override
                    public void onSuccess(Location location) {
                        // Got last known location. In some rare situations this can be null.
                        if (location != null) {
                            double lat = location.getLatitude();
                            double lng = location.getLongitude();
                            String latitude = String.valueOf(lat);
                            String longitude = String.valueOf(lng);
                            tv.append(latitude);
                            tv.append(",");
                            tv.append(longitude);
                            tv.append("/");
                        }
                    }
                });

    } catch (SecurityException e) {
        // lets the user know there is a problem with the gps
    }
}

private void getGattService(BluetoothGattService gattService) {
    if (gattService == null)
        return;

    BluetoothGattCharacteristic characteristic = gattService
            .getCharacteristic(RBLService.UUID_BLE_SHIELD_TX);
    map.put(characteristic.getUuid(), characteristic);

    BluetoothGattCharacteristic characteristicRx = gattService
            .getCharacteristic(RBLService.UUID_BLE_SHIELD_RX);
    mBluetoothLeService.setCharacteristicNotification(characteristicRx,
            true);
    mBluetoothLeService.readCharacteristic(characteristicRx);
    }

private static IntentFilter makeGattUpdateIntentFilter() {
    final IntentFilter intentFilter = new IntentFilter();

    intentFilter.addAction(RBLService.ACTION_GATT_CONNECTED);
    intentFilter.addAction(RBLService.ACTION_GATT_DISCONNECTED);
    intentFilter.addAction(RBLService.ACTION_GATT_SERVICES_DISCOVERED);
    intentFilter.addAction(RBLService.ACTION_DATA_AVAILABLE);

    return intentFilter;
    }
}

and the RBLservice.java is:

/**
* Service for managing connection and data communication with a GATT server
* hosted on a given Bluetooth LE device.
*/
public class RBLService extends Service {
private final static String TAG = RBLService.class.getSimpleName();
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;

public final static String ACTION_GATT_CONNECTED = "ACTION_GATT_CONNECTED";
public final static String ACTION_GATT_DISCONNECTED = "ACTION_GATT_DISCONNECTED";
public final static String ACTION_GATT_SERVICES_DISCOVERED = "ACTION_GATT_SERVICES_DISCOVERED";
public final static String ACTION_GATT_RSSI = "ACTION_GATT_RSSI";
public final static String ACTION_DATA_AVAILABLE = "ACTION_DATA_AVAILABLE";
public final static String EXTRA_DATA = "EXTRA_DATA";

public final static UUID UUID_BLE_SHIELD_TX = UUID
        .fromString(RBLGattAttributes.BLE_SHIELD_TX);
public final static UUID UUID_BLE_SHIELD_RX = UUID
        .fromString(RBLGattAttributes.BLE_SHIELD_RX);
public final static UUID UUID_BLE_SHIELD_SERVICE = UUID
        .fromString(RBLGattAttributes.BLE_SHIELD_SERVICE);

private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                        int newState) {
        String intentAction;

        if (newState == BluetoothProfile.STATE_CONNECTED) {
            intentAction = ACTION_GATT_CONNECTED;
            broadcastUpdate(intentAction);
            Log.i(TAG, "Connected to GATT server.");
            // Attempts to discover services after successful connection.
            Log.i(TAG, "Attempting to start service discovery:"
                    + mBluetoothGatt.discoverServices());
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            intentAction = ACTION_GATT_DISCONNECTED;
            Log.i(TAG, "Disconnected from GATT server.");
            broadcastUpdate(intentAction);
        }
    }

    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            broadcastUpdate(ACTION_GATT_RSSI, rssi);
        } else {
            Log.w(TAG, "onReadRemoteRssi received: " + status);
        }
    }

    ;

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
        } else {
            Log.w(TAG, "onServicesDiscovered received: " + status);
        }
    }

    @Override
    public void onCharacteristicRead(BluetoothGatt gatt,
                                     BluetoothGattCharacteristic characteristic, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
        }
    }

    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic) {
        broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
    }
};

private void broadcastUpdate(final String action) {
    final Intent intent = new Intent(action);
    sendBroadcast(intent);
}

private void broadcastUpdate(final String action, int rssi) {
    final Intent intent = new Intent(action);
    intent.putExtra(EXTRA_DATA, String.valueOf(rssi));
    sendBroadcast(intent);
}

private void broadcastUpdate(final String action,
                             final BluetoothGattCharacteristic characteristic) {
    final Intent intent = new Intent(action);

    // This is special handling for the Heart Rate Measurement profile. Data
    // parsing is
    // carried out as per profile specifications:
    // http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml
    if (UUID_BLE_SHIELD_RX.equals(characteristic.getUuid())) {
        final byte[] rx = characteristic.getValue();
        intent.putExtra(EXTRA_DATA, rx);
    }

    sendBroadcast(intent);
}

public class LocalBinder extends Binder {
    RBLService getService() {
        return RBLService.this;
    }
}

@Override
public IBinder onBind(Intent intent) {
    return mBinder;
}

@Override
public boolean onUnbind(Intent intent) {
    // After using a given device, you should make sure that
    // BluetoothGatt.close() is called
    // such that resources are cleaned up properly. In this particular
    // example, close() is
    // invoked when the UI is disconnected from the Service.
    close();
    return super.onUnbind(intent);
}

private final IBinder mBinder = new LocalBinder();

/**
 * Initializes a reference to the local Bluetooth adapter.
 *
 * @return Return true if the initialization is successful.
 */
public boolean initialize() {
    // For API level 18 and above, get a reference to BluetoothAdapter
    // through
    // BluetoothManager.
    if (mBluetoothManager == null) {
        mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        if (mBluetoothManager == null) {
            Log.e(TAG, "Unable to initialize BluetoothManager.");
            return false;
        }
    }

    mBluetoothAdapter = mBluetoothManager.getAdapter();
    if (mBluetoothAdapter == null) {
        Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
        return false;
    }

    return true;
}

/**
 * Connects to the GATT server hosted on the Bluetooth LE device.
 *
 * @param address The device address of the destination device.
 * @return Return true if the connection is initiated successfully. The
 * connection result is reported asynchronously through the
 * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
 * callback.
 */
public boolean connect(final String address) {
    if (mBluetoothAdapter == null || address == null) {
        Log.w(TAG,
                "BluetoothAdapter not initialized or unspecified address.");
        return false;
    }

    // Previously connected device. Try to reconnect.
    if (mBluetoothDeviceAddress != null
            && address.equals(mBluetoothDeviceAddress)
            && mBluetoothGatt != null) {
        Log.d(TAG,
                "Trying to use an existing mBluetoothGatt for connection.");
        if (mBluetoothGatt.connect()) {
            return true;
        } else {
            return false;
        }
    }

    final BluetoothDevice device = mBluetoothAdapter
            .getRemoteDevice(address);
    if (device == null) {
        Log.w(TAG, "Device not found.  Unable to connect.");
        return false;
    }
    // We want to directly connect to the device, so we are setting the
    // autoConnect
    // parameter to false.
    mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
    Log.d(TAG, "Trying to create a new connection.");
    mBluetoothDeviceAddress = address;

    return true;
}

/**
 * Disconnects an existing connection or cancel a pending connection. The
 * disconnection result is reported asynchronously through the
 * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
 * callback.
 */
public void disconnect() {
    if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized");
        return;
    }
    mBluetoothGatt.disconnect();
}

/**
 * After using a given BLE device, the app must call this method to ensure
 * resources are released properly.
 */
public void close() {
    if (mBluetoothGatt == null) {
        return;
    }
    mBluetoothGatt.close();
    mBluetoothGatt = null;
}

/**
 * Request a read on a given {@code BluetoothGattCharacteristic}. The read
 * result is reported asynchronously through the
 * {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
 * callback.
 *
 * @param characteristic The characteristic to read from.
 */
public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
    if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized");
        return;
    }

    mBluetoothGatt.readCharacteristic(characteristic);
}

public void readRssi() {
    if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized");
        return;
    }

    mBluetoothGatt.readRemoteRssi();
}

public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
    if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized");
        return;
    }

    mBluetoothGatt.writeCharacteristic(characteristic);
}

/**
 * Enables or disables notification on a give characteristic.
 *
 * @param characteristic Characteristic to act on.
 * @param enabled        If true, enable notification. False otherwise.
 */
public void setCharacteristicNotification(
        BluetoothGattCharacteristic characteristic, boolean enabled) {
    if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized");
        return;
    }
    mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);

    if (UUID_BLE_SHIELD_RX.equals(characteristic.getUuid())) {
        BluetoothGattDescriptor descriptor = characteristic
                .getDescriptor(UUID
                        .fromString(RBLGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
        descriptor
                .setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        mBluetoothGatt.writeDescriptor(descriptor);
    }
}

/**
 * Retrieves a list of supported GATT services on the connected device. This
 * should be invoked only after {@code BluetoothGatt#discoverServices()}
 * completes successfully.
 *
 * @return A {@code List} of supported services.
 */
public BluetoothGattService getSupportedGattService() {
    if (mBluetoothGatt == null)
        return null;

    return mBluetoothGatt.getService(UUID_BLE_SHIELD_SERVICE);
}

}


updated question follows here:

With the (much appreciated) help of greeble31 and the following code I have managed to take the last known location just once as I wanted, when displayData is called. What I want to do now is to take the current location (I guess with requestLocationUpdate), but not to trigger an action every time the location changes. How can I update for a few seconds to get a correct GPS fix and after getting the latitude and longitude, stop the location update to save battery until the next call of displayData comes?

Here is the part of the code that is working now and that I need to change, but I can't understand what listener to put and I always get errors, so please help me:

private void displayData(final byte[] byteArray) {
    try {

        mFusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, new OnSuccessListener<Location>() {
                    @Override
                    public void onSuccess(Location location) {
                        // Got last known location. In some rare situations this can be null.

                        if (byteArray != null) {
                            String data = new String(byteArray);
                            tv.setText(n/2 + " measurements since startup...");
                            n += 1;

                            if (location != null) {
                                double lat = location.getLatitude();
                                double lng = location.getLongitude();
                                latitude = String.valueOf(lat);
                                longitude = String.valueOf(lng);
                            }

                            try
                            {
                                FileWriter fw = new FileWriter(textfile,true); //the true will append the new data
                                if (writeDate()) {
                                    fw.write("\n");
                                    fw.write(stringDate);
                                    fw.write(data); //appends the string to the file
                                }
                                else {
                                    fw.write(data); //appends the string to the file
                                    fw.write(" - ");
                                    fw.write(latitude);
                                    fw.write(",");
                                    fw.write(longitude);
                                }
                                fw.close();
                            }
                            catch(IOException ioe)
                            {
                                System.err.println("IOException: " + ioe.getMessage());
                            }


                            // find the amount we need to scroll. This works by
                            // asking the TextView's internal layout for the position
                            // of the final line and then subtracting the TextView's height
                            final int scrollAmount = tv.getLayout().getLineTop(
                                    tv.getLineCount())
                                    - tv.getHeight();
                            // if there is no need to scroll, scrollAmount will be <=0
                            if (scrollAmount > 0)
                                tv.scrollTo(0, scrollAmount);
                            else
                                tv.scrollTo(0, 0);
                        }
                    }
                });

    } catch (SecurityException e) {
        // lets the user know there is a problem with the gps
    }
}

Solution

  • So, a couple things, here:

    1. Simply requesting the ACCESS_FINE_LOCATION permission doesn't guarantee that the FusedLocationProvider is using GPS. Since you're only calling getLastLocation(), it's just going to give you the last location fix, however long ago that was. That action, by itself, doesn't turn on the GPS or anything. Even if it did, GPS can take several seconds to "warm up" before it gets a fix. I'm not sure how long it caches the last GPS fix, or why it doesn't give you at least a network fix when you have ACCESS_FINE_LOCATION. Have you tried it with ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION?
    2. Note that getLastLocation() returns a Task. That is a clue that the operation is asynchronous. In other words, onSuccess() actually executes after getLocation(), and indeed displayData(), have already returned, in practice. Why? Because getLastLocation() has to talk to the Google Play service (which hosts the FusedLocationProvider) via IPC to get the answer. It takes time, albeit only a few ms. So, instead of blocking until it has the answer, it gives you a Task and promises to call onSuccess() at some time in the future. This is why your lat/long show up at the end instead of the beginning.

    I suspect you will get better results if you keep the GPS running constantly -- like, whenever your activity is on-screen -- by using requestLocationUpdates() or some other API that turns on the GPS.

    Your Task problem can be resolved by moving most of your displayData() logic to onSuccess(). In other words, you wait until after you have the lat/long before you start appending the ByteArray data to the TextView.