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.gallery3d.util;
18
19import android.content.Context;
20import android.location.Address;
21import android.location.Geocoder;
22import android.location.Location;
23import android.location.LocationManager;
24import android.net.ConnectivityManager;
25import android.net.NetworkInfo;
26
27import com.android.gallery3d.common.BlobCache;
28
29import java.io.ByteArrayInputStream;
30import java.io.ByteArrayOutputStream;
31import java.io.DataInputStream;
32import java.io.DataOutputStream;
33import java.io.IOException;
34import java.util.List;
35import java.util.Locale;
36
37public class ReverseGeocoder {
38    @SuppressWarnings("unused")
39    private static final String TAG = "ReverseGeocoder";
40    public static final int EARTH_RADIUS_METERS = 6378137;
41    public static final int LAT_MIN = -90;
42    public static final int LAT_MAX = 90;
43    public static final int LON_MIN = -180;
44    public static final int LON_MAX = 180;
45    private static final int MAX_COUNTRY_NAME_LENGTH = 8;
46    // If two points are within 20 miles of each other, use
47    // "Around Palo Alto, CA" or "Around Mountain View, CA".
48    // instead of directly jumping to the next level and saying
49    // "California, US".
50    private static final int MAX_LOCALITY_MILE_RANGE = 20;
51
52    private static final String GEO_CACHE_FILE = "rev_geocoding";
53    private static final int GEO_CACHE_MAX_ENTRIES = 1000;
54    private static final int GEO_CACHE_MAX_BYTES = 500 * 1024;
55    private static final int GEO_CACHE_VERSION = 0;
56
57    public static class SetLatLong {
58        // The latitude and longitude of the min latitude point.
59        public double mMinLatLatitude = LAT_MAX;
60        public double mMinLatLongitude;
61        // The latitude and longitude of the max latitude point.
62        public double mMaxLatLatitude = LAT_MIN;
63        public double mMaxLatLongitude;
64        // The latitude and longitude of the min longitude point.
65        public double mMinLonLatitude;
66        public double mMinLonLongitude = LON_MAX;
67        // The latitude and longitude of the max longitude point.
68        public double mMaxLonLatitude;
69        public double mMaxLonLongitude = LON_MIN;
70    }
71
72    private Context mContext;
73    private Geocoder mGeocoder;
74    private BlobCache mGeoCache;
75    private ConnectivityManager mConnectivityManager;
76    private static Address sCurrentAddress; // last known address
77
78    public ReverseGeocoder(Context context) {
79        mContext = context;
80        mGeocoder = new Geocoder(mContext);
81        mGeoCache = CacheManager.getCache(context, GEO_CACHE_FILE,
82                GEO_CACHE_MAX_ENTRIES, GEO_CACHE_MAX_BYTES,
83                GEO_CACHE_VERSION);
84        mConnectivityManager = (ConnectivityManager)
85                context.getSystemService(Context.CONNECTIVITY_SERVICE);
86    }
87
88    public String computeAddress(SetLatLong set) {
89        // The overall min and max latitudes and longitudes of the set.
90        double setMinLatitude = set.mMinLatLatitude;
91        double setMinLongitude = set.mMinLatLongitude;
92        double setMaxLatitude = set.mMaxLatLatitude;
93        double setMaxLongitude = set.mMaxLatLongitude;
94        if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude)
95                < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) {
96            setMinLatitude = set.mMinLonLatitude;
97            setMinLongitude = set.mMinLonLongitude;
98            setMaxLatitude = set.mMaxLonLatitude;
99            setMaxLongitude = set.mMaxLonLongitude;
100        }
101        Address addr1 = lookupAddress(setMinLatitude, setMinLongitude, true);
102        Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude, true);
103        if (addr1 == null)
104            addr1 = addr2;
105        if (addr2 == null)
106            addr2 = addr1;
107        if (addr1 == null || addr2 == null) {
108            return null;
109        }
110
111        // Get current location, we decide the granularity of the string based
112        // on this.
113        LocationManager locationManager =
114                (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
115        Location location = null;
116        List<String> providers = locationManager.getAllProviders();
117        for (int i = 0; i < providers.size(); ++i) {
118            String provider = providers.get(i);
119            location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null;
120            if (location != null)
121                break;
122        }
123        String currentCity = "";
124        String currentAdminArea = "";
125        String currentCountry = Locale.getDefault().getCountry();
126        if (location != null) {
127            Address currentAddress = lookupAddress(
128                    location.getLatitude(), location.getLongitude(), true);
129            if (currentAddress == null) {
130                currentAddress = sCurrentAddress;
131            } else {
132                sCurrentAddress = currentAddress;
133            }
134            if (currentAddress != null && currentAddress.getCountryCode() != null) {
135                currentCity = checkNull(currentAddress.getLocality());
136                currentCountry = checkNull(currentAddress.getCountryCode());
137                currentAdminArea = checkNull(currentAddress.getAdminArea());
138            }
139        }
140
141        String closestCommonLocation = null;
142        String addr1Locality = checkNull(addr1.getLocality());
143        String addr2Locality = checkNull(addr2.getLocality());
144        String addr1AdminArea = checkNull(addr1.getAdminArea());
145        String addr2AdminArea = checkNull(addr2.getAdminArea());
146        String addr1CountryCode = checkNull(addr1.getCountryCode());
147        String addr2CountryCode = checkNull(addr2.getCountryCode());
148
149        if (currentCity.equals(addr1Locality) || currentCity.equals(addr2Locality)) {
150            String otherCity = currentCity;
151            if (currentCity.equals(addr1Locality)) {
152                otherCity = addr2Locality;
153                if (otherCity.length() == 0) {
154                    otherCity = addr2AdminArea;
155                    if (!currentCountry.equals(addr2CountryCode)) {
156                        otherCity += " " + addr2CountryCode;
157                    }
158                }
159                addr2Locality = addr1Locality;
160                addr2AdminArea = addr1AdminArea;
161                addr2CountryCode = addr1CountryCode;
162            } else {
163                otherCity = addr1Locality;
164                if (otherCity.length() == 0) {
165                    otherCity = addr1AdminArea;
166                    if (!currentCountry.equals(addr1CountryCode)) {
167                        otherCity += " " + addr1CountryCode;
168                    }
169                }
170                addr1Locality = addr2Locality;
171                addr1AdminArea = addr2AdminArea;
172                addr1CountryCode = addr2CountryCode;
173            }
174            closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0));
175            if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
176                if (!currentCity.equals(otherCity)) {
177                    closestCommonLocation += " - " + otherCity;
178                }
179                return closestCommonLocation;
180            }
181
182            // Compare thoroughfare (street address) next.
183            closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare());
184            if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
185                return closestCommonLocation;
186            }
187        }
188
189        // Compare the locality.
190        closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality);
191        if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
192            String adminArea = addr1AdminArea;
193            String countryCode = addr1CountryCode;
194            if (adminArea != null && adminArea.length() > 0) {
195                if (!countryCode.equals(currentCountry)) {
196                    closestCommonLocation += ", " + adminArea + " " + countryCode;
197                } else {
198                    closestCommonLocation += ", " + adminArea;
199                }
200            }
201            return closestCommonLocation;
202        }
203
204        // If the admin area is the same as the current location, we hide it and
205        // instead show the city name.
206        if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) {
207            if ("".equals(addr1Locality)) {
208                addr1Locality = addr2Locality;
209            }
210            if ("".equals(addr2Locality)) {
211                addr2Locality = addr1Locality;
212            }
213            if (!"".equals(addr1Locality)) {
214                if (addr1Locality.equals(addr2Locality)) {
215                    closestCommonLocation = addr1Locality + ", " + currentAdminArea;
216                } else {
217                    closestCommonLocation = addr1Locality + " - " + addr2Locality;
218                }
219                return closestCommonLocation;
220            }
221        }
222
223        // Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE
224        // mile radius.
225        float[] distanceFloat = new float[1];
226        Location.distanceBetween(setMinLatitude, setMinLongitude,
227                setMaxLatitude, setMaxLongitude, distanceFloat);
228        int distance = (int) GalleryUtils.toMile(distanceFloat[0]);
229        if (distance < MAX_LOCALITY_MILE_RANGE) {
230            // Try each of the points and just return the first one to have a
231            // valid address.
232            closestCommonLocation = getLocalityAdminForAddress(addr1, true);
233            if (closestCommonLocation != null) {
234                return closestCommonLocation;
235            }
236            closestCommonLocation = getLocalityAdminForAddress(addr2, true);
237            if (closestCommonLocation != null) {
238                return closestCommonLocation;
239            }
240        }
241
242        // Check the administrative area.
243        closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea);
244        if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
245            String countryCode = addr1CountryCode;
246            if (!countryCode.equals(currentCountry)) {
247                if (countryCode != null && countryCode.length() > 0) {
248                    closestCommonLocation += " " + countryCode;
249                }
250            }
251            return closestCommonLocation;
252        }
253
254        // Check the country codes.
255        closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode);
256        if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
257            return closestCommonLocation;
258        }
259        // There is no intersection, let's choose a nicer name.
260        String addr1Country = addr1.getCountryName();
261        String addr2Country = addr2.getCountryName();
262        if (addr1Country == null)
263            addr1Country = addr1CountryCode;
264        if (addr2Country == null)
265            addr2Country = addr2CountryCode;
266        if (addr1Country == null || addr2Country == null)
267            return null;
268        if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) {
269            closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode;
270        } else {
271            closestCommonLocation = addr1Country + " - " + addr2Country;
272        }
273        return closestCommonLocation;
274    }
275
276    private String checkNull(String locality) {
277        if (locality == null)
278            return "";
279        if (locality.equals("null"))
280            return "";
281        return locality;
282    }
283
284    private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) {
285        if (addr == null)
286            return "";
287        String localityAdminStr = addr.getLocality();
288        if (localityAdminStr != null && !("null".equals(localityAdminStr))) {
289            if (approxLocation) {
290                // TODO: Uncomment these lines as soon as we may translations
291                // for Res.string.around.
292                // localityAdminStr =
293                // mContext.getResources().getString(Res.string.around) + " " +
294                // localityAdminStr;
295            }
296            String adminArea = addr.getAdminArea();
297            if (adminArea != null && adminArea.length() > 0) {
298                localityAdminStr += ", " + adminArea;
299            }
300            return localityAdminStr;
301        }
302        return null;
303    }
304
305    public Address lookupAddress(final double latitude, final double longitude,
306            boolean useCache) {
307        try {
308            long locationKey = (long) (((latitude + LAT_MAX) * 2 * LAT_MAX
309                    + (longitude + LON_MAX)) * EARTH_RADIUS_METERS);
310            byte[] cachedLocation = null;
311            if (useCache && mGeoCache != null) {
312                cachedLocation = mGeoCache.lookup(locationKey);
313            }
314            Address address = null;
315            NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo();
316            if (cachedLocation == null || cachedLocation.length == 0) {
317                if (networkInfo == null || !networkInfo.isConnected()) {
318                    return null;
319                }
320                List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1);
321                if (!addresses.isEmpty()) {
322                    address = addresses.get(0);
323                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
324                    DataOutputStream dos = new DataOutputStream(bos);
325                    Locale locale = address.getLocale();
326                    writeUTF(dos, locale.getLanguage());
327                    writeUTF(dos, locale.getCountry());
328                    writeUTF(dos, locale.getVariant());
329
330                    writeUTF(dos, address.getThoroughfare());
331                    int numAddressLines = address.getMaxAddressLineIndex();
332                    dos.writeInt(numAddressLines);
333                    for (int i = 0; i < numAddressLines; ++i) {
334                        writeUTF(dos, address.getAddressLine(i));
335                    }
336                    writeUTF(dos, address.getFeatureName());
337                    writeUTF(dos, address.getLocality());
338                    writeUTF(dos, address.getAdminArea());
339                    writeUTF(dos, address.getSubAdminArea());
340
341                    writeUTF(dos, address.getCountryName());
342                    writeUTF(dos, address.getCountryCode());
343                    writeUTF(dos, address.getPostalCode());
344                    writeUTF(dos, address.getPhone());
345                    writeUTF(dos, address.getUrl());
346
347                    dos.flush();
348                    if (mGeoCache != null) {
349                        mGeoCache.insert(locationKey, bos.toByteArray());
350                    }
351                    dos.close();
352                }
353            } else {
354                // Parsing the address from the byte stream.
355                DataInputStream dis = new DataInputStream(
356                        new ByteArrayInputStream(cachedLocation));
357                String language = readUTF(dis);
358                String country = readUTF(dis);
359                String variant = readUTF(dis);
360                Locale locale = null;
361                if (language != null) {
362                    if (country == null) {
363                        locale = new Locale(language);
364                    } else if (variant == null) {
365                        locale = new Locale(language, country);
366                    } else {
367                        locale = new Locale(language, country, variant);
368                    }
369                }
370                if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) {
371                    dis.close();
372                    return lookupAddress(latitude, longitude, false);
373                }
374                address = new Address(locale);
375
376                address.setThoroughfare(readUTF(dis));
377                int numAddressLines = dis.readInt();
378                for (int i = 0; i < numAddressLines; ++i) {
379                    address.setAddressLine(i, readUTF(dis));
380                }
381                address.setFeatureName(readUTF(dis));
382                address.setLocality(readUTF(dis));
383                address.setAdminArea(readUTF(dis));
384                address.setSubAdminArea(readUTF(dis));
385
386                address.setCountryName(readUTF(dis));
387                address.setCountryCode(readUTF(dis));
388                address.setPostalCode(readUTF(dis));
389                address.setPhone(readUTF(dis));
390                address.setUrl(readUTF(dis));
391                dis.close();
392            }
393            return address;
394        } catch (Exception e) {
395            // Ignore.
396        }
397        return null;
398    }
399
400    private String valueIfEqual(String a, String b) {
401        return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;
402    }
403
404    public static final void writeUTF(DataOutputStream dos, String string) throws IOException {
405        if (string == null) {
406            dos.writeUTF("");
407        } else {
408            dos.writeUTF(string);
409        }
410    }
411
412    public static final String readUTF(DataInputStream dis) throws IOException {
413        String retVal = dis.readUTF();
414        if (retVal.length() == 0)
415            return null;
416        return retVal;
417    }
418}
419