Get GPS location via a service in Android
Asked Answered
S

7

75

I need to monitor user's locations using a background service, and then load them and show the path to the user.

Using an activity, it was quite easy to get GPS locations, but when I got to do it via a service, I came into a problem as it seems to only work for looper threads (or something like that).

As I've searched the internet for a solution, I've found out that many people got the same problem, but I couldn't find a working solution. Some people say that you need to use prepareloopquit, and some say you have to use a handlerThread, but still, I can't find out how to do such things in a proper way.

Sessile answered 12/1, 2012 at 0:24 Comment(1)
loopy threads :D You mean looper threads. A Looper thread is a special thread with a request queue mechanism. The main or UI thread is a Looper thread. As for the problem with a Service, are you invoking requestLocationUpdates on the main thread? Or are you doing it in a different thread? Because activities and services are run via the main thread which is a looper thread, so invoking requestLocationUpdates must be the same. Can you point out to the other posts mentioning this issue that you are talking about?Archipenko
T
60

I don't understand what exactly is the problem with implementing location listening functionality in the Service. It looks pretty similar to what you do in Activity. Just define a location listener and register for location updates. You can refer to the following code as example:

Manifest file:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name" >
    <activity android:label="@string/app_name" android:name=".LocationCheckerActivity" >
        <intent-filter >
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <service android:name=".MyService" android:process=":my_service" />
</application>

The service file:

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

public class MyService extends Service {

    private static final String TAG = "BOOMBOOMTESTGPS";
    private LocationManager mLocationManager = null;
    private static final int LOCATION_INTERVAL = 1000;
    private static final float LOCATION_DISTANCE = 10f;

    private class LocationListener implements android.location.LocationListener {
        Location mLastLocation;

        public LocationListener(String provider) {
            Log.e(TAG, "LocationListener " + provider);
            mLastLocation = new Location(provider);
        }

        @Override
        public void onLocationChanged(Location location) {
            Log.e(TAG, "onLocationChanged: " + location);
            mLastLocation.set(location);
        }

        @Override
        public void onProviderDisabled(String provider) {
            Log.e(TAG, "onProviderDisabled: " + provider);
        }

        @Override
        public void onProviderEnabled(String provider) {
            Log.e(TAG, "onProviderEnabled: " + provider);
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            Log.e(TAG, "onStatusChanged: " + provider);
        }
    }

    LocationListener[] mLocationListeners = new LocationListener[]{
            new LocationListener(LocationManager.GPS_PROVIDER),
            new LocationListener(LocationManager.NETWORK_PROVIDER)
    };

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand");
        super.onStartCommand(intent, flags, startId);
        return START_STICKY;
    }

    @Override
    public void onCreate() {
        Log.e(TAG, "onCreate");
        initializeLocationManager();
        try {
            mLocationManager.requestLocationUpdates(
                    LocationManager.NETWORK_PROVIDER, LOCATION_INTERVAL, LOCATION_DISTANCE,
                    mLocationListeners[1]);
        } catch (java.lang.SecurityException ex) {
            Log.i(TAG, "fail to request location update, ignore", ex);
        } catch (IllegalArgumentException ex) {
            Log.d(TAG, "network provider does not exist, " + ex.getMessage());
        }
        try {
            mLocationManager.requestLocationUpdates(
                    LocationManager.GPS_PROVIDER, LOCATION_INTERVAL, LOCATION_DISTANCE,
                    mLocationListeners[0]);
        } catch (java.lang.SecurityException ex) {
            Log.i(TAG, "fail to request location update, ignore", ex);
        } catch (IllegalArgumentException ex) {
            Log.d(TAG, "gps provider does not exist " + ex.getMessage());
        }
    }

    @Override
    public void onDestroy() {
        Log.e(TAG, "onDestroy");
        super.onDestroy();
        if (mLocationManager != null) {
            for (int i = 0; i < mLocationListeners.length; i++) {
                try {
                    mLocationManager.removeUpdates(mLocationListeners[i]);
                } catch (Exception ex) {
                    Log.i(TAG, "fail to remove location listners, ignore", ex);
                }
            }
        }
    }

    private void initializeLocationManager() {
        Log.e(TAG, "initializeLocationManager");
        if (mLocationManager == null) {
            mLocationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
        }
    }
}
Thoracotomy answered 12/1, 2012 at 4:28 Comment(9)
i know , it worked fine on a side project , but i work with some people and for some reason , i always get an exception: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() i've looked at the code and i can't see that it works on a special thread , and just like on your code , the service runs on a process with a specified name.Sessile
perhaps it's because the service has its own process , and when you later open the application , it uses a different thread , which is not the original one?Sessile
btw, is it a must to call the "removeUpdates" for the "onDestroy" method? i think removing it won't result in memory leak , no?Sessile
afaik, if you don't remove the listener from the locationManager, the reference to your listener object will remain used by LocationManager, hence GC will not be able to free them(your listeners).Thoracotomy
but if the service is killed/destroyed , doesn't it mean that android can't reference to anything that it has in it?Sessile
how do you get data like latitude and longitude from this GPS service?Aldric
@androiddeveloper declare 'Handler handler = new Handler(Looper.getMainLooper());' to get rid of 'java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()'Gilda
@Hissain correct. I wrote it ages ago and I actually have no idea what I was thinking here. However, you should avoid creating an anonymous handler in the service/activity, since it might cause memory leaks(unless you are careful, of course). That's also why Lint will warn you about it.Sessile
Logging out exception with i and d, the rest with e? Everything works but this is just so ugly.Foresail
D
22
public class GPSService extends Service implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, com.google.android.gms.location.LocationListener {
    private LocationRequest mLocationRequest;
    private GoogleApiClient mGoogleApiClient;
    private static final String LOGSERVICE = "#######";

