Search code examples
androiddesign-patternsfusedlocationproviderapi

Location services across multiple activities


UPDATE - edited code to display my broadcast idea. Using the method causes a !!! FAILED BINDER TRANSACTION !!! and no location data to be displayed by toast.

I was wondering if there was an elegant way to separate google fused location services away from activities. My warning class currently implements location services but I have another activity that takes photos. When a photo is taken I want to associate a location with the image. My current idea is to just use my broadcast receiver in my Bluetooth warning class to listen for a broadcast my camera will send and associate it then with a location. I'm not a fan of the idea because it doesn't separate functionality very well so was hoping for some suggestions or patterns. Code for classes below.

CAMERA CLASS

import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class CameraOperations extends AppCompatActivity {
    public final static String ACTION_PICTURE_RECEIVED = "ACTION_PICTURE_RECEIVED";
    public final static String TRANSFER_DATA = "bitmap";
    ImageView mImageView;
    String photoPath;
    int REQUEST_IMAGE_CAPTURE = 1;
    File photoFile = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_view);
        mImageView = (ImageView) findViewById(R.id.imageView);

        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
            Toast.makeText(CameraOperations.this, "No Camera", Toast.LENGTH_SHORT).show();
        } else {
            dispatchTakePictureIntent();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK){
            if(data == null){
                Toast.makeText(this, "Data is null", Toast.LENGTH_SHORT);
                Bitmap pictureMap = BitmapFactory.decodeFile(photoFile.getAbsolutePath());
                mImageView.setImageBitmap(pictureMap);
                broadcastUpdate(ACTION_PICTURE_RECEIVED, pictureMap);
            }else {
                Toast.makeText(this, "Error Occurred", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void broadcastUpdate(final String action, Bitmap picture) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        picture.compress(Bitmap.CompressFormat.JPEG, 100, stream);
        byte[] bytes = stream.toByteArray();

        Intent intent = new Intent(action);
        intent.putExtra(TRANSFER_DATA, bytes);
        sendBroadcast(intent);

        intent = new Intent(CameraOperations.this, Warning.class);
        startActivity(intent);
        finish();
    }

    private File createImageFile() throws IOException {
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageFileName = "JPEG_" + timeStamp + "_";
        File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File image = File.createTempFile(imageFileName, ".jpg", storageDir);

        photoPath = "file:" + image.getAbsolutePath();
        return image;
    }

    private void dispatchTakePictureIntent() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        try{
            photoFile = createImageFile();
        }catch(IOException e){
            e.printStackTrace();
        }

        if(photoFile != null){
            Uri photoUri = Uri.fromFile(photoFile);
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
        }
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
    }
}

BLUETOOTH WARNING CLASS

import android.Manifest;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Location;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;

