1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser;
6
7import android.content.Context;
8import android.location.Criteria;
9import android.location.Location;
10import android.location.LocationListener;
11import android.location.LocationManager;
12import android.os.Bundle;
13import android.os.Looper;
14import android.util.Log;
15
16import org.chromium.base.ActivityStatus;
17import org.chromium.base.CalledByNative;
18import org.chromium.base.ThreadUtils;
19
20import java.util.List;
21import java.util.concurrent.FutureTask;
22
23/**
24 * Implements the Java side of LocationProviderAndroid.
25 * Delegates all real functionality to the inner class.
26 * See detailed documentation on
27 * content/browser/geolocation/android_location_api_adapter.h.
28 * Based on android.webkit.GeolocationService.java
29 */
30class LocationProvider {
31
32    // Log tag
33    private static final String TAG = "LocationProvider";
34
35    /**
36     * This is the core of android location provider. It is a separate class for clarity
37     * so that it can manage all processing completely in the UI thread. The container class
38     * ensures that the start/stop calls into this class are done in the UI thread.
39     */
40    private static class LocationProviderImpl
41            implements LocationListener, ActivityStatus.StateListener {
42
43        private Context mContext;
44        private LocationManager mLocationManager;
45        private boolean mIsRunning;
46        private boolean mShouldRunAfterActivityResume;
47        private boolean mIsGpsEnabled;
48
49        LocationProviderImpl(Context context) {
50            mContext = context;
51        }
52
53        @Override
54        public void onActivityStateChange(int state) {
55            if (state == ActivityStatus.PAUSED) {
56                mShouldRunAfterActivityResume = mIsRunning;
57                unregisterFromLocationUpdates();
58            } else if (state == ActivityStatus.RESUMED) {
59                assert !mIsRunning;
60                if (mShouldRunAfterActivityResume) {
61                    registerForLocationUpdates();
62                }
63            }
64        }
65
66        /**
67         * Start listening for location updates.
68         * @param gpsEnabled Whether or not we're interested in high accuracy GPS.
69         */
70        private void start(boolean gpsEnabled) {
71            if (!mIsRunning && !mShouldRunAfterActivityResume) {
72                // Currently idle so start listening to activity status changes.
73                ActivityStatus.registerStateListener(this);
74            }
75            mIsGpsEnabled = gpsEnabled;
76            if (ActivityStatus.isPaused()) {
77                mShouldRunAfterActivityResume = true;
78            } else {
79                unregisterFromLocationUpdates();
80                registerForLocationUpdates();
81            }
82        }
83
84        /**
85         * Stop listening for location updates.
86         */
87        private void stop() {
88            unregisterFromLocationUpdates();
89            ActivityStatus.unregisterStateListener(this);
90            mShouldRunAfterActivityResume = false;
91        }
92
93        /**
94         * Returns true if we are currently listening for location updates, false if not.
95         */
96        private boolean isRunning() {
97            return mIsRunning;
98        }
99
100        @Override
101        public void onLocationChanged(Location location) {
102            // Callbacks from the system location sevice are queued to this thread, so it's
103            // possible that we receive callbacks after unregistering. At this point, the
104            // native object will no longer exist.
105            if (mIsRunning) {
106                updateNewLocation(location);
107            }
108        }
109
110        private void updateNewLocation(Location location) {
111            nativeNewLocationAvailable(location.getLatitude(), location.getLongitude(),
112                    location.getTime() / 1000.0,
113                    location.hasAltitude(), location.getAltitude(),
114                    location.hasAccuracy(), location.getAccuracy(),
115                    location.hasBearing(), location.getBearing(),
116                    location.hasSpeed(), location.getSpeed());
117        }
118
119        @Override
120        public void onStatusChanged(String provider, int status, Bundle extras) {
121        }
122
123        @Override
124        public void onProviderEnabled(String provider) {
125        }
126
127        @Override
128        public void onProviderDisabled(String provider) {
129        }
130
131        private void ensureLocationManagerCreated() {
132            if (mLocationManager != null) return;
133            mLocationManager = (LocationManager) mContext.getSystemService(
134                    Context.LOCATION_SERVICE);
135            if (mLocationManager == null) {
136                Log.e(TAG, "Could not get location manager.");
137            }
138        }
139
140        /**
141         * Registers this object with the location service.
142         */
143        private void registerForLocationUpdates() {
144            ensureLocationManagerCreated();
145            if (usePassiveOneShotLocation()) return;
146
147            assert !mIsRunning;
148            mIsRunning = true;
149
150            // We're running on the main thread. The C++ side is responsible to
151            // bounce notifications to the Geolocation thread as they arrive in the mainLooper.
152            try {
153                Criteria criteria = new Criteria();
154                mLocationManager.requestLocationUpdates(0, 0, criteria, this,
155                        ThreadUtils.getUiThreadLooper());
156                if (mIsGpsEnabled) {
157                    criteria.setAccuracy(Criteria.ACCURACY_FINE);
158                    mLocationManager.requestLocationUpdates(0, 0, criteria, this,
159                            ThreadUtils.getUiThreadLooper());
160                }
161            } catch(SecurityException e) {
162                Log.e(TAG, "Caught security exception registering for location updates from " +
163                    "system. This should only happen in DumpRenderTree.");
164            } catch(IllegalArgumentException e) {
165                Log.e(TAG, "Caught IllegalArgumentException registering for location updates.");
166            }
167        }
168
169        /**
170         * Unregisters this object from the location service.
171         */
172        private void unregisterFromLocationUpdates() {
173            if (mIsRunning) {
174                mIsRunning = false;
175                mLocationManager.removeUpdates(this);
176            }
177        }
178
179        private boolean usePassiveOneShotLocation() {
180            if (!isOnlyPassiveLocationProviderEnabled()) return false;
181
182            // Do not request a location update if the only available location provider is
183            // the passive one. Make use of the last known location and call
184            // onLocationChanged directly.
185            final Location location = mLocationManager.getLastKnownLocation(
186                    LocationManager.PASSIVE_PROVIDER);
187            if (location != null) {
188                ThreadUtils.runOnUiThread(new Runnable() {
189                    @Override
190                    public void run() {
191                        updateNewLocation(location);
192                    }
193                });
194            }
195            return true;
196        }
197
198        /*
199         * Checks if the passive location provider is the only provider available
200         * in the system.
201         */
202        private boolean isOnlyPassiveLocationProviderEnabled() {
203            List<String> providers = mLocationManager.getProviders(true);
204            return providers != null && providers.size() == 1
205                    && providers.get(0).equals(LocationManager.PASSIVE_PROVIDER);
206        }
207    }
208
209    // Delegate handling the real work in the main thread.
210    private LocationProviderImpl mImpl;
211
212    private LocationProvider(Context context) {
213        mImpl = new LocationProviderImpl(context);
214    }
215
216    @CalledByNative
217    static LocationProvider create(Context context) {
218        return new LocationProvider(context);
219    }
220
221    /**
222     * Start listening for location updates until we're told to quit. May be
223     * called in any thread.
224     * @param gpsEnabled Whether or not we're interested in high accuracy GPS.
225     */
226    @CalledByNative
227    public boolean start(final boolean gpsEnabled) {
228        FutureTask<Void> task = new FutureTask<Void>(new Runnable() {
229            @Override
230            public void run() {
231                mImpl.start(gpsEnabled);
232            }
233        }, null);
234        ThreadUtils.runOnUiThread(task);
235        return true;
236    }
237
238    /**
239     * Stop listening for location updates. May be called in any thread.
240     */
241    @CalledByNative
242    public void stop() {
243        FutureTask<Void> task = new FutureTask<Void>(new Runnable() {
244            @Override
245            public void run() {
246                mImpl.stop();
247            }
248        }, null);
249        ThreadUtils.runOnUiThread(task);
250    }
251
252    /**
253     * Returns true if we are currently listening for location updates, false if not.
254     * Must be called only in the UI thread.
255     */
256    public boolean isRunning() {
257        assert ThreadUtils.runningOnUiThread();
258        return mImpl.isRunning();
259    }
260
261    // Native functions
262    public static native void nativeNewLocationAvailable(
263            double latitude, double longitude, double timeStamp,
264            boolean hasAltitude, double altitude,
265            boolean hasAccuracy, double accuracy,
266            boolean hasHeading, double heading,
267            boolean hasSpeed, double speed);
268    public static native void nativeNewErrorAvailable(String message);
269}
270