    @Override
    public void onCreate() {
        super.onCreate();
        buildGoogleApiClient();
        Log.i(LOGSERVICE, "onCreate");

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(LOGSERVICE, "onStartCommand");

        if (!mGoogleApiClient.isConnected())
            mGoogleApiClient.connect();
        return START_STICKY;
    }


    @Override
    public void onConnected(Bundle bundle) {
        Log.i(LOGSERVICE, "onConnected" + bundle);

        Location l = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
        if (l != null) {
            Log.i(LOGSERVICE, "lat " + l.getLatitude());
            Log.i(LOGSERVICE, "lng " + l.getLongitude());

        }

        startLocationUpdate();
    }

    @Override
    public void onConnectionSuspended(int i) {
        Log.i(LOGSERVICE, "onConnectionSuspended " + i);

    }

    @Override
    public void onLocationChanged(Location location) {
        Log.i(LOGSERVICE, "lat " + location.getLatitude());
        Log.i(LOGSERVICE, "lng " + location.getLongitude());
        LatLng mLocation = (new LatLng(location.getLatitude(), location.getLongitude()));
        EventBus.getDefault().post(mLocation);

    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(LOGSERVICE, "onDestroy - Estou sendo destruido ");

    }

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

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Log.i(LOGSERVICE, "onConnectionFailed ");

    }

    private void initLocationRequest() {
        mLocationRequest = new LocationRequest();
        mLocationRequest.setInterval(5000);
        mLocationRequest.setFastestInterval(2000);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    }

    private void startLocationUpdate() {
        initLocationRequest();

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            return;
        }
        LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this);
    }

    private void stopLocationUpdate() {
        LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);

    }

    protected synchronized void buildGoogleApiClient() {
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addOnConnectionFailedListener(this)
                .addConnectionCallbacks(this)
                .addApi(LocationServices.API)
                .build();
    }

}
Dermato answered 24/5, 2016 at 17:40 Comment(5)
I'm using the lib EventBus to send the location to view. --> EventBus.getDefault().post(mLocation)Dermato
It's not running in the background. I am using Red MI note 3. Its working fine when the app is on but not working as a service when i kill the app.Cloris
how to deal with permissions and location settings here ??Glisson
@DarpanPathak it is xiaomi related issue. You must add permissions in phone settings for appSchnorr
Why does onLocationChanged() send LatLng instead of directly send Location object?Kislev
S
14

All these answers doesn't work from Android 6.0 (Marshmallow) - to - Android 8.1 (Oreo) - 8, Due to Dozer mode that restrict the service - whatever service or any background operation that requires discrete things in background would be no longer able to run.

So the approach would be listening to the system FusedLocationApiClient through BroadCastReciever that always listening the location and work even in Doze mode.

Posting the link would be pointless, please search FusedLocation with Broadcast receiver.

Seventy answered 29/1, 2019 at 11:18 Comment(4)
Asking for a Location in the background using Services or IntentServices will Result in a new updates per hour. This is true for Android 8+Temporary
Foreground services don't have the same issue as background processes. I use a foreground service and it handles gps updates.Minatory
@LukeDupin does it update periodically even with the screen locked?Deem
@AlfredoBazoLopez Yes.Minatory
L
2

Just complementing, I implemented this way and usually worked in my Service class

In my Service

@Override
public void onCreate()
{
    mHandler = new Handler(Looper.getMainLooper());
    mHandler.post(this);
    super.onCreate();
}

@Override
public void onDestroy() 
{
    mHandler.removeCallbacks(this);     
    super.onDestroy();
}

@Override
public void run()
{
    InciarGPSTracker();
}
Lulululuabourg answered 20/11, 2014 at 18:33 Comment(0)
O
2

