1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.server.location;
18
19import android.content.Context;
20import android.location.Address;
21import android.location.Country;
22import android.location.Geocoder;
23import android.location.Location;
24import android.location.LocationListener;
25import android.location.LocationManager;
26import android.os.Binder;
27import android.os.Bundle;
28import android.util.Slog;
29
30import java.io.IOException;
31import java.util.ArrayList;
32import java.util.List;
33import java.util.Timer;
34import java.util.TimerTask;
35
36/**
37 * This class detects which country the user currently is in through the enabled
38 * location providers and the GeoCoder
39 * <p>
40 * Use {@link #detectCountry} to start querying. If the location can not be
41 * resolved within the given time, the last known location will be used to get
42 * the user country through the GeoCoder. The IllegalStateException will be
43 * thrown if there is a ongoing query.
44 * <p>
45 * The current query can be stopped by {@link #stop()}
46 *
47 * @hide
48 */
49public class LocationBasedCountryDetector extends CountryDetectorBase {
50    private final static String TAG = "LocationBasedCountryDetector";
51    private final static long QUERY_LOCATION_TIMEOUT = 1000 * 60 * 5; // 5 mins
52
53    /**
54     * Used for canceling location query
55     */
56    protected Timer mTimer;
57
58    /**
59     * The thread to query the country from the GeoCoder.
60     */
61    protected Thread mQueryThread;
62    protected List<LocationListener> mLocationListeners;
63
64    private LocationManager mLocationManager;
65    private List<String> mEnabledProviders;
66
67    public LocationBasedCountryDetector(Context ctx) {
68        super(ctx);
69        mLocationManager = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
70    }
71
72    /**
73     * @return the ISO 3166-1 two letters country code from the location
74     */
75    protected String getCountryFromLocation(Location location) {
76        String country = null;
77        Geocoder geoCoder = new Geocoder(mContext);
78        try {
79            List<Address> addresses = geoCoder.getFromLocation(
80                    location.getLatitude(), location.getLongitude(), 1);
81            if (addresses != null && addresses.size() > 0) {
82                country = addresses.get(0).getCountryCode();
83            }
84        } catch (IOException e) {
85            Slog.w(TAG, "Exception occurs when getting country from location");
86        }
87        return country;
88    }
89
90    protected boolean isAcceptableProvider(String provider) {
91        // We don't want to actively initiate a location fix here (with gps or network providers).
92        return LocationManager.PASSIVE_PROVIDER.equals(provider);
93    }
94
95    /**
96     * Register a listener with a provider name
97     */
98    protected void registerListener(String provider, LocationListener listener) {
99        final long bid = Binder.clearCallingIdentity();
100        try {
101            mLocationManager.requestLocationUpdates(provider, 0, 0, listener);
102        } finally {
103            Binder.restoreCallingIdentity(bid);
104        }
105    }
106
107    /**
108     * Unregister an already registered listener
109     */
110    protected void unregisterListener(LocationListener listener) {
111        final long bid = Binder.clearCallingIdentity();
112        try {
113            mLocationManager.removeUpdates(listener);
114        } finally {
115            Binder.restoreCallingIdentity(bid);
116        }
117    }
118
119    /**
120     * @return the last known location from all providers
121     */
122    protected Location getLastKnownLocation() {
123        final long bid = Binder.clearCallingIdentity();
124        try {
125            List<String> providers = mLocationManager.getAllProviders();
126            Location bestLocation = null;
127            for (String provider : providers) {
128                Location lastKnownLocation = mLocationManager.getLastKnownLocation(provider);
129                if (lastKnownLocation != null) {
130                    if (bestLocation == null ||
131                            bestLocation.getElapsedRealtimeNanos() <
132                            lastKnownLocation.getElapsedRealtimeNanos()) {
133                        bestLocation = lastKnownLocation;
134                    }
135                }
136            }
137            return bestLocation;
138        } finally {
139            Binder.restoreCallingIdentity(bid);
140        }
141    }
142
143    /**
144     * @return the timeout for querying the location.
145     */
146    protected long getQueryLocationTimeout() {
147        return QUERY_LOCATION_TIMEOUT;
148    }
149
150    protected List<String> getEnabledProviders() {
151        if (mEnabledProviders == null) {
152            mEnabledProviders = mLocationManager.getProviders(true);
153        }
154        return mEnabledProviders;
155    }
156
157    /**
158     * Start detecting the country.
159     * <p>
160     * Queries the location from all location providers, then starts a thread to query the
161     * country from GeoCoder.
162     */
163    @Override
164    public synchronized Country detectCountry() {
165        if (mLocationListeners  != null) {
166            throw new IllegalStateException();
167        }
168        // Request the location from all enabled providers.
169        List<String> enabledProviders = getEnabledProviders();
170        int totalProviders = enabledProviders.size();
171        if (totalProviders > 0) {
172            mLocationListeners = new ArrayList<LocationListener>(totalProviders);
173            for (int i = 0; i < totalProviders; i++) {
174                String provider = enabledProviders.get(i);
175                if (isAcceptableProvider(provider)) {
176                    LocationListener listener = new LocationListener () {
177                        @Override
178                        public void onLocationChanged(Location location) {
179                            if (location != null) {
180                                LocationBasedCountryDetector.this.stop();
181                                queryCountryCode(location);
182                            }
183                        }
184                        @Override
185                        public void onProviderDisabled(String provider) {
186                        }
187                        @Override
188                        public void onProviderEnabled(String provider) {
189                        }
190                        @Override
191                        public void onStatusChanged(String provider, int status, Bundle extras) {
192                        }
193                    };
194                    mLocationListeners.add(listener);
195                    registerListener(provider, listener);
196                }
197            }
198
199            mTimer = new Timer();
200            mTimer.schedule(new TimerTask() {
201                @Override
202                public void run() {
203                    mTimer = null;
204                    LocationBasedCountryDetector.this.stop();
205                    // Looks like no provider could provide the location, let's try the last
206                    // known location.
207                    queryCountryCode(getLastKnownLocation());
208                }
209            }, getQueryLocationTimeout());
210        } else {
211            // There is no provider enabled.
212            queryCountryCode(getLastKnownLocation());
213        }
214        return mDetectedCountry;
215    }
216
217    /**
218     * Stop the current query without notifying the listener.
219     */
220    @Override
221    public synchronized void stop() {
222        if (mLocationListeners != null) {
223            for (LocationListener listener : mLocationListeners) {
224                unregisterListener(listener);
225            }
226            mLocationListeners = null;
227        }
228        if (mTimer != null) {
229            mTimer.cancel();
230            mTimer = null;
231        }
232    }
233
234    /**
235     * Start a new thread to query the country from Geocoder.
236     */
237    private synchronized void queryCountryCode(final Location location) {
238        if (location == null) {
239            notifyListener(null);
240            return;
241        }
242        if (mQueryThread != null) return;
243        mQueryThread = new Thread(new Runnable() {
244            @Override
245            public void run() {
246                String countryIso = null;
247                if (location != null) {
248                    countryIso = getCountryFromLocation(location);
249                }
250                if (countryIso != null) {
251                    mDetectedCountry = new Country(countryIso, Country.COUNTRY_SOURCE_LOCATION);
252                } else {
253                    mDetectedCountry = null;
254                }
255                notifyListener(mDetectedCountry);
256                mQueryThread = null;
257            }
258        });
259        mQueryThread.start();
260    }
261}
262