public class Warning extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
    private final static String TAG = Warning.class.getSimpleName();
    private String mDeviceAddress;
    private HandleConnectionService mBluetoothLeService;
    private boolean quitService;
    private final int PLAY_SERVICES_REQUEST_TIME = 1000;
    private Location mLastLocation;
    private GoogleApiClient mGoogleClient;
    private boolean requestingLocationUpdates = false;
    private LocationRequest locationRequest;

    private int UPDATE_INTERVAL = 10000; // 10 seconds
    private int FASTEST_INTERVAL = 5000; // 5 seconds
    private int DISTANCEMOVED = 10; //In meters


    private final ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder service) {
            mBluetoothLeService = ((HandleConnectionService.LocalBinder) service).getService();
            if (!mBluetoothLeService.initialize()) {
                Log.e(TAG, "Unable to initialize Bluetooth");
                finish();
            }
            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 (HandleConnectionService.ACTION_GATT_DISCONNECTED.equals(action)) {
                if (!quitService) {
                    mBluetoothLeService.connect(mDeviceAddress);
                    Log.w(TAG, "Attempting to reconnect");
                }
                Log.w(TAG, "Disconnected, activity closing");
            } else if (HandleConnectionService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
                getGattService(mBluetoothLeService.getSupportedGattService());
            } else if (HandleConnectionService.ACTION_DATA_AVAILABLE.equals(action)) {
                checkWarning(intent.getByteArrayExtra(HandleConnectionService.EXTRA_DATA));
            }else if(CameraOperations.ACTION_PICTURE_RECEIVED.equals(action)){
                byte[] bytes = intent.getByteArrayExtra("bitmap");
                Bitmap picture = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                findLocation();
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second);

        Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
        setSupportActionBar(myToolbar);

        if (checkGoogleServices()) {
            buildGoogleApiClient();
        }

        quitService = false;
        Intent intent = getIntent();
        mDeviceAddress = intent.getStringExtra(Device.EXTRA_DEVICE_ADDRESS);

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

    @Override
    protected void onResume() {
        super.onResume();
        registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.mainmenu, menu);
        MenuItem connect = menu.findItem(R.id.connect);
        connect.setVisible(false);
        MenuItem disconnect = menu.findItem(R.id.disconnect);
        disconnect.setVisible(true);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Intent intent;
        switch (item.getItemId()) {
            case R.id.home:
                new AlertDialog.Builder(this)
                        .setIcon(android.R.drawable.ic_dialog_alert)
                        .setTitle("Return Home")
                        .setMessage("Returning home will disconnect you, continue?")
                        .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                quitService = true;
                                mBluetoothLeService.disconnect();
                                mBluetoothLeService.close();

                                Intent intent = new Intent(Warning.this, Main.class);
                                startActivity(intent);
                            }
                        })
                        .setNegativeButton("No", null)
                        .show();

                return true;
            case R.id.connect:
                Toast.makeText(this, "Connect Pressed", Toast.LENGTH_SHORT).show();
                return true;

            case R.id.disconnect:
                disconnectOperation();
                intent = new Intent(Warning.this, Main.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
                return true;

            case R.id.profiles:
                Toast.makeText(this, "Profiles Pressed", Toast.LENGTH_SHORT).show();
                return true;


            case R.id.camera:
                Toast.makeText(this, "Camera Pressed", Toast.LENGTH_SHORT).show();
                intent = new Intent(Warning.this, CameraOperations.class);
                startActivity(intent);
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            quitService = true;
            mBluetoothLeService.disconnect();
            mBluetoothLeService.close();

            Intent intent = new Intent(Warning.this, Main.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent);
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBluetoothLeService.disconnect();
        mBluetoothLeService.close();

        System.exit(0);
    }

    public boolean disconnectOperation(){
        this.quitService = true;
        mBluetoothLeService.disconnect();
        mBluetoothLeService.close();
        return true;
    }

    private void checkWarning(byte[] byteArray) {
        if (byteArray != null) {
            for (int i = 0; i < byteArray.length; i++) {
                if (byteArray[i] == 48) {
                   findLocation();
                }
            }
        }
    }

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

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

    private static IntentFilter makeGattUpdateIntentFilter() {
        final IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(HandleConnectionService.ACTION_GATT_CONNECTED);
        intentFilter.addAction(HandleConnectionService.ACTION_GATT_DISCONNECTED);
        intentFilter.addAction(HandleConnectionService.ACTION_GATT_SERVICES_DISCOVERED);
        intentFilter.addAction(HandleConnectionService.ACTION_DATA_AVAILABLE);
        intentFilter.addAction(CameraOperations.ACTION_PICTURE_RECEIVED);

        return intentFilter;
    }

    private boolean checkGoogleServices() {
        GoogleApiAvailability googleAPI = GoogleApiAvailability.getInstance();
        int available = googleAPI.isGooglePlayServicesAvailable(this);
        if (available != ConnectionResult.SUCCESS) {
            if (googleAPI.isUserResolvableError(available)) {
                googleAPI.getErrorDialog(this, available, PLAY_SERVICES_REQUEST_TIME).show();
            }
            return false;
        }

        return true;
    }

    public void findLocation() {
        int REQUEST_CODE_ASK_PERMISSIONS = 123;
        double latitude = 0.0;
        double longitude = 0.0;

        if(Build.VERSION.SDK_INT >= 23){
            boolean fineLocationAccess = ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
            boolean courseLocationAccess = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
            boolean accessGranted = fineLocationAccess && courseLocationAccess;

            if (accessGranted) {
                mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleClient);
                if(mLastLocation != null) {
                    latitude = mLastLocation.getLatitude();
                    longitude = mLastLocation.getLongitude();

                    Toast.makeText(this, "latitude: " + latitude + " longitude: " + longitude, Toast.LENGTH_LONG).show();
                }else{
                    Log.i("Warning", "Unable to get Location");
                }
            }else{
                Log.i("Connection", "Request permission");
            }
        }else{
            mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleClient);
            if(mLastLocation != null) {
                latitude = mLastLocation.getLatitude();
                longitude = mLastLocation.getLongitude();

                Toast.makeText(this, "latitude: " + latitude + " longitude: " + longitude, Toast.LENGTH_LONG).show();
            }else{
                Log.i("Warning", "Unable to get Location");
            }
        }
    }

    public void buildGoogleApiClient() {
        mGoogleClient = new GoogleApiClient.Builder(this)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(LocationServices.API).build();
    }

    @Override
    public void onConnected(Bundle bundle) {
        Log.w("Connected", " : " + mGoogleClient.isConnected());
    }

    @Override
    public void onConnectionSuspended(int i) {
        Toast.makeText(this, "Location Services Stopped", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Toast.makeText(this, "Location Services Connection Fail", Toast.LENGTH_SHORT).show();
    }

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

        if (mGoogleClient != null) {
            mGoogleClient.connect();
        }
    }
}

Solution

  • I think your original idea is actually pretty sound as long as you construct it a certain way. The activity that is knowledgeable about the location information can update and store that location independent of any other activity. This would probably be ideal to have as a service actually that runs every x amount of time, or when the location changes.

    Inside of that same class you can have a generic broadcast receiver that handles request for that location information. You can make this as generic as you want in terms of allowing other applications/activities you make also being to leverage it, as long as the location data is returned in a way that is usable to each requester.

    From within your activity that takes photos (and eventually any activity you desire), when you take a photo, you can send a broadcast requesting that location information, with some default or null info if none is returned. One of the complexities is knowing how long to wait for this location info before completing. In this scenario the location activity is the 'generalized' one in that multiple activities can request location data if you construct it correctly.

    Another potentially robust approach is to make the photo taking activity the generalized one. One way to do this is that when you take a photo, you can have your photo activity send broadcast to the location activity that contains the photos filepath/uri/identifying information, but you don't wait for a response. Instead, you make the location activity listen for this broadcast, and write to the photos meta data with the location using the provided identifying info to find said photo. In this way your photo taking app isn't stuck waiting for the location activity to finish (or event start if it ever does).

    Good luck, sounds like a fun project.