Take a look at Google Play Location Samples GitHub repository

Location Updates using a Foreground Service: Get updates about a device's location using a bound and started foreground service.

Location Updates in the Background (Kotlin): Location Updates using a PendingIntent.

Oratorian answered 5/12, 2019 at 12:34 Comment(3)
The links are swapped.Brotherton
Links are dead.Whopping
The last two links: "Page not found · GitHub"Klug
W
1

2023 solution using Hilt

Here comes an excelent implementation using Hilt and solid principles. If someone has the time, please edit this and explain to make it better. This was based on https://github.com/philipplackner/BackgroundLocationTracking.

MainActivity:

ActivityCompat.requestPermissions(
    this,
    arrayOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION,
    ),
    0
)

//To start service
Intent(applicationContext, LocationService::class.java).apply {
    action = LocationService.ACTION_START
    startService(this)
}

//To stop service
Intent(applicationContext, LocationService::class.java).apply {
    action = LocationService.ACTION_STOP
    startService(this)
}

Service:

import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import online.slis.core.data.repository.LocationApi
import javax.inject.Inject

@AndroidEntryPoint
class LocationService : Service() {
    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    @Inject
    lateinit var locationClient: LocationApi

    override fun onBind(p0: Intent?): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_START -> start()
            ACTION_STOP -> stop()
        }
        return super.onStartCommand(intent, flags, startId)
    }

    private fun start() {
        locationClient
            .getLocationUpdates(5000)//location updates from 5 to 5
            .catch { e -> e.printStackTrace() }
            .onEach { location ->
                val lat = location.latitude.toString()
                val long = location.longitude.toString()

                Log.d("locationupdates", "lat: $lat, long: $long")
            }
            .launchIn(serviceScope)
    }

    private fun stop() {
        stopSelf()
    }

    override fun onDestroy() {
        serviceScope.cancel()
    }

    companion object {
        const val ACTION_START = "ACTION_START"
        const val ACTION_STOP = "ACTION_STOP"
    }
}

Modules:

@Module
@InstallIn(SingletonComponent::class)
object DataProviderModule{
    @Provides
    @Singleton
    fun provideFusedLocationProviderClient(
        @ApplicationContext context: Context
    ): FusedLocationProviderClient {
        return LocationServices.getFusedLocationProviderClient(context)
    }
}

@Module
@InstallIn(SingletonComponent::class)
interface DataModule {
    @Binds
    @Singleton
    fun bindLocationRepository(
        locationRepository: LocationRepository
    ): LocationApi
}

LocationApi:

interface LocationApi {
    fun getLocationUpdates(interval: Long): Flow<Location>
    class LocationException(message: String): Exception()
}

LocationRepository:

class LocationRepository @Inject constructor(
    @ApplicationContext private val context: Context,
    private val client: FusedLocationProviderClient
) : LocationApi {

    @SuppressLint("MissingPermission")
    override fun getLocationUpdates(interval: Long): Flow<Location> {
        return callbackFlow {
            if (!context.hasLocationPermission()) {
                throw LocationApi.LocationException("Missing location permission")
            }

            val locationManager =
                context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
            val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
            val isNetworkEnabled =
                locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)

            if (!isGpsEnabled && !isNetworkEnabled) {
                throw LocationApi.LocationException("GPS is disabled")
            }

            val request = LocationRequest.Builder(interval)
                .setMinUpdateIntervalMillis(interval)
                .build()

            val locationCallback = object : LocationCallback() {
                override fun onLocationResult(result: LocationResult) {
                    super.onLocationResult(result)
                    result.locations.lastOrNull()?.let { location ->
                        launch { send(location) }
                    }
                }
            }

            client.requestLocationUpdates(
                request,
                locationCallback,
                Looper.getMainLooper()
            )

            awaitClose {
                client.removeLocationUpdates(locationCallback)
            }
        }
    }
}

fun Context.hasLocationPermission(): Boolean {
    return ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.ACCESS_COARSE_LOCATION
    ) == PackageManager.PERMISSION_GRANTED &&
            ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED
}
Winshell answered 17/2, 2023 at 10:49 Comment(0)
G
0

Here is my solution:

Step1: Register Serice in manifest

<receiver
    android:name=".MySMSBroadcastReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="com.google.android.gms.auth.api.phone.SMS_RETRIEVED" />
    </intent-filter>
</receiver>

Step2: Code Of Service

public class FusedLocationService extends Service {

    private String mLastUpdateTime = null;

    // bunch of location related apis
    private FusedLocationProviderClient mFusedLocationClient;
    private SettingsClient mSettingsClient;
    private LocationRequest mLocationRequest;
    private LocationSettingsRequest mLocationSettingsRequest;
    private LocationCallback mLocationCallback;
    private Location lastLocation;

