1 /* 2 * Copyright (C) 2009 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.cooliris.media; 18 19 import java.io.BufferedInputStream; 20 import java.io.BufferedOutputStream; 21 import java.io.ByteArrayInputStream; 22 import java.io.ByteArrayOutputStream; 23 import java.io.DataInputStream; 24 import java.io.DataOutputStream; 25 import java.io.IOException; 26 import java.util.List; 27 import java.util.Locale; 28 29 import android.content.Context; 30 import android.location.Address; 31 import android.location.Criteria; 32 import android.location.Geocoder; 33 import android.location.Location; 34 import android.location.LocationManager; 35 import android.os.Process; 36 37 public final class ReverseGeocoder extends Thread { 38 private static final int MAX_COUNTRY_NAME_LENGTH = 8; 39 // If two points are within 20 miles of each other, use 40 // "Around Palo Alto, CA" or "Around Mountain View, CA". 41 // instead of directly jumping to the next level and saying 42 // "California, US". 43 private static final int MAX_LOCALITY_MILE_RANGE = 20; 44 private static final Deque<MediaSet> sQueue = new Deque<MediaSet>(); 45 private static final DiskCache sGeoCache = new DiskCache("geocoder-cache"); 46 private static final String TAG = "ReverseGeocoder"; 47 private static Criteria LOCATION_CRITERIA = new Criteria(); 48 private static Address sCurrentAddress; // last known address 49 50 static { 51 LOCATION_CRITERIA.setAccuracy(Criteria.ACCURACY_COARSE); 52 LOCATION_CRITERIA.setPowerRequirement(Criteria.NO_REQUIREMENT); 53 LOCATION_CRITERIA.setBearingRequired(false); 54 LOCATION_CRITERIA.setSpeedRequired(false); 55 LOCATION_CRITERIA.setAltitudeRequired(false); 56 } 57 58 private Geocoder mGeocoder; 59 private final Context mContext; 60 61 public ReverseGeocoder(Context context) { 62 super(TAG); 63 mContext = context; 64 start(); 65 } 66 67 public void enqueue(MediaSet set) { 68 Deque<MediaSet> inQueue = sQueue; 69 synchronized (inQueue) { 70 inQueue.addFirst(set); 71 inQueue.notify(); 72 } 73 } 74 75 @Override 76 public void run() { 77 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 78 Deque<MediaSet> queue = sQueue; 79 mGeocoder = new Geocoder(mContext); 80 queue.clear(); 81 try { 82 for (;;) { 83 // Wait for the next request. 84 MediaSet set; 85 synchronized (queue) { 86 while ((set = queue.pollFirst()) == null) { 87 queue.wait(); 88 } 89 } 90 // Process the request. 91 process(set); 92 } 93 } catch (InterruptedException e) { 94 // Terminate the thread. 95 } 96 } 97 98 public void flushCache() { 99 sGeoCache.flush(); 100 } 101 102 public void shutdown() { 103 flushCache(); 104 this.interrupt(); 105 } 106 107 private boolean process(final MediaSet set) { 108 if (!set.mLatLongDetermined) { 109 // No latitude, longitude information available. 110 set.mReverseGeocodedLocationComputed = true; 111 return false; 112 } 113 set.mReverseGeocodedLocation = computeMostGranularCommonLocation(set); 114 set.mReverseGeocodedLocationComputed = true; 115 return true; 116 } 117 118 protected String computeMostGranularCommonLocation(final MediaSet set) { 119 // The overall min and max latitudes and longitudes of the set. 120 double setMinLatitude = set.mMinLatLatitude; 121 double setMinLongitude = set.mMinLatLongitude; 122 double setMaxLatitude = set.mMaxLatLatitude; 123 double setMaxLongitude = set.mMaxLatLongitude; 124 if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude) < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) { 125 setMinLatitude = set.mMinLonLatitude; 126 setMinLongitude = set.mMinLonLongitude; 127 setMaxLatitude = set.mMaxLonLatitude; 128 setMaxLongitude = set.mMaxLonLongitude; 129 } 130 Address addr1 = lookupAddress(setMinLatitude, setMinLongitude); 131 Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude); 132 if (addr1 == null) 133 addr1 = addr2; 134 if (addr2 == null) 135 addr2 = addr1; 136 if (addr1 == null || addr2 == null) { 137 return null; 138 } 139 140 // Get current location, we decide the granularity of the string based 141 // on this. 142 LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); 143 Location location = null; 144 List<String> providers = locationManager.getAllProviders(); 145 for (int i = 0; i < providers.size(); ++i) { 146 String provider = providers.get(i); 147 location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null; 148 if (location != null) 149 break; 150 } 151 String currentCity = ""; 152 String currentAdminArea = ""; 153 String currentCountry = Locale.getDefault().getCountry(); 154 if (location != null) { 155 Address currentAddress = lookupAddress(location.getLatitude(), location.getLongitude()); 156 if (currentAddress == null) { 157 currentAddress = sCurrentAddress; 158 } else { 159 sCurrentAddress = currentAddress; 160 } 161 if (currentAddress != null && currentAddress.getCountryCode() != null) { 162 currentCity = checkNull(currentAddress.getLocality()); 163 currentCountry = checkNull(currentAddress.getCountryCode()); 164 currentAdminArea = checkNull(currentAddress.getAdminArea()); 165 } 166 } 167 168 String closestCommonLocation = null; 169 String addr1Locality = checkNull(addr1.getLocality()); 170 String addr2Locality = checkNull(addr2.getLocality()); 171 String addr1AdminArea = checkNull(addr1.getAdminArea()); 172 String addr2AdminArea = checkNull(addr2.getAdminArea()); 173 String addr1CountryCode = checkNull(addr1.getCountryCode()); 174 String addr2CountryCode = checkNull(addr2.getCountryCode()); 175 176 if (currentCity.equals(addr1Locality) && currentCity.equals(addr2Locality)) { 177 String otherCity = currentCity; 178 if (currentCity.equals(addr1Locality)) { 179 otherCity = addr2Locality; 180 if (otherCity.length() == 0) { 181 otherCity = addr2AdminArea; 182 if (!currentCountry.equals(addr2CountryCode)) { 183 otherCity += " " + addr2CountryCode; 184 } 185 } 186 addr2Locality = addr1Locality; 187 addr2AdminArea = addr1AdminArea; 188 addr2CountryCode = addr1CountryCode; 189 } else { 190 otherCity = addr1Locality; 191 if (otherCity.length() == 0) { 192 otherCity = addr1AdminArea + " " + addr1CountryCode; 193 ; 194 if (!currentCountry.equals(addr1CountryCode)) { 195 otherCity += " " + addr1CountryCode; 196 } 197 } 198 addr1Locality = addr2Locality; 199 addr1AdminArea = addr2AdminArea; 200 addr1CountryCode = addr2CountryCode; 201 } 202 closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0)); 203 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) { 204 if (!currentCity.equals(otherCity)) { 205 closestCommonLocation += " - " + otherCity; 206 } 207 return closestCommonLocation; 208 } 209 210 // Compare thoroughfare (street address) next. 211 closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare()); 212 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) { 213 return closestCommonLocation; 214 } 215 } 216 217 // Compare the locality. 218 closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality); 219 if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { 220 String adminArea = addr1AdminArea; 221 String countryCode = addr1CountryCode; 222 if (adminArea != null && adminArea.length() > 0) { 223 if (!countryCode.equals(currentCountry)) { 224 closestCommonLocation += ", " + adminArea + " " + countryCode; 225 } else { 226 closestCommonLocation += ", " + adminArea; 227 } 228 } 229 return closestCommonLocation; 230 } 231 232 // If the admin area is the same as the current location, we hide it and 233 // instead show the city name. 234 if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) { 235 if ("".equals(addr1Locality)) { 236 addr1Locality = addr2Locality; 237 } 238 if ("".equals(addr2Locality)) { 239 addr2Locality = addr1Locality; 240 } 241 if (!"".equals(addr1Locality)) { 242 if (addr1Locality.equals(addr2Locality)) { 243 closestCommonLocation = addr1Locality + ", " + currentAdminArea; 244 } else { 245 closestCommonLocation = addr1Locality + " - " + addr2Locality; 246 } 247 return closestCommonLocation; 248 } 249 } 250 251 // Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE 252 // mile radius. 253 int distance = (int) LocationMediaFilter.toMile(LocationMediaFilter.distanceBetween(setMinLatitude, setMinLongitude, 254 setMaxLatitude, setMaxLongitude)); 255 if (distance < MAX_LOCALITY_MILE_RANGE) { 256 // Try each of the points and just return the first one to have a 257 // valid address. 258 closestCommonLocation = getLocalityAdminForAddress(addr1, true); 259 if (closestCommonLocation != null) { 260 return closestCommonLocation; 261 } 262 closestCommonLocation = getLocalityAdminForAddress(addr2, true); 263 if (closestCommonLocation != null) { 264 return closestCommonLocation; 265 } 266 } 267 268 // Check the administrative area. 269 closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea); 270 if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { 271 String countryCode = addr1CountryCode; 272 if (!countryCode.equals(currentCountry)) { 273 if (countryCode != null && countryCode.length() > 0) { 274 closestCommonLocation += " " + countryCode; 275 } 276 } 277 return closestCommonLocation; 278 } 279 280 // Check the country codes. 281 closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode); 282 if (closestCommonLocation != null && !("".equals(closestCommonLocation))) { 283 return closestCommonLocation; 284 } 285 // There is no intersection, let's choose a nicer name. 286 String addr1Country = addr1.getCountryName(); 287 String addr2Country = addr2.getCountryName(); 288 if (addr1Country == null) 289 addr1Country = addr1CountryCode; 290 if (addr2Country == null) 291 addr2Country = addr2CountryCode; 292 if (addr1Country == null || addr2Country == null) 293 return null; 294 if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) { 295 closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode; 296 } else { 297 closestCommonLocation = addr1Country + " - " + addr2Country; 298 } 299 return closestCommonLocation; 300 } 301 302 private String checkNull(String locality) { 303 if (locality == null) 304 return ""; 305 if (locality.equals("null")) 306 return ""; 307 return locality; 308 } 309 310 protected String getReverseGeocodedLocation(final double latitude, final double longitude, final int desiredNumDetails) { 311 String location = null; 312 int numDetails = 0; 313 try { 314 Address addr = lookupAddress(latitude, longitude); 315 316 if (addr != null) { 317 // Look at the first line of the address, thorough fare and 318 // feature 319 // name in order and pick one. 320 location = addr.getAddressLine(0); 321 if (location != null && !("null".equals(location))) { 322 numDetails++; 323 } else { 324 location = addr.getThoroughfare(); 325 if (location != null && !("null".equals(location))) { 326 numDetails++; 327 } else { 328 location = addr.getFeatureName(); 329 if (location != null && !("null".equals(location))) { 330 numDetails++; 331 } 332 } 333 } 334 335 if (numDetails == desiredNumDetails) { 336 return location; 337 } 338 339 String locality = addr.getLocality(); 340 if (locality != null && !("null".equals(locality))) { 341 if (location != null && location.length() > 0) { 342 location += ", " + locality; 343 } else { 344 location = locality; 345 } 346 numDetails++; 347 } 348 349 if (numDetails == desiredNumDetails) { 350 return location; 351 } 352 353 String adminArea = addr.getAdminArea(); 354 if (adminArea != null && !("null".equals(adminArea))) { 355 if (location != null && location.length() > 0) { 356 location += ", " + adminArea; 357 } else { 358 location = adminArea; 359 } 360 numDetails++; 361 } 362 363 if (numDetails == desiredNumDetails) { 364 return location; 365 } 366 367 String countryCode = addr.getCountryCode(); 368 if (countryCode != null && !("null".equals(countryCode))) { 369 if (location != null && location.length() > 0) { 370 location += ", " + countryCode; 371 } else { 372 location = addr.getCountryName(); 373 } 374 } 375 } 376 377 return location; 378 } catch (Exception e) { 379 return null; 380 } 381 } 382 383 private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) { 384 if (addr == null) 385 return ""; 386 String localityAdminStr = addr.getLocality(); 387 if (localityAdminStr != null && !("null".equals(localityAdminStr))) { 388 if (approxLocation) { 389 // TODO: Uncomment these lines as soon as we may translations 390 // for Res.string.around. 391 // localityAdminStr = 392 // mContext.getResources().getString(Res.string.around) + " " + 393 // localityAdminStr; 394 } 395 String adminArea = addr.getAdminArea(); 396 if (adminArea != null && adminArea.length() > 0) { 397 localityAdminStr += ", " + adminArea; 398 } 399 return localityAdminStr; 400 } 401 return null; 402 } 403 404 private Address lookupAddress(final double latitude, final double longitude) { 405 try { 406 long locationKey = (long) (((latitude + LocationMediaFilter.LAT_MAX) * 2 * LocationMediaFilter.LAT_MAX + (longitude + LocationMediaFilter.LON_MAX)) * LocationMediaFilter.EARTH_RADIUS_METERS); 407 byte[] cachedLocation = sGeoCache.get(locationKey, 0); 408 Address address = null; 409 if (cachedLocation == null || cachedLocation.length == 0) { 410 try { 411 List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1); 412 if (!addresses.isEmpty()) { 413 address = addresses.get(0); 414 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 415 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256)); 416 Locale locale = address.getLocale(); 417 Utils.writeUTF(dos, locale.getLanguage()); 418 Utils.writeUTF(dos, locale.getCountry()); 419 Utils.writeUTF(dos, locale.getVariant()); 420 421 Utils.writeUTF(dos, address.getThoroughfare()); 422 int numAddressLines = address.getMaxAddressLineIndex(); 423 dos.writeInt(numAddressLines); 424 for (int i = 0; i < numAddressLines; ++i) { 425 Utils.writeUTF(dos, address.getAddressLine(i)); 426 } 427 Utils.writeUTF(dos, address.getFeatureName()); 428 Utils.writeUTF(dos, address.getLocality()); 429 Utils.writeUTF(dos, address.getAdminArea()); 430 Utils.writeUTF(dos, address.getSubAdminArea()); 431 432 Utils.writeUTF(dos, address.getCountryName()); 433 Utils.writeUTF(dos, address.getCountryCode()); 434 Utils.writeUTF(dos, address.getPostalCode()); 435 Utils.writeUTF(dos, address.getPhone()); 436 Utils.writeUTF(dos, address.getUrl()); 437 438 dos.flush(); 439 sGeoCache.put(locationKey, bos.toByteArray(), 0); 440 dos.close(); 441 } 442 } finally { 443 444 } 445 } else { 446 // Parsing the address from the byte stream. 447 DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(cachedLocation), 256)); 448 String language = Utils.readUTF(dis); 449 String country = Utils.readUTF(dis); 450 String variant = Utils.readUTF(dis); 451 Locale locale = null; 452 if (language != null) { 453 if (country == null) { 454 locale = new Locale(language); 455 } else if (variant == null) { 456 locale = new Locale(language, country); 457 } else { 458 locale = new Locale(language, country, variant); 459 } 460 } 461 if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) { 462 sGeoCache.delete(locationKey); 463 dis.close(); 464 return lookupAddress(latitude, longitude); 465 } 466 address = new Address(locale); 467 468 address.setThoroughfare(Utils.readUTF(dis)); 469 int numAddressLines = dis.readInt(); 470 for (int i = 0; i < numAddressLines; ++i) { 471 address.setAddressLine(i, Utils.readUTF(dis)); 472 } 473 address.setFeatureName(Utils.readUTF(dis)); 474 address.setLocality(Utils.readUTF(dis)); 475 address.setAdminArea(Utils.readUTF(dis)); 476 address.setSubAdminArea(Utils.readUTF(dis)); 477 478 address.setCountryName(Utils.readUTF(dis)); 479 address.setCountryCode(Utils.readUTF(dis)); 480 address.setPostalCode(Utils.readUTF(dis)); 481 address.setPhone(Utils.readUTF(dis)); 482 address.setUrl(Utils.readUTF(dis)); 483 dis.close(); 484 } 485 return address; 486 } catch (Exception e) { 487 // Ignore. 488 } 489 return null; 490 } 491 492 private String valueIfEqual(String a, String b) { 493 return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null; 494 } 495 } 496