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 
17 package com.android.gallery3d.util;
18 
19 import android.content.Context;
20 import android.location.Address;
21 import android.location.Geocoder;
22 import android.location.Location;
23 import android.location.LocationManager;
24 import android.net.ConnectivityManager;
25 import android.net.NetworkInfo;
26 
27 import com.android.gallery3d.common.BlobCache;
28 
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.DataInputStream;
32 import java.io.DataOutputStream;
33 import java.io.IOException;
34 import java.util.List;
35 import java.util.Locale;
36 
37 public 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 
ReverseGeocoder(Context context)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 
computeAddress(SetLatLong set)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 
checkNull(String locality)276     private String checkNull(String locality) {
277         if (locality == null)
278             return "";
279         if (locality.equals("null"))
280             return "";
281         return locality;
282     }
283 
getLocalityAdminForAddress(final Address addr, final boolean approxLocation)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 
lookupAddress(final double latitude, final double longitude, boolean useCache)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 
valueIfEqual(String a, String b)400     private String valueIfEqual(String a, String b) {
401         return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;
402     }
403 
writeUTF(DataOutputStream dos, String string)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 
readUTF(DataInputStream dis)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