    // location updates interval - 10sec
    private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 5000;

    // fastest updates interval - 5 sec
    // location updates will be received if another app is requesting the locations
    // than your app can handle
    private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = 500;
    private DatabaseReference locationRef;
    private int notificationBuilder = 0;
    private boolean isInitRef;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.log("LOCATION GET DURATION", "start in service");
        init();
        return START_STICKY;
    }

    /**
     * Initilize Location Apis
     * Create Builder if Share location true
     */
    private void init() {
        mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
        mSettingsClient = LocationServices.getSettingsClient(this);
        mLocationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(LocationResult locationResult) {
                super.onLocationResult(locationResult);
                receiveLocation(locationResult);
            }
        };

        mLocationRequest = new LocationRequest();
        mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
        mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

        LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
        builder.addLocationRequest(mLocationRequest);
        mLocationSettingsRequest = builder.build();
        startLocationUpdates();
    }

    /**
     * Request Location Update
     */
    @SuppressLint("MissingPermission")
    private void startLocationUpdates() {
        mSettingsClient
                .checkLocationSettings(mLocationSettingsRequest)
                .addOnSuccessListener(locationSettingsResponse -> {
                    Log.log(TAG, "All location settings are satisfied. No MissingPermission");

                    //noinspection MissingPermission
                    mFusedLocationClient.requestLocationUpdates(mLocationRequest, mLocationCallback, Looper.myLooper());
                })
                .addOnFailureListener(e -> {
                    int statusCode = ((ApiException) e).getStatusCode();
                    switch (statusCode) {
                        case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                            Log.loge("Location settings are not satisfied. Attempting to upgrade " + "location settings ");
                            break;
                        case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                            Log.loge("Location settings are inadequate, and cannot be " + "fixed here. Fix in Settings.");
                    }
                });
    }

    /**
     * onLocationResult
     * on Receive Location  share to other activity and save if save true
     *
     * @param locationResult
     */
    private void receiveLocation(LocationResult locationResult) {
        lastLocation = locationResult.getLastLocation();

        LocationInstance.getInstance().changeState(lastLocation);

        saveLocation();
    }

    private void saveLocation() {
        String saveLocation = getsaveLocationStatus(this);

        if (saveLocation.equalsIgnoreCase("true") && notificationBuilder == 0) {
            notificationBuilder();
            notificationBuilder = 1;
        } else if (saveLocation.equalsIgnoreCase("false") && notificationBuilder == 1) {
            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).cancel(1);
            notificationBuilder = 0;
        }

        Log.logd("receiveLocation : Share :- " + saveLocation + ", [Lat " + lastLocation.getLatitude() + ", Lng" + lastLocation.getLongitude() + "], Time :- " + mLastUpdateTime);

        if (saveLocation.equalsIgnoreCase("true") || getPreviousMin() < getCurrentMin()) {
            setLatLng(this, lastLocation);

            mLastUpdateTime = DateFormat.getTimeInstance().format(new Date());

            if (isOnline(this) && !getUserId(this).equalsIgnoreCase("")) {
                if (!isInitRef) {
                    locationRef = getFirebaseInstance().child(getUserId(this)).child("location");
                    isInitRef = true;
                }
                if (isInitRef) {
                    locationRef.setValue(new LocationModel(lastLocation.getLatitude(), lastLocation.getLongitude(), mLastUpdateTime));
                }
            }
        }
    }

    private int getPreviousMin() {
        int previous_min = 0;
        if (mLastUpdateTime != null) {
            String[] pretime = mLastUpdateTime.split(":");
            previous_min = Integer.parseInt(pretime[1].trim()) + 1;

            if (previous_min > 59) {
                previous_min = 0;
            }
        }
        return previous_min;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopLocationUpdates();
    }

    /**
     * Remove Location Update
     */
    public void stopLocationUpdates() {
        mFusedLocationClient
                .removeLocationUpdates(mLocationCallback)
                .addOnCompleteListener(task -> Log.logd("stopLocationUpdates : "));
    }

    private void notificationBuilder() {
        if (Build.VERSION.SDK_INT >= 26) {
            String CHANNEL_ID = "my_channel_01";
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel human readable title",
                    NotificationManager.IMPORTANCE_DEFAULT);

            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);

            Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                    .setContentTitle("")
                    .setContentText("").build();

            startForeground(1, notification);
        }
    }

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

Step 3: Manifests

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

Step 4: Gradle

implementation 'com.google.android.gms:play-services-location:16.0.0'
Grivet answered 25/6, 2019 at 10:13 Comment(2)
and how to use this??Callery
Follow the steps as mention here.Grivet

© 2022 - 2024 — McMap. All rights reserved.