aboutsummaryrefslogtreecommitdiffstats
path: root/main/src/cgeo/geocaching/playservices/LocationProvider.java
blob: 027ae2996f7bb421da09aa69cbcb386bd5cd9a30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package cgeo.geocaching.playservices;

import cgeo.geocaching.sensors.GeoData;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.RxUtils;

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

import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Subscriber;
import rx.functions.Action0;
import rx.functions.Func1;
import rx.observers.Subscribers;
import rx.subjects.ReplaySubject;
import rx.subscriptions.Subscriptions;

import android.content.Context;
import android.location.Location;
import android.os.Bundle;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class LocationProvider implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener {

    private static final LocationRequest LOCATION_REQUEST =
            LocationRequest.create().setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY).setInterval(2000).setFastestInterval(250);
    private static final LocationRequest LOCATION_REQUEST_LOW_POWER =
            LocationRequest.create().setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY).setInterval(10000).setFastestInterval(5000);
    private static final AtomicInteger mostPreciseCount = new AtomicInteger(0);
    private static final AtomicInteger lowPowerCount = new AtomicInteger(0);
    private static LocationProvider instance = null;
    private static final ReplaySubject<GeoData> subject = ReplaySubject.createWithSize(1);
    private final GoogleApiClient locationClient;

    private static synchronized LocationProvider getInstance(final Context context) {
        if (instance == null) {
            instance = new LocationProvider(context);
        }
        return instance;
    }

    private synchronized void updateRequest() {
        if (locationClient.isConnected()) {
            if (mostPreciseCount.get() > 0) {
                Log.d("LocationProvider: requesting most precise locations");
                LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, LOCATION_REQUEST, this, RxUtils.looperCallbacksLooper);
            } else if (lowPowerCount.get() > 0) {
                Log.d("LocationProvider: requesting low-power locations");
                LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, LOCATION_REQUEST_LOW_POWER, this, RxUtils.looperCallbacksLooper);
            } else {
                Log.d("LocationProvider: stopping location requests");
                LocationServices.FusedLocationApi.removeLocationUpdates(locationClient, this);
            }
        }
    }

    private static Observable<GeoData> get(final Context context, final AtomicInteger reference) {
        final LocationProvider instance = getInstance(context);
        return Observable.create(new OnSubscribe<GeoData>() {
            @Override
            public void call(final Subscriber<? super GeoData> subscriber) {
                if (reference.incrementAndGet() == 1) {
                    instance.updateRequest();
                }
                subscriber.add(Subscriptions.create(new Action0() {
                    @Override
                    public void call() {
                        RxUtils.looperCallbacksWorker.schedule(new Action0() {
                            @Override
                            public void call() {
                                if (reference.decrementAndGet() == 0) {
                                    instance.updateRequest();
                                }
                            }
                        }, 2500, TimeUnit.MILLISECONDS);
                    }
                }));
                subscriber.add(subject.subscribe(Subscribers.from(subscriber)));
            }
        });
    }

    public static Observable<GeoData> getMostPrecise(final Context context) {
        return get(context, mostPreciseCount).onBackpressureDrop();
    }

    public static Observable<GeoData> getLowPower(final Context context) {
        // Low-power location without the last stored location
        final Observable<GeoData> lowPowerObservable = get(context, lowPowerCount).skip(1);

        // High-power location without the last stored location
        final Observable<GeoData> highPowerObservable = get(context, mostPreciseCount).skip(1);

        // Use either low-power (with a 6 seconds head start) or high-power observables to obtain a location
        // no less precise than 20 meters.
        final Observable<GeoData> untilPreciseEnoughObservable =
                lowPowerObservable.mergeWith(highPowerObservable.delaySubscription(6, TimeUnit.SECONDS))
                        .takeUntil(new Func1<GeoData, Boolean>() {
                            @Override
                            public Boolean call(final GeoData geoData) {
                                return geoData.getAccuracy() <= 20;
                            }
                        });

        // After sending the last known location, try to get a precise location then use the low-power mode. If no
        // location information is given for 25 seconds (if the network location is turned off for example), get
        // back to the precise location and try again.
        return subject.first().concatWith(untilPreciseEnoughObservable.concatWith(lowPowerObservable).timeout(25, TimeUnit.SECONDS).retry()).onBackpressureDrop();
    }

    /**
     * Build a new geo data provider object.
     * <p/>
     * There is no need to instantiate more than one such object in an application, as observers can be added
     * at will.
     *
     * @param context the context used to retrieve the system services
     */
    private LocationProvider(final Context context) {
        final GeoData initialLocation = GeoData.getInitialLocation(context);
        subject.onNext(initialLocation != null ? initialLocation : GeoData.DUMMY_LOCATION);
        locationClient = new GoogleApiClient.Builder(context)
                .addApi(LocationServices.API)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();
        locationClient.connect();
    }

    @Override
    public void onConnected(final Bundle bundle) {
        updateRequest();
    }

    @Override
    public void onConnectionFailed(final ConnectionResult connectionResult) {
        Log.e("cannot connect to Google Play location service: " + connectionResult);
        subject.onError(new RuntimeException("Connection failed: " + connectionResult));
    }

    @Override
    public void onLocationChanged(final Location location) {
        if (Settings.useLowPowerMode()) {
            location.setProvider(GeoData.LOW_POWER_PROVIDER);
        }
        subject.onNext(new GeoData(location));
    }

    @Override
    public void onConnectionSuspended(final int arg0) {
        // empty
    }
}