Home | History | Annotate | Download | only in media
      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