From f7d515057b9754c9d5e75f781bb88078867b8425 Mon Sep 17 00:00:00 2001 From: Xiao-Long Chen Date: Mon, 12 Sep 2016 09:34:02 +0200 Subject: Re-add dialer lookup. Author: Xiao-Long Chen Date: Mon Sep 12 09:34:02 2016 +0200 Re-add dialer lookup. BUGBASH-612: do not send phone numbers to non-ssl sites for reverse/forward/people lookups Change-Id: I677460ad5767b8698ee24d6d43ff159aee55387a Author: Joey Date: Wed Mar 28 21:11:16 2018 +0200 Dialer: comply with EU's GDPR Disable lookup by default and add a disclaimer for the feature Change-Id: If7a181952304dbaee736762bdfd5819eddc5f89b Signed-off-by: Joey Change-Id: I4ff90a678618fa8c7b5970dff3dd246b0c87135c --- .../app/settings/DialerSettingsActivity.java | 6 + .../dialer/binary/aosp/AospDialerApplication.java | 60 ++- java/com/android/dialer/lookup/AndroidManifest.xml | 31 ++ java/com/android/dialer/lookup/ContactBuilder.java | 475 +++++++++++++++++++++ java/com/android/dialer/lookup/DirectoryId.java | 33 ++ java/com/android/dialer/lookup/ForwardLookup.java | 63 +++ java/com/android/dialer/lookup/LookupCache.java | 296 +++++++++++++ .../android/dialer/lookup/LookupCacheService.java | 111 +++++ java/com/android/dialer/lookup/LookupProvider.java | 462 ++++++++++++++++++++ java/com/android/dialer/lookup/LookupSettings.java | 131 ++++++ .../dialer/lookup/LookupSettingsFragment.java | 137 ++++++ java/com/android/dialer/lookup/LookupUtils.java | 168 ++++++++ java/com/android/dialer/lookup/PeopleLookup.java | 56 +++ java/com/android/dialer/lookup/ReverseLookup.java | 103 +++++ .../dialer/lookup/ReverseLookupService.java | 197 +++++++++ .../dialer/lookup/auskunft/AuskunftApi.java | 114 +++++ .../lookup/auskunft/AuskunftPeopleLookup.java | 57 +++ .../lookup/auskunft/AuskunftReverseLookup.java | 54 +++ .../lookup/dastelefonbuch/TelefonbuchApi.java | 85 ++++ .../dastelefonbuch/TelefonbuchReverseLookup.java | 65 +++ .../dialer/lookup/google/GoogleForwardLookup.java | 237 ++++++++++ .../lookup/opencnam/OpenCnamReverseLookup.java | 99 +++++ .../openstreetmap/OpenStreetMapForwardLookup.java | 156 +++++++ .../ic_places_picture_180_holo_light.png | Bin 0 -> 698 bytes .../drawable-hdpi/ic_places_picture_holo_light.png | Bin 0 -> 424 bytes .../ic_places_picture_180_holo_light.png | Bin 0 -> 1140 bytes .../ic_places_picture_holo_light.png | Bin 0 -> 632 bytes .../ic_places_picture_180_holo_light.png | Bin 0 -> 1171 bytes .../ic_places_picture_holo_light.png | Bin 0 -> 636 bytes .../android/dialer/lookup/res/values/cm_arrays.xml | 53 +++ .../dialer/lookup/res/values/cm_strings.xml | 36 ++ .../dialer/lookup/res/xml/lookup_settings.xml | 70 +++ .../dialer/lookup/yellowpages/YellowPagesApi.java | 204 +++++++++ .../yellowpages/YellowPagesReverseLookup.java | 119 ++++++ .../dialer/lookup/zabasearch/ZabaSearchApi.java | 104 +++++ .../lookup/zabasearch/ZabaSearchReverseLookup.java | 57 +++ 36 files changed, 3838 insertions(+), 1 deletion(-) create mode 100644 java/com/android/dialer/lookup/AndroidManifest.xml create mode 100644 java/com/android/dialer/lookup/ContactBuilder.java create mode 100644 java/com/android/dialer/lookup/DirectoryId.java create mode 100644 java/com/android/dialer/lookup/ForwardLookup.java create mode 100644 java/com/android/dialer/lookup/LookupCache.java create mode 100644 java/com/android/dialer/lookup/LookupCacheService.java create mode 100644 java/com/android/dialer/lookup/LookupProvider.java create mode 100644 java/com/android/dialer/lookup/LookupSettings.java create mode 100644 java/com/android/dialer/lookup/LookupSettingsFragment.java create mode 100644 java/com/android/dialer/lookup/LookupUtils.java create mode 100644 java/com/android/dialer/lookup/PeopleLookup.java create mode 100644 java/com/android/dialer/lookup/ReverseLookup.java create mode 100644 java/com/android/dialer/lookup/ReverseLookupService.java create mode 100644 java/com/android/dialer/lookup/auskunft/AuskunftApi.java create mode 100644 java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java create mode 100644 java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java create mode 100644 java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java create mode 100644 java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java create mode 100644 java/com/android/dialer/lookup/google/GoogleForwardLookup.java create mode 100644 java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java create mode 100644 java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java create mode 100644 java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/values/cm_arrays.xml create mode 100644 java/com/android/dialer/lookup/res/values/cm_strings.xml create mode 100644 java/com/android/dialer/lookup/res/xml/lookup_settings.xml create mode 100644 java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java create mode 100644 java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java create mode 100644 java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java create mode 100644 java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java index cdea6faa5..6ffa62a44 100644 --- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java +++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java @@ -39,6 +39,7 @@ import com.android.dialer.blocking.FilteredNumberCompat; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.telephony.TelephonyManagerCompat; import com.android.dialer.configprovider.ConfigProviderComponent; +import com.android.dialer.lookup.LookupSettingsFragment; import com.android.dialer.proguard.UsedByReflection; import com.android.dialer.util.PermissionsUtil; import com.android.dialer.voicemail.settings.VoicemailSettingsFragment; @@ -113,6 +114,11 @@ public class DialerSettingsActivity extends AppCompatPreferenceActivity { quickResponseSettingsHeader.intent = quickResponseSettingsIntent; target.add(quickResponseSettingsHeader); + final Header lookupSettingsHeader = new Header(); + lookupSettingsHeader.titleRes = R.string.lookup_settings_label; + lookupSettingsHeader.fragment = LookupSettingsFragment.class.getName(); + target.add(lookupSettingsHeader); + TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); diff --git a/java/com/android/dialer/binary/aosp/AospDialerApplication.java b/java/com/android/dialer/binary/aosp/AospDialerApplication.java index 4ca94e277..8d94bb92b 100644 --- a/java/com/android/dialer/binary/aosp/AospDialerApplication.java +++ b/java/com/android/dialer/binary/aosp/AospDialerApplication.java @@ -16,15 +16,34 @@ package com.android.dialer.binary.aosp; +import android.content.Context; +import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.android.contacts.common.extensions.PhoneDirectoryExtender; +import com.android.contacts.common.extensions.PhoneDirectoryExtenderFactory; import com.android.dialer.binary.common.DialerApplication; import com.android.dialer.inject.ContextModule; +import com.android.dialer.lookup.LookupCacheService; +import com.android.dialer.lookup.LookupProvider; +import com.android.dialer.lookup.LookupSettings; +import com.android.dialer.lookup.ReverseLookupService; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.PhoneNumberCacheBindings; +import com.android.dialer.phonenumbercache.PhoneNumberCacheBindingsFactory; +import com.android.incallui.bindings.InCallUiBindings; +import com.android.incallui.bindings.InCallUiBindingsFactory; +import com.android.incallui.bindings.InCallUiBindingsStub; +import com.android.incallui.bindings.PhoneNumberService; + +import java.util.List; /** * The application class for the AOSP Dialer. This is a version of the Dialer app that has no * dependency on Google Play Services. */ -public class AospDialerApplication extends DialerApplication { +public class AospDialerApplication extends DialerApplication implements + PhoneNumberCacheBindingsFactory, PhoneDirectoryExtenderFactory, InCallUiBindingsFactory { /** Returns a new instance of the root component for the AOSP Dialer. */ @Override @@ -32,4 +51,43 @@ public class AospDialerApplication extends DialerApplication { protected Object buildRootComponent() { return DaggerAospDialerRootComponent.builder().contextModule(new ContextModule(this)).build(); } + + @Override + public PhoneDirectoryExtender newPhoneDirectoryExtender() { + return new PhoneDirectoryExtender() { + @Override + public boolean isEnabled(Context context) { + return LookupSettings.isForwardLookupEnabled(AospDialerApplication.this) + || LookupSettings.isPeopleLookupEnabled(AospDialerApplication.this); + } + + @Override + @Nullable + public Uri getContentUri() { + return LookupProvider.NEARBY_AND_PEOPLE_LOOKUP_URI; + } + }; + } + + @Override + public InCallUiBindings newInCallUiBindings() { + return new InCallUiBindingsStub() { + @Override + @Nullable + public PhoneNumberService newPhoneNumberService(Context context) { + return new ReverseLookupService(context); + } + }; + } + + @Override + public PhoneNumberCacheBindings newPhoneNumberCacheBindings() { + return new PhoneNumberCacheBindings() { + @Override + @Nullable + public CachedNumberLookupService getCachedNumberLookupService() { + return new LookupCacheService(); + } + }; + } } diff --git a/java/com/android/dialer/lookup/AndroidManifest.xml b/java/com/android/dialer/lookup/AndroidManifest.xml new file mode 100644 index 000000000..0a278db15 --- /dev/null +++ b/java/com/android/dialer/lookup/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/java/com/android/dialer/lookup/ContactBuilder.java b/java/com/android/dialer/lookup/ContactBuilder.java new file mode 100644 index 000000000..e88f956d2 --- /dev/null +++ b/java/com/android/dialer/lookup/ContactBuilder.java @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.ContentResolver; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.DisplayNameSources; +import android.text.TextUtils; +import android.util.Log; + +import com.android.contacts.common.util.Constants; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.w3c.dom.Text; + +import java.sql.Struct; +import java.util.ArrayList; + +public class ContactBuilder { + private static final String TAG = ContactBuilder.class.getSimpleName(); + + private static final boolean DEBUG = false; + + /** Default photo for businesses if no other image is found */ + public static final String PHOTO_URI_BUSINESS = new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority("com.android.dialer") + .appendPath(String.valueOf(R.drawable.ic_places_picture_180_holo_light)) + .build() + .toString(); + + private final ArrayList
addresses = new ArrayList<>(); + private final ArrayList phoneNumbers = new ArrayList<>(); + private final ArrayList websites = new ArrayList<>(); + + private final long directoryId; + private Name name; + private final String normalizedNumber; + private final String formattedNumber; + private Uri photoUri; + + public static ContactBuilder forForwardLookup(String number) { + return new ContactBuilder(DirectoryId.NEARBY, null, number); + } + + public static ContactBuilder forPeopleLookup(String number) { + return new ContactBuilder(DirectoryId.PEOPLE, null, number); + } + + public static ContactBuilder forReverseLookup(String normalizedNumber, String formattedNumber) { + return new ContactBuilder(DirectoryId.NULL, normalizedNumber, formattedNumber); + } + + private ContactBuilder(long directoryId, String normalizedNumber, String formattedNumber) { + this.directoryId = directoryId; + this.normalizedNumber = normalizedNumber; + this.formattedNumber = formattedNumber; + } + + public ContactBuilder(Uri encodedContactUri) throws JSONException { + String jsonData = encodedContactUri.getEncodedFragment(); + String directoryIdStr = encodedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); + long directoryId = DirectoryId.DEFAULT; + + if (!TextUtils.isEmpty(directoryIdStr)) { + try { + directoryId = Long.parseLong(directoryIdStr); + } catch (NumberFormatException e) { + Log.e(TAG, "Error parsing directory id of uri " + encodedContactUri, e); + } + } + + this.directoryId = directoryId; + this.formattedNumber = null; + this.normalizedNumber = null; + + try { + // name + JSONObject json = new JSONObject(jsonData); + JSONObject contact = json.optJSONObject(Contacts.CONTENT_ITEM_TYPE); + JSONObject nameObj = contact.optJSONObject(StructuredName.CONTENT_ITEM_TYPE); + name = new Name(nameObj); + + if (contact != null) { + // numbers + if (contact.has(Phone.CONTENT_ITEM_TYPE)) { + String phoneData = contact.getString(Phone.CONTENT_ITEM_TYPE); + Object phoneObject = new JSONTokener(phoneData).nextValue(); + JSONArray phoneNumbersJson; + if (phoneObject instanceof JSONObject) { + phoneNumbersJson = new JSONArray(); + phoneNumbersJson.put(phoneObject); + } else { + phoneNumbersJson = contact.getJSONArray(Phone.CONTENT_ITEM_TYPE); + } + for (int i = 0; i < phoneNumbersJson.length(); ++i) { + JSONObject phoneObj = phoneNumbersJson.getJSONObject(i); + phoneNumbers.add(new PhoneNumber(phoneObj)); + } + } + + // address + if (contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) { + JSONArray addressesJson = contact.getJSONArray(StructuredPostal.CONTENT_ITEM_TYPE); + for (int i = 0; i < addressesJson.length(); ++i) { + JSONObject addrObj = addressesJson.getJSONObject(i); + addresses.add(new Address(addrObj)); + } + } + + // websites + if (contact.has(Website.CONTENT_ITEM_TYPE)) { + JSONArray websitesJson = contact.getJSONArray(Website.CONTENT_ITEM_TYPE); + for (int i = 0; i < websitesJson.length(); ++i) { + JSONObject websiteObj = websitesJson.getJSONObject(i); + final WebsiteUrl websiteUrl = new WebsiteUrl(websiteObj); + if (!TextUtils.isEmpty(websiteUrl.url)) { + websites.add(new WebsiteUrl(websiteObj)); + } + } + } + } + } catch(JSONException e) { + Log.e(TAG, "Error parsing encoded fragment of uri " + encodedContactUri, e); + throw e; + } + } + + public ContactBuilder addAddress(Address address) { + if (DEBUG) Log.d(TAG, "Adding address"); + if (address != null) { + addresses.add(address); + } + return this; + } + + public ContactBuilder addPhoneNumber(PhoneNumber phoneNumber) { + if (DEBUG) Log.d(TAG, "Adding phone number"); + if (phoneNumber != null) { + phoneNumbers.add(phoneNumber); + } + return this; + } + + public ContactBuilder addWebsite(WebsiteUrl website) { + if (DEBUG) Log.d(TAG, "Adding website"); + if (website != null) { + websites.add(website); + } + return this; + } + + public ContactBuilder setName(Name name) { + if (DEBUG) Log.d(TAG, "Setting name"); + if (name != null) { + this.name = name; + } + return this; + } + + public ContactBuilder setPhotoUri(String photoUri) { + if (photoUri != null) { + setPhotoUri(Uri.parse(photoUri)); + } + return this; + } + + public ContactBuilder setPhotoUri(Uri photoUri) { + if (DEBUG) Log.d(TAG, "Setting photo URI"); + this.photoUri = photoUri; + return this; + } + + public ContactInfo build() { + if (name == null) { + throw new IllegalStateException("Name has not been set"); + } + + // Use the incoming call's phone number if no other phone number + // is specified. The reverse lookup source could present the phone + // number differently (eg. without the area code). + if (phoneNumbers.isEmpty()) { + PhoneNumber pn = new PhoneNumber(); + // Use the formatted number where possible + pn.number = formattedNumber != null + ? formattedNumber : normalizedNumber; + pn.type = Phone.TYPE_MAIN; + addPhoneNumber(pn); + } + + try { + JSONObject contact = new JSONObject(); + + // Insert the name + contact.put(StructuredName.CONTENT_ITEM_TYPE, name.getJsonObject()); + + // Insert phone numbers + JSONArray phoneNumbersJson = new JSONArray(); + for (PhoneNumber number : phoneNumbers) { + phoneNumbersJson.put(number.getJsonObject()); + } + contact.put(Phone.CONTENT_ITEM_TYPE, phoneNumbersJson); + + // Insert addresses if there are any + if (!addresses.isEmpty()) { + JSONArray addressesJson = new JSONArray(); + for (Address address : addresses) { + addressesJson.put(address.getJsonObject()); + } + contact.put(StructuredPostal.CONTENT_ITEM_TYPE, addressesJson); + } + + // Insert websites if there are any + if (!websites.isEmpty()) { + JSONArray websitesJson = new JSONArray(); + for (WebsiteUrl site : websites) { + websitesJson.put(site.getJsonObject()); + } + contact.put(Website.CONTENT_ITEM_TYPE, websitesJson); + } + + ContactInfo info = new ContactInfo(); + info.name = name.displayName; + info.normalizedNumber = normalizedNumber; + info.number = phoneNumbers.get(0).number; + info.type = phoneNumbers.get(0).type; + info.label = phoneNumbers.get(0).label; + info.photoUri = photoUri; + + String json = new JSONObject() + .put(Contacts.DISPLAY_NAME, name.displayName) + .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.ORGANIZATION) + .put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT) + .put(Contacts.CONTENT_ITEM_TYPE, contact) + .toString(); + + if (json != null) { + info.lookupUri = Contacts.CONTENT_LOOKUP_URI + .buildUpon() + .appendPath(Constants.LOOKUP_URI_ENCODED) + .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, + String.valueOf(directoryId)) + .encodedFragment(json) + .build(); + } + + return info; + } catch (JSONException e) { + Log.e(TAG, "Failed to build contact", e); + return null; + } + } + + // android.provider.ContactsContract.CommonDataKinds.StructuredPostal + public static class Address { + public String formattedAddress; + public int type; + public String label; + public String street; + public String poBox; + public String neighborhood; + public String city; + public String region; + public String postCode; + public String country; + + public static Address createFormattedHome(String address) { + if (address == null) { + return null; + } + Address a = new Address(); + a.formattedAddress = address; + a.type = StructuredPostal.TYPE_HOME; + return a; + } + + public JSONObject getJsonObject() throws JSONException { + JSONObject json = new JSONObject(); + json.putOpt(StructuredPostal.FORMATTED_ADDRESS, formattedAddress); + json.put(StructuredPostal.TYPE, type); + json.putOpt(StructuredPostal.LABEL, label); + json.putOpt(StructuredPostal.STREET, street); + json.putOpt(StructuredPostal.POBOX, poBox); + json.putOpt(StructuredPostal.NEIGHBORHOOD, neighborhood); + json.putOpt(StructuredPostal.CITY, city); + json.putOpt(StructuredPostal.REGION, region); + json.putOpt(StructuredPostal.POSTCODE, postCode); + json.putOpt(StructuredPostal.COUNTRY, country); + return json; + } + + public Address() {} + + public Address(JSONObject json) throws JSONException { + if (json.has(StructuredPostal.FORMATTED_ADDRESS)) { + formattedAddress = json.getString(StructuredPostal.FORMATTED_ADDRESS); + } + } + + public String toString() { + return "formattedAddress: " + formattedAddress + "; " + + "type: " + type + "; " + + "label: " + label + "; " + + "street: " + street + "; " + + "poBox: " + poBox + "; " + + "neighborhood: " + neighborhood + "; " + + "city: " + city + "; " + + "region: " + region + "; " + + "postCode: " + postCode + "; " + + "country: " + country; + } + } + + // android.provider.ContactsContract.CommonDataKinds.StructuredName + public static class Name { + public String displayName; + public String givenName; + public String familyName; + public String prefix; + public String middleName; + public String suffix; + public String phoneticGivenName; + public String phoneticMiddleName; + public String phoneticFamilyName; + + public static Name createDisplayName(String displayName) { + Name name = new Name(); + name.displayName = displayName; + return name; + } + + public JSONObject getJsonObject() throws JSONException { + JSONObject json = new JSONObject(); + json.putOpt(StructuredName.DISPLAY_NAME, displayName); + json.putOpt(StructuredName.GIVEN_NAME, givenName); + json.putOpt(StructuredName.FAMILY_NAME, familyName); + json.putOpt(StructuredName.PREFIX, prefix); + json.putOpt(StructuredName.MIDDLE_NAME, middleName); + json.putOpt(StructuredName.SUFFIX, suffix); + json.putOpt(StructuredName.PHONETIC_GIVEN_NAME, phoneticGivenName); + json.putOpt(StructuredName.PHONETIC_MIDDLE_NAME, phoneticMiddleName); + json.putOpt(StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName); + return json; + } + + public Name(JSONObject json) throws JSONException { + if (json != null) { + displayName = json.optString(StructuredName.DISPLAY_NAME, null); + } + } + + public Name() {} + + public String toString() { + return "displayName: " + displayName + "; " + + "givenName: " + givenName + "; " + + "familyName: " + familyName + "; " + + "prefix: " + prefix + "; " + + "middleName: " + middleName + "; " + + "suffix: " + suffix + "; " + + "phoneticGivenName: " + phoneticGivenName + "; " + + "phoneticMiddleName: " + phoneticMiddleName + "; " + + "phoneticFamilyName: " + phoneticFamilyName; + } + } + + // android.provider.ContactsContract.CommonDataKinds.Phone + public static class PhoneNumber { + public String number; + public int type; + public String label; + + public static PhoneNumber createMainNumber(String number) { + PhoneNumber n = new PhoneNumber(); + n.number = number; + n.type = Phone.TYPE_MAIN; + return n; + } + + public JSONObject getJsonObject() throws JSONException { + JSONObject json = new JSONObject(); + json.put(Phone.NUMBER, number); + json.put(Phone.TYPE, type); + json.putOpt(Phone.LABEL, label); + return json; + } + + public PhoneNumber(JSONObject json) throws JSONException { + number = json.getString(Phone.NUMBER); + type = json.getInt(Phone.TYPE); + if (json.has(Phone.LABEL)) { + label = json.getString(Phone.LABEL); + } + } + + public PhoneNumber() {} + + public String toString() { + return "number: " + number + "; " + + "type: " + type + "; " + + "label: " + label; + } + } + + // android.provider.ContactsContract.CommonDataKinds.Website + public static class WebsiteUrl { + public String url; + public int type; + public String label; + + public static WebsiteUrl createProfile(String url) { + if (url == null) { + return null; + } + WebsiteUrl u = new WebsiteUrl(); + u.url = url; + u.type = Website.TYPE_PROFILE; + return u; + } + + public JSONObject getJsonObject() throws JSONException { + JSONObject json = new JSONObject(); + json.put(Website.URL, url); + json.put(Website.TYPE, type); + json.putOpt(Website.LABEL, label); + return json; + } + + public WebsiteUrl() {} + + public WebsiteUrl(JSONObject json) throws JSONException { + if (json.has(Website.URL)) { + url = json.getString(Website.URL); + } + if (json.has(Website.TYPE)) { + type = json.getInt(Website.TYPE); + } + if (json.has(Website.LABEL)) { + label = json.getString(Website.LABEL); + } + } + + public String toString() { + return "url: " + url + "; " + + "type: " + type + "; " + + "label: " + label; + } + } +} diff --git a/java/com/android/dialer/lookup/DirectoryId.java b/java/com/android/dialer/lookup/DirectoryId.java new file mode 100644 index 000000000..023585c36 --- /dev/null +++ b/java/com/android/dialer/lookup/DirectoryId.java @@ -0,0 +1,33 @@ +package com.android.dialer.lookup; + +import android.net.Uri; +import android.provider.ContactsContract; + +public class DirectoryId { + // default contacts directory + public static final long DEFAULT = ContactsContract.Directory.DEFAULT; + + // id for a non existant directory + public static final long NULL = Long.MAX_VALUE; + + // id for nearby forward lookup results (not a real directory) + public static final long NEARBY = NULL - 1; + + // id for people forward lookup results (not a real directory) + public static final long PEOPLE = NULL - 2; + + public static boolean isFakeDirectory(long directory) { + return directory == NULL || directory == NEARBY || directory == PEOPLE; + } + + public static long fromUri(Uri lookupUri) { + long directory = DirectoryId.DEFAULT; + if (lookupUri != null) { + String dqp = lookupUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); + if (dqp != null) { + directory = Long.valueOf(dqp); + } + } + return directory; + } +} diff --git a/java/com/android/dialer/lookup/ForwardLookup.java b/java/com/android/dialer/lookup/ForwardLookup.java new file mode 100644 index 000000000..2f59aeba1 --- /dev/null +++ b/java/com/android/dialer/lookup/ForwardLookup.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.location.Location; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.google.GoogleForwardLookup; +import com.android.dialer.lookup.openstreetmap.OpenStreetMapForwardLookup; + +import java.util.List; + +public abstract class ForwardLookup { + private static final String TAG = ForwardLookup.class.getSimpleName(); + + private static ForwardLookup INSTANCE = null; + + public static ForwardLookup getInstance(Context context) { + String provider = LookupSettings.getForwardLookupProvider(context); + + if (INSTANCE == null || !isInstance(provider)) { + Log.d(TAG, "Chosen forward lookup provider: " + provider); + + if (provider.equals(LookupSettings.FLP_GOOGLE)) { + INSTANCE = new GoogleForwardLookup(context); + } else if (provider.equals(LookupSettings.FLP_OPENSTREETMAP)) { + INSTANCE = new OpenStreetMapForwardLookup(context); + } + } + + return INSTANCE; + } + + private static boolean isInstance(String provider) { + if (provider.equals(LookupSettings.FLP_GOOGLE) + && INSTANCE instanceof GoogleForwardLookup) { + return true; + } else if (provider.equals(LookupSettings.FLP_OPENSTREETMAP) + && INSTANCE instanceof OpenStreetMapForwardLookup) { + return true; + } else { + return false; + } + } + + public abstract List lookup(Context context, String filter, Location lastLocation); +} diff --git a/java/com/android/dialer/lookup/LookupCache.java b/java/com/android/dialer/lookup/LookupCache.java new file mode 100644 index 000000000..6fe3a9f94 --- /dev/null +++ b/java/com/android/dialer/lookup/LookupCache.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.util.DialerUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; + +public class LookupCache { + private static final String TAG = LookupCache.class.getSimpleName(); + + public static final String NAME = "Name"; + public static final String TYPE = "Type"; + public static final String LABEL = "Label"; + public static final String NUMBER = "Number"; + public static final String FORMATTED_NUMBER = "FormattedNumber"; + public static final String NORMALIZED_NUMBER = "NormalizedNumber"; + public static final String PHOTO_ID = "PhotoID"; + public static final String LOOKUP_URI = "LookupURI"; + + public static boolean hasCachedContact(Context context, String number) { + String normalizedNumber = formatE164(context, number); + if (normalizedNumber == null) { + return false; + } + + File file = getFilePath(context, normalizedNumber); + return file.exists(); + } + + public static void cacheContact(Context context, ContactInfo info) { + File file = getFilePath(context, info.normalizedNumber); + + if (file.exists()) { + file.delete(); + } + + FileOutputStream out = null; + JsonWriter writer = null; + + try { + out = new FileOutputStream(file); + writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8")); + writer.setIndent(" "); + List messages = new ArrayList(); + + writer.beginObject(); + if (info.name != null) { + writer.name(NAME).value(info.name); + } + writer.name(TYPE).value(info.type); + if (info.label != null) { + writer.name(LABEL).value(info.label); + } + if (info.number != null) { + writer.name(NUMBER).value(info.number); + } + if (info.formattedNumber != null) { + writer.name(FORMATTED_NUMBER).value(info.formattedNumber); + } + if (info.normalizedNumber != null) { + writer.name(NORMALIZED_NUMBER).value(info.normalizedNumber); + } + writer.name(PHOTO_ID).value(info.photoId); + + if (info.lookupUri != null) { + writer.name(LOOKUP_URI).value(info.lookupUri.toString()); + } + + // We do not save the photo URI. If there's a cached image, that + // will be used when the contact is retrieved. Otherwise, photoUri + // will be set to null. + + writer.endObject(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + DialerUtils.closeQuietly(writer); + DialerUtils.closeQuietly(out); + } + } + + public static ContactInfo getCachedContact(Context context, String number) { + String normalizedNumber = formatE164(context, number); + if (normalizedNumber == null) { + return null; + } + + File file = getFilePath(context, normalizedNumber); + if (!file.exists()) { + // Whatever is calling this should probably check anyway + return null; + } + + ContactInfo info = new ContactInfo(); + + FileInputStream in = null; + JsonReader reader = null; + + try { + in = new FileInputStream(file); + reader = new JsonReader(new InputStreamReader(in, "UTF-8")); + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + + if (NAME.equals(name)) { + info.name = reader.nextString(); + } else if (TYPE.equals(name)) { + info.type = reader.nextInt(); + } else if (LABEL.equals(name)) { + info.label = reader.nextString(); + } else if (NUMBER.equals(name)) { + info.number = reader.nextString(); + } else if (FORMATTED_NUMBER.equals(name)) { + info.formattedNumber = reader.nextString(); + } else if (NORMALIZED_NUMBER.equals(name)) { + info.normalizedNumber = reader.nextString(); + } else if (PHOTO_ID.equals(name)) { + info.photoId = reader.nextInt(); + } else if (LOOKUP_URI.equals(name)) { + Uri lookupUri = Uri.parse(reader.nextString()); + + if (hasCachedImage(context, normalizedNumber)) { + // Insert cached photo URI + Uri image = Uri.withAppendedPath(LookupProvider.IMAGE_CACHE_URI, + Uri.encode(normalizedNumber)); + + String json = lookupUri.getEncodedFragment(); + if (json != null) { + try { + JSONObject jsonObj = new JSONObject(json); + jsonObj.putOpt(Contacts.PHOTO_URI, image.toString()); + lookupUri = lookupUri.buildUpon() + .encodedFragment(jsonObj.toString()) + .build(); + } catch (JSONException e) { + Log.e(TAG, "Failed to add image URI to json", e); + } + } + + info.photoUri = image; + } + + info.lookupUri = lookupUri; + } + } + reader.endObject(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + DialerUtils.closeQuietly(reader); + DialerUtils.closeQuietly(in); + } + + return info; + } + + public static void deleteCachedContacts(Context context) { + File dir = new File(context.getCacheDir(), "lookup"); + if (!dir.exists()) { + Log.v(TAG, "Lookup cache directory does not exist. Not clearing it."); + return; + } + + if (!dir.isDirectory()) { + Log.e(TAG, "Path " + dir + " is not a directory"); + return; + } + + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + file.delete(); + } + } + } + } + + public static void deleteCachedContact(Context context, String normalizedNumber) { + File f = getFilePath(context, normalizedNumber); + if (f.exists()) { + f.delete(); + } + + f = getImagePath(context, normalizedNumber); + if (f.exists()) { + f.delete(); + } + } + + public static boolean hasCachedImage(Context context, String number) { + String normalizedNumber = formatE164(context, number); + if (normalizedNumber == null) { + return false; + } + + File file = getImagePath(context, normalizedNumber); + return file.exists(); + } + + public static Uri cacheImage(Context context, String normalizedNumber, Bitmap bmp) { + // Compress the cached images to save space + if (bmp == null) { + Log.e(TAG, "Failed to cache image"); + return null; + } + + File image = getImagePath(context, normalizedNumber); + FileOutputStream out = null; + + try { + out = new FileOutputStream(image); + bmp.compress(Bitmap.CompressFormat.WEBP, 100, out); + return Uri.fromFile(image); + } catch (Exception e) { + e.printStackTrace(); + } finally { + DialerUtils.closeQuietly(out); + } + return null; + } + + public static Bitmap getCachedImage(Context context, String normalizedNumber) { + File image = getImagePath(context, normalizedNumber); + if (!image.exists()) { + return null; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeFile(image.getPath(), options); + } + + private static String formatE164(Context context, String number) { + TelephonyManager tm = context.getSystemService(TelephonyManager.class); + String countryIso = tm.getSimCountryIso().toUpperCase(); + return PhoneNumberUtils.formatNumberToE164(number, countryIso); + } + + private static File getFilePath(Context context, String normalizedNumber) { + File dir = new File(context.getCacheDir(), "lookup"); + if (!dir.exists()) { + dir.mkdirs(); + } + + return new File(dir, normalizedNumber + ".json"); + } + + public static File getImagePath(Context context, String normalizedNumber) { + File dir = new File(context.getCacheDir(), "lookup"); + if (!dir.exists()) { + dir.mkdirs(); + } + + return new File(dir, normalizedNumber + ".webp"); + } +} diff --git a/java/com/android/dialer/lookup/LookupCacheService.java b/java/com/android/dialer/lookup/LookupCacheService.java new file mode 100644 index 000000000..43dc9f06c --- /dev/null +++ b/java/com/android/dialer/lookup/LookupCacheService.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2018 The LineageOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; + +import com.android.dialer.logging.ContactSource; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.ContactInfo; + +import java.io.InputStream; + +public class LookupCacheService implements CachedNumberLookupService { + @Override + public CachedContactInfo buildCachedContactInfo(ContactInfo info) { + return new LookupCachedContactInfo(info); + } + + @Override + public void addContact(Context context, CachedContactInfo cachedInfo) { + LookupCache.cacheContact(context, cachedInfo.getContactInfo()); + } + + @Override + public CachedContactInfo lookupCachedContactFromNumber(Context context, String number) { + ContactInfo info = LookupCache.getCachedContact(context, number); + return info != null ? new LookupCachedContactInfo(info) : null; + } + + @Override + public void clearAllCacheEntries(Context context) { + LookupCache.deleteCachedContacts(context); + } + + @Override + public boolean isBusiness(ContactSource.Type sourceType) { + // We don't store source type, so assume false + return false; + } + + @Override + public boolean canReportAsInvalid(ContactSource.Type sourceType, String objectId) { + return false; + } + + @Override + public boolean reportAsInvalid(Context context, CachedContactInfo cachedContactInfo) { + return false; + } + + @Override + public @Nullable Uri addPhoto(Context context, String number, InputStream in) { + TelephonyManager tm = context.getSystemService(TelephonyManager.class); + String countryIso = tm.getSimCountryIso().toUpperCase(); + String normalized = number != null + ? PhoneNumberUtils.formatNumberToE164(number, countryIso) : null; + if (normalized != null) { + Bitmap bitmap = BitmapFactory.decodeStream(in, null, null); + if (bitmap != null) { + return LookupCache.cacheImage(context, normalized, bitmap); + } + } + return null; + } + + private static class LookupCachedContactInfo implements CachedContactInfo { + private final ContactInfo info; + + private LookupCachedContactInfo(ContactInfo info) { + this.info = info; + } + + @Override + @NonNull public ContactInfo getContactInfo() { + return info; + } + + @Override + public void setSource(ContactSource.Type sourceType, String name, long directoryId) { + } + + @Override + public void setDirectorySource(String name, long directoryId) { + } + + @Override + public void setLookupKey(String lookupKey) { + } + } +} diff --git a/java/com/android/dialer/lookup/LookupProvider.java b/java/com/android/dialer/lookup/LookupProvider.java new file mode 100644 index 000000000..5e8fc1d99 --- /dev/null +++ b/java/com/android/dialer/lookup/LookupProvider.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.Contacts; +import android.provider.Settings; +import android.util.Log; + +import com.android.dialer.searchfragment.common.Projections; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public class LookupProvider extends ContentProvider { + private static final String TAG = LookupProvider.class.getSimpleName(); + + private static final boolean DEBUG = false; + + public static final String AUTHORITY = "com.android.dialer.lookup"; + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + public static final Uri NEARBY_LOOKUP_URI = + Uri.withAppendedPath(AUTHORITY_URI, "nearby"); + public static final Uri PEOPLE_LOOKUP_URI = + Uri.withAppendedPath(AUTHORITY_URI, "people"); + public static final Uri NEARBY_AND_PEOPLE_LOOKUP_URI = + Uri.withAppendedPath(AUTHORITY_URI, "nearby_and_people"); + public static final Uri IMAGE_CACHE_URI = + Uri.withAppendedPath(AUTHORITY_URI, "images"); + + private static final UriMatcher uriMatcher = new UriMatcher(-1); + private final LinkedList activeTasks = new LinkedList<>(); + + private static final int NEARBY = 0; + private static final int PEOPLE = 1; + private static final int NEARBY_AND_PEOPLE = 2; + private static final int IMAGE = 3; + + static { + uriMatcher.addURI(AUTHORITY, "nearby/*", NEARBY); + uriMatcher.addURI(AUTHORITY, "people/*", PEOPLE); + uriMatcher.addURI(AUTHORITY, "nearby_and_people/*", NEARBY_AND_PEOPLE); + uriMatcher.addURI(AUTHORITY, "images/*", IMAGE); + } + + private class FutureCallable implements Callable { + private final Callable callable; + private volatile FutureTask future; + + public FutureCallable(Callable callable) { + future = null; + this.callable = callable; + } + + public T call() throws Exception { + Log.v(TAG, "Future called for " + Thread.currentThread().getName()); + + T result = callable.call(); + if (future == null) { + return result; + } + + synchronized (activeTasks) { + activeTasks.remove(future); + } + + future = null; + return result; + } + + public void setFuture(FutureTask future) { + this.future = future; + } + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, final String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + if (DEBUG) Log.v(TAG, "query: " + uri); + + Location lastLocation = null; + final int match = uriMatcher.match(uri); + + switch (match) { + case NEARBY: + case NEARBY_AND_PEOPLE: + if (!PermissionsUtil.hasLocationPermissions(getContext())) { + Log.v(TAG, "Location permission is missing, can not determine location."); + } else if (!isLocationEnabled()) { + Log.v(TAG, "Location settings is disabled, can no determine location."); + } else { + lastLocation = getLastLocation(); + } + if (match == NEARBY && lastLocation == null) { + Log.v(TAG, "No location available, ignoring query."); + return null; + } + // fall through to the actual query + + case PEOPLE: + final String filter = Uri.encode(uri.getLastPathSegment()); + String limit = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY); + + int maxResults = -1; + + try { + if (limit != null) { + maxResults = Integer.parseInt(limit); + } + } catch (NumberFormatException e) { + Log.e(TAG, "query: invalid limit parameter: '" + limit + "'"); + } + + final Location finalLastLocation = lastLocation; + final int finalMaxResults = maxResults; + + return execute(new Callable() { + @Override + public Cursor call() { + return handleFilter(match, projection, filter, finalMaxResults, finalLastLocation); + } + }, "FilterThread"); + } + + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("insert() not supported"); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("update() not supported"); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("delete() not supported"); + } + + @Override + public String getType(Uri uri) { + int match = uriMatcher.match(uri); + + switch (match) { + case NEARBY: + case PEOPLE: + case NEARBY_AND_PEOPLE: + return Contacts.CONTENT_ITEM_TYPE; + + default: + return null; + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + switch (uriMatcher.match(uri)) { + case IMAGE: + String number = uri.getLastPathSegment(); + File image = LookupCache.getImagePath(getContext(), number); + + if (mode.equals("r")) { + if (image == null || !image.exists() || !image.isFile()) { + throw new FileNotFoundException("Cached image does not exist"); + } + + return ParcelFileDescriptor.open(image, ParcelFileDescriptor.MODE_READ_ONLY); + } else { + throw new FileNotFoundException("The URI is read only"); + } + + default: + throw new FileNotFoundException("Invalid URI: " + uri); + } + } + + /** + * Check if the location services is on. + * + * @return Whether location services are enabled + */ + private boolean isLocationEnabled() { + try { + int mode = Settings.Secure.getInt(getContext().getContentResolver(), + Settings.Secure.LOCATION_MODE); + + return mode != Settings.Secure.LOCATION_MODE_OFF; + } catch (Settings.SettingNotFoundException e) { + Log.e(TAG, "Failed to get location mode", e); + return false; + } + } + + /** + * Get location from last location query. + * + * @return The last location + */ + private Location getLastLocation() { + LocationManager locationManager = getContext().getSystemService(LocationManager.class); + + try { + locationManager.requestSingleUpdate(new Criteria(), new LocationListener() { + @Override + public void onLocationChanged(Location location) { + } + + @Override + public void onProviderDisabled(String provider) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + }, Looper.getMainLooper()); + + return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Process filter/query and perform the lookup. + * + * @param projection Columns to include in query + * @param filter String to lookup + * @param maxResults Maximum number of results + * @param lastLocation Coordinates of last location query + * @return Cursor for the results + */ + private Cursor handleFilter(int type, String[] projection, String filter, + int maxResults, Location lastLocation) { + if (DEBUG) Log.v(TAG, "handleFilter(" + filter + ")"); + + if (filter == null) { + return null; + } + + try { + filter = URLDecoder.decode(filter, "UTF-8"); + } catch (UnsupportedEncodingException e) { + } + + ArrayList results = null; + if ((type == NEARBY || type == NEARBY_AND_PEOPLE) && lastLocation != null) { + ForwardLookup fl = ForwardLookup.getInstance(getContext()); + List nearby = fl.lookup(getContext(), filter, lastLocation); + if (nearby != null) { + results.addAll(nearby); + } + } + if (type == PEOPLE || type == NEARBY_AND_PEOPLE) { + PeopleLookup pl = PeopleLookup.getInstance(getContext()); + List people = pl.lookup(getContext(), filter); + if (people != null) { + results.addAll(people); + } + } + + if (results.isEmpty()) { + if (DEBUG) Log.v(TAG, "handleFilter(" + filter + "): No results"); + return null; + } + + Cursor cursor = null; + try { + cursor = buildResultCursor(projection, results, maxResults); + if (DEBUG) { + Log.v(TAG, "handleFilter(" + filter + "): " + cursor.getCount() + " matches"); + } + } catch (JSONException e) { + Log.e(TAG, "JSON failure", e); + } + + return cursor; + } + + /** + * Query results. + * + * @param projection Columns to include in query + * @param results Results for the forward lookup + * @param maxResults Maximum number of rows/results to add to cursor + * @return Cursor for forward lookup query results + */ + private Cursor buildResultCursor(String[] projection, List results, int maxResults) + throws JSONException { + // Extended directories always use this projection + MatrixCursor cursor = new MatrixCursor(Projections.DATA_PROJECTION); + + int id = 1; + for (ContactInfo result : results) { + Object[] row = new Object[Projections.DATA_PROJECTION.length]; + + row[Projections.ID] = id; + row[Projections.PHONE_TYPE] = result.type; + row[Projections.PHONE_LABEL] = getAddress(result); + row[Projections.PHONE_NUMBER] = result.number; + row[Projections.DISPLAY_NAME] = result.name; + row[Projections.PHOTO_ID] = 0; + row[Projections.PHOTO_URI] = result.photoUri; + row[Projections.LOOKUP_KEY] = result.lookupUri.getEncodedFragment(); + row[Projections.CONTACT_ID] = id; + + cursor.addRow(row); + + if (maxResults != -1 && cursor.getCount() >= maxResults) { + break; + } + + id++; + } + + return cursor; + } + + private String getAddress(ContactInfo info) { + // Hack: Show city or address for phone label, so they appear in the results list + + String city = null; + String address = null; + + try { + String jsonString = info.lookupUri.getEncodedFragment(); + JSONObject json = new JSONObject(jsonString); + JSONObject contact = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE); + + if (!contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) { + return null; + } + + JSONArray addresses = contact.getJSONArray(StructuredPostal.CONTENT_ITEM_TYPE); + if (addresses.length() == 0) { + return null; + } + + JSONObject addressEntry = addresses.getJSONObject(0); + if (addressEntry.has(StructuredPostal.CITY)) { + city = addressEntry.getString(StructuredPostal.CITY); + } + if (addressEntry.has(StructuredPostal.FORMATTED_ADDRESS)) { + address = addressEntry.getString(StructuredPostal.FORMATTED_ADDRESS); + } + } catch (JSONException e) { + Log.e(TAG, "Failed to get address", e); + } + + if (city != null) { + return city; + } else if (address != null) { + return address; + } else { + return null; + } + } + + /** + * Execute thread that is killed after a specified amount of time. + * + * @param callable The thread + * @param name Name of the thread + * @return Instance of the thread + */ + private T execute(Callable callable, String name) { + FutureCallable futureCallable = new FutureCallable(callable); + FutureTask future = new FutureTask(futureCallable); + futureCallable.setFuture(future); + + synchronized (activeTasks) { + activeTasks.addLast(future); + Log.v(TAG, "Currently running tasks: " + activeTasks.size()); + + while (activeTasks.size() > 8) { + Log.w(TAG, "Too many tasks, canceling one"); + activeTasks.removeFirst().cancel(true); + } + } + + Log.v(TAG, "Starting task " + name); + + new Thread(future, name).start(); + + try { + Log.v(TAG, "Getting future " + name); + return future.get(10000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Log.w(TAG, "Task was interrupted: " + name); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + Log.w(TAG, "Task threw an exception: " + name, e); + } catch (TimeoutException e) { + Log.w(TAG, "Task timed out: " + name); + future.cancel(true); + } catch (CancellationException e) { + Log.w(TAG, "Task was cancelled: " + name); + } + + return null; + } +} diff --git a/java/com/android/dialer/lookup/LookupSettings.java b/java/com/android/dialer/lookup/LookupSettings.java new file mode 100644 index 000000000..2686f689b --- /dev/null +++ b/java/com/android/dialer/lookup/LookupSettings.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.provider.Settings; + +import java.util.List; + +public final class LookupSettings { + private static final String TAG = LookupSettings.class.getSimpleName(); + + /** Forward lookup providers */ + public static final String FLP_GOOGLE = "Google"; + public static final String FLP_OPENSTREETMAP = "OpenStreetMap"; + public static final String FLP_DEFAULT = FLP_GOOGLE; + + /** People lookup providers */ + public static final String PLP_AUSKUNFT = "Auskunft"; + public static final String PLP_DEFAULT = PLP_AUSKUNFT; + + /** Reverse lookup providers */ + public static final String RLP_OPENCNAM = "OpenCnam"; + public static final String RLP_YELLOWPAGES = "YellowPages"; + public static final String RLP_YELLOWPAGES_CA = "YellowPages_CA"; + public static final String RLP_ZABASEARCH = "ZabaSearch"; + public static final String RLP_CYNGN_CHINESE = "CyngnChinese"; + public static final String RLP_DASTELEFONBUCH = "DasTelefonbuch"; + public static final String RLP_AUSKUNFT = "Auskunft"; + public static final String RLP_DEFAULT = RLP_OPENCNAM; + + /** Preferences */ + private static final String SHARED_PREFERENCES_NAME = "lookup_settings"; + private static final String ENABLE_FORWARD_LOOKUP = "enable_forward_lookup"; + private static final String ENABLE_PEOPLE_LOOKUP = "enable_people_lookup"; + private static final String ENABLE_REVERSE_LOOKUP = "enable_reverse_lookup"; + private static final String FORWARD_LOOKUP_PROVIDER = "forward_lookup_provider"; + private static final String PEOPLE_LOOKUP_PROVIDER = "people_lookup_provider"; + private static final String REVERSE_LOOKUP_PROVIDER = "reverse_lookup_provider"; + private static final String OPENCNAM_ACCOUNT_SID = "opencnam_account_sid"; + private static final String OPENCNAM_AUTH_TOKEN = "opencnam_auth_token"; + + private LookupSettings() { + } + + private static SharedPreferences getSharedPreferences(Context context) { + return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + } + + public static boolean isForwardLookupEnabled(Context context) { + return getSharedPreferences(context).getBoolean(ENABLE_FORWARD_LOOKUP, false); + } + + public static void setForwardLookupEnabled(Context context, boolean value) { + getSharedPreferences(context).edit().putBoolean(ENABLE_FORWARD_LOOKUP, value).commit(); + } + + public static boolean isPeopleLookupEnabled(Context context) { + return getSharedPreferences(context).getBoolean(ENABLE_PEOPLE_LOOKUP, false); + } + + public static void setPeopleLookupEnabled(Context context, boolean value) { + getSharedPreferences(context).edit().putBoolean(ENABLE_PEOPLE_LOOKUP, value).commit(); + } + + public static boolean isReverseLookupEnabled(Context context) { + return getSharedPreferences(context).getBoolean(ENABLE_REVERSE_LOOKUP, false); + } + + public static void setReverseLookupEnabled(Context context, boolean value) { + getSharedPreferences(context).edit().putBoolean(ENABLE_REVERSE_LOOKUP, value).commit(); + } + + public static String getForwardLookupProvider(Context context) { + return getSharedPreferences(context).getString(FORWARD_LOOKUP_PROVIDER, FLP_DEFAULT); + } + + public static void setForwardLookupProvider(Context context, String value) { + getSharedPreferences(context).edit().putString(FORWARD_LOOKUP_PROVIDER, value).commit(); + } + + public static String getPeopleLookupProvider(Context context) { + return getSharedPreferences(context).getString(PEOPLE_LOOKUP_PROVIDER, PLP_DEFAULT); + } + + public static void setPeopleLookupProvider(Context context, String value) { + getSharedPreferences(context).edit().putString(PEOPLE_LOOKUP_PROVIDER, value).commit(); + } + + public static String getReverseLookupProvider(Context context) { + return getSharedPreferences(context).getString(REVERSE_LOOKUP_PROVIDER, RLP_DEFAULT); + } + + public static void setReverseLookupProvider(Context context, String value) { + getSharedPreferences(context).edit().putString(REVERSE_LOOKUP_PROVIDER, value).commit(); + } + + public static String getOpenCnamAccountSid(Context context) { + return getSharedPreferences(context).getString(OPENCNAM_ACCOUNT_SID, null); + } + + public static void setOpenCnamAccountSid(Context context, String value) { + getSharedPreferences(context).edit().putString(OPENCNAM_ACCOUNT_SID, value).commit(); + } + + public static String getOpenCnamAuthToken(Context context) { + return getSharedPreferences(context).getString(OPENCNAM_AUTH_TOKEN, null); + } + + public static void setOpenCnamAuthToken(Context context, String value) { + getSharedPreferences(context).edit().putString(OPENCNAM_AUTH_TOKEN, value).commit(); + } +} diff --git a/java/com/android/dialer/lookup/LookupSettingsFragment.java b/java/com/android/dialer/lookup/LookupSettingsFragment.java new file mode 100644 index 000000000..bca92f978 --- /dev/null +++ b/java/com/android/dialer/lookup/LookupSettingsFragment.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.ListPreference; +import android.preference.PreferenceFragment; +import android.preference.SwitchPreference; + +import com.android.dialer.R; + +import java.util.Arrays; + +public class LookupSettingsFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener { + + private static final String KEY_ENABLE_FORWARD_LOOKUP = "enable_forward_lookup"; + private static final String KEY_ENABLE_PEOPLE_LOOKUP = "enable_people_lookup"; + private static final String KEY_ENABLE_REVERSE_LOOKUP = "enable_reverse_lookup"; + private static final String KEY_FORWARD_LOOKUP_PROVIDER = "forward_lookup_provider"; + private static final String KEY_PEOPLE_LOOKUP_PROVIDER = "people_lookup_provider"; + private static final String KEY_REVERSE_LOOKUP_PROVIDER = "reverse_lookup_provider"; + + private SwitchPreference enableForwardLookup; + private SwitchPreference enablePeopleLookup; + private SwitchPreference enableReverseLookup; + private ListPreference forwardLookupProvider; + private ListPreference peopleLookupProvider; + private ListPreference reverseLookupProvider; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.lookup_settings); + + enableForwardLookup = (SwitchPreference) findPreference(KEY_ENABLE_FORWARD_LOOKUP); + enablePeopleLookup = (SwitchPreference) findPreference(KEY_ENABLE_PEOPLE_LOOKUP); + enableReverseLookup = (SwitchPreference) findPreference(KEY_ENABLE_REVERSE_LOOKUP); + + enableForwardLookup.setOnPreferenceChangeListener(this); + enablePeopleLookup.setOnPreferenceChangeListener(this); + enableReverseLookup.setOnPreferenceChangeListener(this); + + forwardLookupProvider = (ListPreference) findPreference(KEY_FORWARD_LOOKUP_PROVIDER); + peopleLookupProvider = (ListPreference) findPreference(KEY_PEOPLE_LOOKUP_PROVIDER); + reverseLookupProvider = (ListPreference) findPreference(KEY_REVERSE_LOOKUP_PROVIDER); + + forwardLookupProvider.setOnPreferenceChangeListener(this); + peopleLookupProvider.setOnPreferenceChangeListener(this); + reverseLookupProvider.setOnPreferenceChangeListener(this); + } + + @Override + public void onResume() { + super.onResume(); + + restoreLookupProviderSwitches(); + restoreLookupProviders(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Context context = getContext(); + + if (preference == enableForwardLookup) { + LookupSettings.setForwardLookupEnabled(context, (Boolean) newValue); + } else if (preference == enablePeopleLookup) { + LookupSettings.setPeopleLookupEnabled(context, (Boolean) newValue); + } else if (preference == enableReverseLookup) { + LookupSettings.setReverseLookupEnabled(context, (Boolean) newValue); + } else if (preference == forwardLookupProvider) { + LookupSettings.setForwardLookupProvider(context, (String) newValue); + } else if (preference == peopleLookupProvider) { + LookupSettings.setPeopleLookupProvider(context, (String) newValue); + } else if (preference == reverseLookupProvider) { + LookupSettings.setReverseLookupProvider(context, (String) newValue); + } + + return true; + } + + private void restoreLookupProviderSwitches() { + Context context = getContext(); + + enableForwardLookup.setChecked(LookupSettings.isForwardLookupEnabled(context)); + enablePeopleLookup.setChecked(LookupSettings.isPeopleLookupEnabled(context)); + enableReverseLookup.setChecked(LookupSettings.isReverseLookupEnabled(context)); + } + + private void restoreLookupProviders() { + Context context = getContext(); + + restoreLookupProvider(forwardLookupProvider, LookupSettings.getForwardLookupProvider(context)); + restoreLookupProvider(peopleLookupProvider, LookupSettings.getPeopleLookupProvider(context)); + restoreLookupProvider(reverseLookupProvider, LookupSettings.getReverseLookupProvider(context)); + } + + private void restoreLookupProvider(ListPreference pref, String provider) { + Context context = getContext(); + + if (pref.getEntries().length < 1) { + pref.setEnabled(false); + return; + } + + if (provider == null) { + pref.setValueIndex(0); + + if (pref == forwardLookupProvider) { + LookupSettings.setForwardLookupProvider(context, pref.getValue()); + } else if (pref == peopleLookupProvider) { + LookupSettings.setPeopleLookupProvider(context, pref.getValue()); + } else if (pref == reverseLookupProvider) { + LookupSettings.setReverseLookupProvider(context, pref.getValue()); + } + } else { + pref.setValue(provider); + } + } +} diff --git a/java/com/android/dialer/lookup/LookupUtils.java b/java/com/android/dialer/lookup/LookupUtils.java new file mode 100644 index 000000000..b6e453392 --- /dev/null +++ b/java/com/android/dialer/lookup/LookupUtils.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.text.Html; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LookupUtils { + private static final String USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0"; + + private static HttpURLConnection prepareHttpConnection(String url, Map headers) + throws IOException { + // open connection + HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); + // set user agent (default value is null) + urlConnection.setRequestProperty("User-Agent", USER_AGENT); + // set all other headers if not null + if (headers != null) { + for (Map.Entry header : headers.entrySet()) { + urlConnection.setRequestProperty(header.getKey(), header.getValue()); + } + } + + return urlConnection; + } + + private static byte[] httpFetch(HttpURLConnection urlConnection) throws IOException { + // query url, read and return buffered response body + // we want to make sure that the connection gets closed here + InputStream is = new BufferedInputStream(urlConnection.getInputStream()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] result = null; + try { + byte[] partial = new byte[4096]; + int read; + while ((read = is.read(partial, 0, 4096)) != -1) { + baos.write(partial, 0, read); + } + result = baos.toByteArray(); + } finally { + is.close(); + baos.close(); + } + return result; + } + + private static Charset determineCharset(HttpURLConnection connection) { + String contentType = connection.getContentType(); + if (contentType != null) { + String[] split = contentType.split(";"); + for (int i = 0; i < split.length; i++) { + String trimmed = split[i].trim(); + if (trimmed.startsWith("charset=")) { + try { + return Charset.forName(trimmed.substring(8)); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + // we don't know about this charset -> ignore + } + } + } + } + return Charset.defaultCharset(); + } + + public static String httpGet(String url, Map headers) throws IOException { + HttpURLConnection connection = prepareHttpConnection(url, headers); + try { + byte[] response = httpFetch(connection); + return new String(response, determineCharset(connection)); + } finally { + connection.disconnect(); + } + } + + public static byte[] httpGetBytes(String url, Map headers) throws IOException { + HttpURLConnection connection = prepareHttpConnection(url, headers); + try { + return httpFetch(connection); + } finally { + connection.disconnect(); + } + } + + public static String httpPost(String url, Map headers, String postData) + throws IOException { + HttpURLConnection connection = prepareHttpConnection(url, headers); + + try { + // write postData to buffered output stream + if (postData != null) { + connection.setDoOutput(true); + BufferedWriter bw = new BufferedWriter( + new OutputStreamWriter(connection.getOutputStream())); + try { + bw.write(postData, 0, postData.length()); + // close connection and re-throw exception + } finally { + bw.close(); + } + } + byte[] response = httpFetch(connection); + return new String(response, determineCharset(connection)); + } finally { + connection.disconnect(); + } + } + + public static List allRegexResults(String input, String regex, boolean dotall) { + if (input == null) { + return null; + } + Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0); + Matcher matcher = pattern.matcher(input); + + List regexResults = new ArrayList(); + while (matcher.find()) { + regexResults.add(matcher.group(1).trim()); + } + return regexResults; + } + + public static String firstRegexResult(String input, String regex, boolean dotall) { + if (input == null) { + return null; + } + Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0); + Matcher m = pattern.matcher(input); + return m.find() ? m.group(1).trim() : null; + } + + public static String fromHtml(String input) { + if (input == null) { + return null; + } + return Html.fromHtml(input).toString().trim(); + } +} diff --git a/java/com/android/dialer/lookup/PeopleLookup.java b/java/com/android/dialer/lookup/PeopleLookup.java new file mode 100644 index 000000000..c7e53dfc3 --- /dev/null +++ b/java/com/android/dialer/lookup/PeopleLookup.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.auskunft.AuskunftPeopleLookup; + +import java.util.List; + +public abstract class PeopleLookup { + private static final String TAG = PeopleLookup.class.getSimpleName(); + + private static PeopleLookup INSTANCE = null; + + public static PeopleLookup getInstance(Context context) { + String provider = LookupSettings.getPeopleLookupProvider(context); + + if (INSTANCE == null || !isInstance(provider)) { + Log.d(TAG, "Chosen people lookup provider: " + provider); + + if (provider.equals(LookupSettings.PLP_AUSKUNFT)) { + INSTANCE = new AuskunftPeopleLookup(context); + } + } + + return INSTANCE; + } + + private static boolean isInstance(String provider) { + if (provider.equals(LookupSettings.PLP_AUSKUNFT) + && INSTANCE instanceof AuskunftPeopleLookup) { + return true; + } else { + return false; + } + } + + public abstract List lookup(Context context, String filter); +} diff --git a/java/com/android/dialer/lookup/ReverseLookup.java b/java/com/android/dialer/lookup/ReverseLookup.java new file mode 100644 index 000000000..a2cc89656 --- /dev/null +++ b/java/com/android/dialer/lookup/ReverseLookup.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.auskunft.AuskunftReverseLookup; +import com.android.dialer.lookup.dastelefonbuch.TelefonbuchReverseLookup; +import com.android.dialer.lookup.opencnam.OpenCnamReverseLookup; +import com.android.dialer.lookup.yellowpages.YellowPagesReverseLookup; +import com.android.dialer.lookup.zabasearch.ZabaSearchReverseLookup; + +import java.io.IOException; + +public abstract class ReverseLookup { + private static final String TAG = ReverseLookup.class.getSimpleName(); + + private static ReverseLookup INSTANCE = null; + + public static ReverseLookup getInstance(Context context) { + String provider = LookupSettings.getReverseLookupProvider(context); + + if (INSTANCE == null || !isInstance(provider)) { + Log.d(TAG, "Chosen reverse lookup provider: " + provider); + + if (provider.equals(LookupSettings.RLP_OPENCNAM)) { + INSTANCE = new OpenCnamReverseLookup(context); + } else if (provider.equals(LookupSettings.RLP_YELLOWPAGES) + || provider.equals(LookupSettings.RLP_YELLOWPAGES_CA)) { + INSTANCE = new YellowPagesReverseLookup(context, provider); + } else if (provider.equals(LookupSettings.RLP_ZABASEARCH)) { + INSTANCE = new ZabaSearchReverseLookup(context); + } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH)) { + INSTANCE = new TelefonbuchReverseLookup(context); + } else if (provider.equals(LookupSettings.RLP_AUSKUNFT)) { + INSTANCE = new AuskunftReverseLookup(context); + } + } + + return INSTANCE; + } + + private static boolean isInstance(String provider) { + if (provider.equals(LookupSettings.RLP_OPENCNAM) + && INSTANCE instanceof OpenCnamReverseLookup) { + return true; + } else if ((provider.equals(LookupSettings.RLP_YELLOWPAGES) + || provider.equals(LookupSettings.RLP_YELLOWPAGES_CA)) + && INSTANCE instanceof YellowPagesReverseLookup) { + return true; + } else if (provider.equals(LookupSettings.RLP_ZABASEARCH) + && INSTANCE instanceof ZabaSearchReverseLookup) { + return true; + } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH) + && INSTANCE instanceof TelefonbuchReverseLookup) { + return true; + } else if (provider.equals(LookupSettings.RLP_AUSKUNFT) + && INSTANCE instanceof AuskunftReverseLookup) { + return true; + } else { + return false; + } + } + + /** + * Lookup image + * + * @param context The application context + * @param uri The image URI + */ + public Bitmap lookupImage(Context context, Uri uri) { + return null; + } + + /** + * Perform phone number lookup. + * + * @param context The application context + * @param normalizedNumber The normalized phone number + * @param formattedNumber The formatted phone number + * @return The phone number info object + */ + public abstract ContactInfo lookupNumber(Context context, + String normalizedNumber, String formattedNumber) throws IOException; +} diff --git a/java/com/android/dialer/lookup/ReverseLookupService.java b/java/com/android/dialer/lookup/ReverseLookupService.java new file mode 100644 index 000000000..02e873b34 --- /dev/null +++ b/java/com/android/dialer/lookup/ReverseLookupService.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; + +import com.android.dialer.location.GeoUtil; +import com.android.dialer.logging.ContactLookupResult; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.incallui.bindings.PhoneNumberService; + +import java.io.IOException; + +public class ReverseLookupService implements PhoneNumberService, Handler.Callback { + private final HandlerThread backgroundThread; + private final Handler backgroundHandler; + private final Handler handler; + private final Context context; + private final TelephonyManager telephonyManager; + + private static final int MSG_LOOKUP = 1; + private static final int MSG_NOTIFY_NUMBER = 2; + + public ReverseLookupService(Context context) { + this.context = context; + telephonyManager = context.getSystemService(TelephonyManager.class); + + // TODO: stop after a while? + backgroundThread = new HandlerThread("ReverseLookup"); + backgroundThread.start(); + + backgroundHandler = new Handler(backgroundThread.getLooper(), this); + handler = new Handler(this); + } + + @Override + public void getPhoneNumberInfo(String phoneNumber, NumberLookupListener numberListener) { + if (!LookupSettings.isReverseLookupEnabled(context)) { + LookupCache.deleteCachedContacts(context); + return; + } + + String countryIso = telephonyManager.getSimCountryIso().toUpperCase(); + String normalizedNumber = phoneNumber != null + ? PhoneNumberUtils.formatNumberToE164(phoneNumber, countryIso) : null; + + // Can't do reverse lookup without a number + if (normalizedNumber == null) { + return; + } + + LookupRequest request = new LookupRequest(); + request.normalizedNumber = normalizedNumber; + request.formattedNumber = PhoneNumberUtils.formatNumber(phoneNumber, + request.normalizedNumber, GeoUtil.getCurrentCountryIso(context)); + request.numberListener = numberListener; + + backgroundHandler.obtainMessage(MSG_LOOKUP, request).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOOKUP: { + // background thread + LookupRequest request = (LookupRequest) msg.obj; + request.contactInfo = doLookup(request); + if (request.contactInfo != null) { + handler.obtainMessage(MSG_NOTIFY_NUMBER, request).sendToTarget(); + } + break; + } + case MSG_NOTIFY_NUMBER: { + // main thread + LookupRequest request = (LookupRequest) msg.obj; + if (request.numberListener != null) { + LookupNumberInfo info = new LookupNumberInfo(request.contactInfo); + request.numberListener.onPhoneNumberInfoComplete(info); + } + break; + } + } + + return true; + } + + private ContactInfo doLookup(LookupRequest request) { + final String number = request.normalizedNumber; + + if (LookupCache.hasCachedContact(context, number)) { + ContactInfo info = LookupCache.getCachedContact(context, number); + if (!ContactInfo.EMPTY.equals(info)) { + return info; + } else if (info != null) { + // If we have an empty cached contact, remove it and redo lookup + LookupCache.deleteCachedContact(context, number); + } + } + + try { + ReverseLookup inst = ReverseLookup.getInstance(context); + ContactInfo info = inst.lookupNumber(context, number, request.formattedNumber); + if (info != null && !info.equals(ContactInfo.EMPTY)) { + LookupCache.cacheContact(context, info); + return info; + } + } catch (IOException e) { + // ignored + } + + return null; + } + + private Bitmap fetchImage(LookupRequest request, Uri uri) { + if (!LookupCache.hasCachedImage(context, request.normalizedNumber)) { + Bitmap bmp = ReverseLookup.getInstance(context).lookupImage(context, uri); + if (bmp != null) { + LookupCache.cacheImage(context, request.normalizedNumber, bmp); + } + } + + return LookupCache.getCachedImage(context, request.normalizedNumber); + } + + private static class LookupRequest { + String normalizedNumber; + String formattedNumber; + NumberLookupListener numberListener; + ContactInfo contactInfo; + } + + private static class LookupNumberInfo implements PhoneNumberInfo { + private final ContactInfo info; + private LookupNumberInfo(ContactInfo info) { + this.info = info; + } + + @Override + public String getDisplayName() { + return info.name; + } + @Override + public String getNumber() { + return info.number; + } + @Override + public int getPhoneType() { + return info.type; + } + @Override + public String getPhoneLabel() { + return info.label; + } + @Override + public String getNormalizedNumber() { + return info.normalizedNumber; + } + @Override + public String getImageUrl() { + return info.photoUri != null ? info.photoUri.toString() : null; + } + @Override + public boolean isBusiness() { + // FIXME + return false; + } + @Override + public String getLookupKey() { + return info.lookupKey; + } + @Override + public ContactLookupResult.Type getLookupSource() { + return ContactLookupResult.Type.REMOTE; + } + } +} diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftApi.java b/java/com/android/dialer/lookup/auskunft/AuskunftApi.java new file mode 100644 index 000000000..5b6b2512c --- /dev/null +++ b/java/com/android/dialer/lookup/auskunft/AuskunftApi.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.auskunft; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.android.dialer.lookup.LookupUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public final class AuskunftApi { + private static final String TAG = AuskunftApi.class.getSimpleName(); + + private static final String PEOPLE_LOOKUP_URL = "https://auskunft.at/suche"; + + private static final String SEARCH_RESULTS_REGEX = + "(?i)(.*?)(.*?) query(String filter) throws IOException { + // build URI + Uri uri = Uri.parse(PEOPLE_LOOKUP_URL) + .buildUpon() + .appendQueryParameter("query", filter) + .build(); + + // get all search entry sections + List entries = LookupUtils.allRegexResults( + LookupUtils.httpGet(uri.toString(), null), SEARCH_RESULTS_REGEX, true); + + // abort lookup if nothing found + if (entries == null || entries.isEmpty()) { + Log.w(TAG, "nothing found"); + return null; + } + + // build response by iterating through the search entries and parsing their HTML data + List infos = new ArrayList(); + for (String entry : entries) { + // parse wanted data and replace null values + String name = replaceNullResult(LookupUtils.firstRegexResult(entry, NAME_REGEX, true)); + String address = replaceNullResult(LookupUtils.firstRegexResult(entry, ADDRESS_REGEX, true)); + String number = replaceNullResult(LookupUtils.firstRegexResult(entry, NUMBER_REGEX, true)); + // ignore entry if name or number is empty (should not occur) + // missing addresses won't be a problem (but do occur) + if (name.isEmpty() || number.isEmpty()) { + continue; + } + + ContactInfo info = new ContactInfo(); + info.name = cleanupResult(name); + info.number = cleanupResult(number); + info.address = cleanupResult(address); + info.url = uri.toString(); + + infos.add(info); + } + return infos; + } + + private static String cleanupResult(String result) { + // get displayable text + result = LookupUtils.fromHtml(result); + // replace newlines with spaces + result = result.replaceAll("\\r|\\n", " "); + // replace multiple spaces with one + result = result.replaceAll("\\s+", " "); + // remove business identifier that is originally not part of the name + result = result.replace(BUSINESS_IDENTIFIER, ""); + // final trimming + result = result.trim(); + + return result; + } + + private static String replaceNullResult(String result) { + return (result == null) ? "" : result; + } + + static class ContactInfo { + String name; + String number; + String url; + String address; + }; +} diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java b/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java new file mode 100644 index 000000000..6feb1a58f --- /dev/null +++ b/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.auskunft; + +import android.content.Context; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.PeopleLookup; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class AuskunftPeopleLookup extends PeopleLookup { + private static final String TAG = AuskunftPeopleLookup.class.getSimpleName(); + + public AuskunftPeopleLookup(Context context) { + } + + @Override + public List lookup(Context context, String filter) { + try { + List infos = AuskunftApi.query(filter); + if (infos != null) { + List result = new ArrayList<>(); + for (AuskunftApi.ContactInfo info : infos) { + result.add(ContactBuilder.forPeopleLookup(info.number) + .setName(ContactBuilder.Name.createDisplayName(info.name)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.number)) + .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.url)) + .addAddress(ContactBuilder.Address.createFormattedHome(info.address)) + .build()); + } + return result; + } + } catch (IOException e) { + Log.e(TAG, "People lookup failed", e); + } + return null; + } +} diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java b/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java new file mode 100644 index 000000000..6b6f41543 --- /dev/null +++ b/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.auskunft; + +import android.content.Context; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ReverseLookup; + +import java.io.IOException; +import java.util.List; + +public class AuskunftReverseLookup extends ReverseLookup { + public AuskunftReverseLookup(Context context) { + } + + @Override + public ContactInfo lookupNumber(Context context, String normalizedNumber, + String formattedNumber) throws IOException { + // only Austrian numbers are supported + if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+43")) { + return null; + } + + // query the API and return null if nothing found or general error + List infos = AuskunftApi.query(normalizedNumber); + AuskunftApi.ContactInfo info = infos != null && !infos.isEmpty() ? infos.get(0) : null; + if (info == null) { + return null; + } + + return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber) + .setName(ContactBuilder.Name.createDisplayName(info.name)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.number)) + .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.url)) + .addAddress(ContactBuilder.Address.createFormattedHome(info.address)) + .build(); + } +} diff --git a/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java new file mode 100644 index 000000000..ab1eb4085 --- /dev/null +++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2014 Danny Baumann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.dastelefonbuch; + +import android.content.Context; +import android.net.Uri; + +import com.android.dialer.lookup.LookupUtils; + +import java.io.IOException; + +public class TelefonbuchApi { + private static final String TAG = TelefonbuchApi.class.getSimpleName(); + + private static final String REVERSE_LOOKUP_URL = + "https://www.dastelefonbuch.de/?s=a20000" + + "&cmd=search&sort_ok=0&sp=55&vert_ok=0&aktion=23"; + + private static String NAME_REGEX ="\\s*\n?(.*?)\n?\\s*"; + private static String NUMBER_REGEX = ".*(.*?)
"; + private static String ADDRESS_REGEX = "\n?(.*?)
"; + + private TelefonbuchApi() { + } + + public static ContactInfo reverseLookup(Context context, String number) throws IOException { + Uri uri = Uri.parse(REVERSE_LOOKUP_URL) + .buildUpon() + .appendQueryParameter("kw", number) + .build(); + // Cut out everything we're not interested in (scripts etc.) to + // speed up the subsequent matching. + String output = LookupUtils.firstRegexResult( + LookupUtils.httpGet(uri.toString(), null), ": Treffer(.*)Ende Treffer", true); + + String name = parseValue(output, NAME_REGEX, true, false); + if (name == null) { + return null; + } + + String phoneNumber = parseValue(output, NUMBER_REGEX, false, true); + String address = parseValue(output, ADDRESS_REGEX, true, true); + + ContactInfo info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = phoneNumber != null ? phoneNumber : number; + info.website = uri.toString(); + + return info; + } + + private static String parseValue(String output, String regex, + boolean dotall, boolean removeSpans) { + String result = LookupUtils.firstRegexResult(output, regex, dotall); + if (result != null && removeSpans) { + // completely remove hidden spans (including contents) ... + result = result.replaceAll("", ""); + // ... and remove span wrappers around data content + result = result.replaceAll("", ""); + } + return LookupUtils.fromHtml(result); + } + + public static class ContactInfo { + String name; + String address; + String formattedNumber; + String website; + } +} diff --git a/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java new file mode 100644 index 000000000..cd89499d1 --- /dev/null +++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 Danny Baumann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.dastelefonbuch; + +import android.content.Context; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ReverseLookup; + +import java.io.IOException; + +public class TelefonbuchReverseLookup extends ReverseLookup { + private static final String TAG = TelefonbuchReverseLookup.class.getSimpleName(); + + public TelefonbuchReverseLookup(Context context) { + } + + /** + * Perform phone number lookup. + * + * @param context The application context + * @param normalizedNumber The normalized phone number + * @param formattedNumber The formatted phone number + * @return The phone number info object + */ + @Override + public ContactInfo lookupNumber(Context context, + String normalizedNumber, String formattedNumber) throws IOException { + if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+49")) { + // Das Telefonbuch only supports German numbers + return null; + } + + TelefonbuchApi.ContactInfo info = TelefonbuchApi.reverseLookup(context, normalizedNumber); + if (info == null) { + return null; + } + + return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber) + .setName(ContactBuilder.Name.createDisplayName(info.name)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber)) + .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website)) + .addAddress(ContactBuilder.Address.createFormattedHome(info.address)) + .build(); + } +} diff --git a/java/com/android/dialer/lookup/google/GoogleForwardLookup.java b/java/com/android/dialer/lookup/google/GoogleForwardLookup.java new file mode 100644 index 000000000..bae11c4ba --- /dev/null +++ b/java/com/android/dialer/lookup/google/GoogleForwardLookup.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.google; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.Location; +import android.net.Uri; +import android.os.Build; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.text.Html; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ForwardLookup; +import com.android.dialer.lookup.LookupUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +public class GoogleForwardLookup extends ForwardLookup { + private static final String TAG = GoogleForwardLookup.class.getSimpleName(); + + private static final boolean DEBUG = false; + + private static final String QUERY_FILTER = "q"; + private static final String QUERY_LANGUAGE = "hl"; + private static final String QUERY_LOCATION = "sll"; + private static final String QUERY_RADIUS = "radius"; + private static final String QUERY_RANDOM = "gs_gbg"; + + private static final String RESULT_ADDRESS = "a"; + private static final String RESULT_NUMBER = "b"; + private static final String RESULT_DISTANCE = "c"; + private static final String RESULT_PHOTO_URI = "d"; + private static final String RESULT_WEBSITE = "f"; + private static final String RESULT_CITY = "g"; + + /** Base for the query URL */ + private static final String LOOKUP_URL = "https://www.google.com/complete/search?gs_ri=dialer"; + + /** Minimum query length + * (default for dialer_nearby_places_min_query_len) */ + private static final int MIN_QUERY_LEN = 2; + + /** Maximum query length + * (default for dialer_nearby_places_max_query_len) */ + private static final int MAX_QUERY_LEN = 50; + + /** Radius (in miles) + * (default for dialer_nearby_places_directory_radius_meters) */ + private static final int RADIUS = 1000; + + /** User agent string */ + private final String userAgent; + + public GoogleForwardLookup(Context context) { + StringBuilder sb = new StringBuilder("GoogleDialer "); + try { + sb.append(context.getPackageManager().getPackageInfo( + context.getPackageName(), 0).versionName); + sb.append(" "); + sb.append(Build.FINGERPRINT); + } catch (PackageManager.NameNotFoundException e) { + sb.setLength(0); + } + userAgent = sb.toString(); + } + + @Override + public List lookup(Context context, String filter, Location lastLocation) { + int length = filter.length(); + + if (length >= MIN_QUERY_LEN) { + if (length > MAX_QUERY_LEN) { + filter = filter.substring(0, MAX_QUERY_LEN); + } + + try { + Uri.Builder builder = Uri.parse(LOOKUP_URL).buildUpon(); + + // Query string + builder.appendQueryParameter(QUERY_FILTER, filter); + + // Language + builder.appendQueryParameter(QUERY_LANGUAGE, + context.getResources().getConfiguration().locale.getLanguage()); + + // Location (latitude and longitude) + builder.appendQueryParameter(QUERY_LOCATION, + String.format("%f,%f", lastLocation.getLatitude(), lastLocation.getLongitude())); + + // Radius distance + builder.appendQueryParameter(QUERY_RADIUS, Integer.toString(RADIUS)); + + // Random string (not really required) + builder.appendQueryParameter(QUERY_RANDOM, getRandomNoiseString()); + + Map headers = new HashMap<>(); + headers.put("User-Agent", userAgent); + JSONArray results = new JSONArray( + LookupUtils.httpGet(builder.build().toString(), headers)); + + if (DEBUG) Log.v(TAG, "Results: " + results); + + return getEntries(results); + } catch (IOException e) { + Log.e(TAG, "Failed to execute query", e); + } catch (JSONException e) { + Log.e(TAG, "JSON error", e); + } + } + + return null; + } + + /** + * Parse JSON results and return them as an array of ContactInfo + * + * @param results The JSON results returned from the server + * @return Array of ContactInfo containing the result information + */ + private List getEntries(JSONArray results) throws JSONException { + ArrayList details = new ArrayList<>(); + JSONArray entries = results.getJSONArray(1); + + for (int i = 0; i < entries.length(); i++) { + try { + JSONArray entry = entries.getJSONArray(i); + + String displayName = decodeHtml(entry.getString(0)); + + JSONObject params = entry.getJSONObject(3); + + String phoneNumber = decodeHtml(params.getString(RESULT_NUMBER)); + + String address = decodeHtml(params.getString(RESULT_ADDRESS)); + String city = decodeHtml(params.getString(RESULT_CITY)); + + String profileUrl = params.optString(RESULT_WEBSITE, null); + String photoUri = params.optString(RESULT_PHOTO_URI, null); + + ContactBuilder.Address a = new ContactBuilder.Address(); + a.formattedAddress = address; + a.city = city; + a.type = StructuredPostal.TYPE_WORK; + + details.add(ContactBuilder.forForwardLookup(phoneNumber) + .setName(ContactBuilder.Name.createDisplayName(displayName)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber)) + .addWebsite(ContactBuilder.WebsiteUrl.createProfile(profileUrl)) + .addAddress(a) + .setPhotoUri(photoUri != null ? photoUri : ContactBuilder.PHOTO_URI_BUSINESS) + .build()); + } catch (JSONException e) { + Log.e(TAG, "Skipping the suggestions at index " + i, e); + } + } + + return details; + } + + /** + * Generate a random string of alphanumeric characters of length [4, 36) + * + * @return Random alphanumeric string + */ + private String getRandomNoiseString() { + StringBuilder garbage = new StringBuilder(); + int length = getRandomInteger(32) + 4; + + for (int i = 0; i < length; i++) { + int asciiCode; + + if (Math.random() >= 0.3) { + if (Math.random() <= 0.5) { + // Lowercase letters + asciiCode = getRandomInteger(26) + 97; + } else { + // Uppercase letters + asciiCode = getRandomInteger(26) + 65; + } + } else { + // Numbers + asciiCode = getRandomInteger(10) + 48; + } + + garbage.append(Character.toString((char) asciiCode)); + } + + return garbage.toString(); + } + + /** + * Generate number in the range [0, max). + * + * @param max Upper limit (non-inclusive) + * @return Random number inside [0, max) + */ + private int getRandomInteger(int max) { + return (int) Math.floor(Math.random() * max); + } + + /** + * Convert HTML to unformatted plain text. + * + * @param s HTML content + * @return Unformatted plain text + */ + private String decodeHtml(String s) { + return Html.fromHtml(s).toString(); + } +} diff --git a/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java new file mode 100644 index 000000000..458dbc136 --- /dev/null +++ b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.opencnam; + +import android.content.Context; +import android.net.Uri; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.LookupSettings; +import com.android.dialer.lookup.LookupUtils; +import com.android.dialer.lookup.ReverseLookup; + +import java.io.IOException; + +public class OpenCnamReverseLookup extends ReverseLookup { + private static final String TAG = OpenCnamReverseLookup.class.getSimpleName(); + + private static final boolean DEBUG = false; + + private static final String LOOKUP_URL = "https://api.opencnam.com/v2/phone/"; + + /** Query parameters for paid accounts */ + private static final String ACCOUNT_SID = "account_sid"; + private static final String AUTH_TOKEN = "auth_token"; + + public OpenCnamReverseLookup(Context context) { + } + + /** + * Perform phone number lookup. + * + * @param context The application context + * @param normalizedNumber The normalized phone number + * @param formattedNumber The formatted phone number + * @return The phone number info object + */ + @Override + public ContactInfo lookupNumber(Context context, + String normalizedNumber, String formattedNumber) throws IOException { + if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+1")) { + // Any non-US number will return "We currently accept only US numbers" + return null; + } + + String displayName = httpGetRequest(context, normalizedNumber); + if (DEBUG) Log.d(TAG, "Reverse lookup returned name: " + displayName); + + // Check displayName. The free tier of the service will return the + // following for some numbers: + // "CNAM for phone "NORMALIZED" is currently unavailable for Hobbyist Tier users." + + if (displayName.contains("Hobbyist Tier")) { + return null; + } + + String number = formattedNumber != null ? formattedNumber : normalizedNumber; + + return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber) + .setName(ContactBuilder.Name.createDisplayName(displayName)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(number)) + .setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS) + .build(); + } + + private String httpGetRequest(Context context, String number) throws IOException { + Uri.Builder builder = Uri.parse(LOOKUP_URL + number).buildUpon(); + + // Paid account + String accountSid = LookupSettings.getOpenCnamAccountSid(context); + String authToken = LookupSettings.getOpenCnamAuthToken(context); + + if (!TextUtils.isEmpty(accountSid) && !TextUtils.isEmpty(authToken)) { + Log.d(TAG, "Using paid account"); + + builder.appendQueryParameter(ACCOUNT_SID, accountSid); + builder.appendQueryParameter(AUTH_TOKEN, authToken); + } + + return LookupUtils.httpGet(builder.build().toString(), null); + } +} diff --git a/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java b/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java new file mode 100644 index 000000000..4482e6043 --- /dev/null +++ b/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014 The OmniROM Project + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Partially based on OmniROM's implementation + +package com.android.dialer.lookup.openstreetmap; + +import android.content.Context; +import android.location.Location; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ForwardLookup; +import com.android.dialer.lookup.LookupUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class OpenStreetMapForwardLookup extends ForwardLookup { + private static final String TAG = OpenStreetMapForwardLookup.class.getSimpleName(); + + /** Search within radius (meters) */ + private static final int RADIUS = 30000; + + /** Query URL */ + private static final String LOOKUP_URL = "https://overpass-api.de/api/interpreter"; + private static final String LOOKUP_QUERY = + "[out:json];node[name~\"%s\"][phone](around:%d,%f,%f);out body;"; + + private static final String RESULT_ELEMENTS = "elements"; + private static final String RESULT_TAGS = "tags"; + private static final String TAG_NAME = "name"; + private static final String TAG_PHONE = "phone"; + private static final String TAG_HOUSENUMBER = "addr:housenumber"; + private static final String TAG_STREET = "addr:street"; + private static final String TAG_CITY = "addr:city"; + private static final String TAG_POSTCODE = "addr:postcode"; + private static final String TAG_WEBSITE = "website"; + + public OpenStreetMapForwardLookup(Context context) { + } + + @Override + public List lookup(Context context, String filter, Location lastLocation) { + // The OSM API doesn't support case-insentive searches, but does + // support regular expressions. + String regex = ""; + for (int i = 0; i < filter.length(); i++) { + char c = filter.charAt(i); + regex += "[" + Character.toUpperCase(c) + Character.toLowerCase(c) + "]"; + } + + String request = String.format(Locale.ENGLISH, LOOKUP_QUERY, regex, + RADIUS, lastLocation.getLatitude(), lastLocation.getLongitude()); + + try { + return getEntries(new JSONObject(LookupUtils.httpPost(LOOKUP_URL, null, request))); + } catch (IOException e) { + Log.e(TAG, "Failed to execute query", e); + } catch (JSONException e) { + Log.e(TAG, "JSON error", e); + } + + return null; + } + + private List getEntries(JSONObject results) throws JSONException { + ArrayList details = new ArrayList<>(); + JSONArray elements = results.getJSONArray(RESULT_ELEMENTS); + + for (int i = 0; i < elements.length(); i++) { + try { + JSONObject element = elements.getJSONObject(i); + JSONObject tags = element.getJSONObject(RESULT_TAGS); + + String displayName = tags.getString(TAG_NAME); + String phoneNumber = tags.getString(TAG_PHONE); + + // Take the first number if there are multiple + if (phoneNumber.contains(";")) { + phoneNumber = phoneNumber.split(";")[0]; + phoneNumber = phoneNumber.trim(); + } + + // The address is split + String addressHouseNumber = tags.optString(TAG_HOUSENUMBER, null); + String addressStreet = tags.optString(TAG_STREET, null); + String addressCity = tags.optString(TAG_CITY, null); + String addressPostCode = tags.optString(TAG_POSTCODE, null); + + String address = String.format("%s %s, %s %s", + addressHouseNumber != null ? addressHouseNumber : "", + addressStreet != null ? addressStreet : "", + addressCity != null ? addressCity : "", + addressPostCode != null ? addressPostCode : ""); + + address = address.trim().replaceAll("\\s+", " "); + if (address.isEmpty()) { + address = null; + } + + ContactBuilder builder = ContactBuilder.forForwardLookup(phoneNumber) + .setName(ContactBuilder.Name.createDisplayName(displayName)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber)) + .setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS); + + if (address != null) { + ContactBuilder.Address a = new ContactBuilder.Address(); + a.formattedAddress = address; + a.city = addressCity; + a.street = addressStreet; + a.postCode = addressPostCode; + a.type = StructuredPostal.TYPE_WORK; + builder.addAddress(a); + } + + String website = tags.optString(TAG_WEBSITE, null); + if (website != null) { + ContactBuilder.WebsiteUrl w = new ContactBuilder.WebsiteUrl(); + w.url = website; + w.type = Website.TYPE_HOMEPAGE; + builder.addWebsite(w); + } + + details.add(builder.build()); + } catch (JSONException e) { + Log.e(TAG, "Skipping the suggestions at index " + i, e); + } + } + + return details; + } +} diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png new file mode 100644 index 000000000..f0bbe7345 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png new file mode 100644 index 000000000..f70e8e711 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png new file mode 100644 index 000000000..6409ab185 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png new file mode 100644 index 000000000..7c92a6030 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png new file mode 100644 index 000000000..97b982257 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png new file mode 100644 index 000000000..43029bd81 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/values/cm_arrays.xml b/java/com/android/dialer/lookup/res/values/cm_arrays.xml new file mode 100644 index 000000000..a566727e3 --- /dev/null +++ b/java/com/android/dialer/lookup/res/values/cm_arrays.xml @@ -0,0 +1,53 @@ + + + + + Google + OpenStreetMap + + + + Google + OpenStreetMap + + + + Auskunft + + + + Auskunft (AT) + + + + Auskunft + DasTelefonbuch + OpenCnam + YellowPages + YellowPages_CA + ZabaSearch + + + + Auskunft (AT) + Das Telefonbuch (DE) + OpenCnam (US) + YellowPages (US) + YellowPages (CA) + ZabaSearch (US) + + diff --git a/java/com/android/dialer/lookup/res/values/cm_strings.xml b/java/com/android/dialer/lookup/res/values/cm_strings.xml new file mode 100644 index 000000000..fed7c0067 --- /dev/null +++ b/java/com/android/dialer/lookup/res/values/cm_strings.xml @@ -0,0 +1,36 @@ + + + + + Nearby places + People + + + Phone number lookup + Forward lookup + Show nearby places when searching in the dialer + People lookup + Show online results for people when searching in the dialer + Reverse lookup + Look up information about the person or place for unknown numbers on incoming calls + Forward lookup provider + People lookup provider + Reverse lookup provider + + + Lookups may send queries over a secure protocol (https) to remote websites to gather information. The query may include the other party\'s phone number or the search query + diff --git a/java/com/android/dialer/lookup/res/xml/lookup_settings.xml b/java/com/android/dialer/lookup/res/xml/lookup_settings.xml new file mode 100644 index 000000000..006345f52 --- /dev/null +++ b/java/com/android/dialer/lookup/res/xml/lookup_settings.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java b/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java new file mode 100644 index 000000000..30d5aafd3 --- /dev/null +++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.yellowpages; + +import android.content.Context; +import android.text.TextUtils; + +import com.android.dialer.lookup.LookupUtils; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class YellowPagesApi { + private static final String TAG = YellowPagesApi.class.getSimpleName(); + + static final String LOOKUP_URL_UNITED_STATES = + "https://www.yellowpages.com/phone?phone_search_terms="; + static final String LOOKUP_URL_CANADA = + "https://www.yellowpages.ca/search/si/1/"; + + private final String number; + private String output = null; + private ContactInfo info = null; + private final String lookupUrl; + + public YellowPagesApi(String number, String lookupUrl) { + this.number = number; + this.lookupUrl = lookupUrl; + } + + private void fetchPage() throws IOException { + output = LookupUtils.httpGet(lookupUrl + number, null); + } + + private String getPhotoUrl(String website) throws IOException { + String output = LookupUtils.httpGet(website, null); + String galleryRef = LookupUtils.firstRegexResult(output, + "href=\"([^\"]+gallery\\?lid=[^\"]+)\"", true); + if (galleryRef == null) { + return null; + } + + // Get first image + return LookupUtils.firstRegexResult( + LookupUtils.httpGet("https://www.yellowpages.com" + galleryRef, null), + "\"type\":\"image\",\"src\":\"([^\"]+)\"", true); + } + + private String[] parseNameWebsiteUnitedStates() { + Pattern regexNameAndWebsite = Pattern.compile( + "]+?)\"[^>]+?class=\"url[^>]+?>([^<]+)", + Pattern.DOTALL); + String name = null; + String website = null; + + Matcher m = regexNameAndWebsite.matcher(output); + if (m.find()) { + website = m.group(1).trim(); + name = m.group(2).trim(); + } + + return new String[] { name, website }; + } + + private String[] parseNameWebsiteCanada() { + Pattern regexNameAndWebsite = Pattern.compile( + "class=\"ypgListingTitleLink utagLink\".*?href=\"(.*?)\">" + + "(.*?)", + Pattern.DOTALL); + String name = null; + String website = null; + + Matcher m = regexNameAndWebsite.matcher(output); + if (m.find()) { + website = m.group(1).trim(); + name = LookupUtils.fromHtml(m.group(2).trim()); + } + + if (website != null) { + website = "https://www.yellowpages.ca" + website; + } + + return new String[] { name, website }; + } + + private String parseNumberUnitedStates() { + return LookupUtils.firstRegexResult(output, + "business-phone.*?>\n*([^\n<]+)\n*<", true); + } + + private String parseNumberCanada() { + return LookupUtils.firstRegexResult(output, + "(.*?)", true); + } + + private String parseAddressUnitedStates() { + String addressStreet = LookupUtils.firstRegexResult(output, + "street-address.*?>\n*([^\n<]+)\n*<", true); + if (addressStreet != null && addressStreet.endsWith(",")) { + addressStreet = addressStreet.substring(0, addressStreet.length() - 1); + } + + String addressCity = LookupUtils.firstRegexResult(output, + "locality.*?>\n*([^\n<]+)\n*<", true); + String addressState = LookupUtils.firstRegexResult(output, + "region.*?>\n*([^\n<]+)\n*<", true); + String addressZip = LookupUtils.firstRegexResult(output, + "postal-code.*?>\n*([^\n<]+)\n*<", true); + + StringBuilder sb = new StringBuilder(); + + if (!TextUtils.isEmpty(addressStreet)) { + sb.append(addressStreet); + } + if (!TextUtils.isEmpty(addressCity)) { + sb.append(", "); + sb.append(addressCity); + } + if (!TextUtils.isEmpty(addressState)) { + sb.append(", "); + sb.append(addressState); + } + if (!TextUtils.isEmpty(addressZip)) { + sb.append(", "); + sb.append(addressZip); + } + + String address = sb.toString(); + return address.isEmpty() ? null : address; + } + + private String parseAddressCanada() { + String address = LookupUtils.firstRegexResult(output, + "(.*?)", true); + return LookupUtils.fromHtml(address); + } + + private void buildContactInfo() throws IOException { + Matcher m; + + String name = null; + String website = null; + String phoneNumber = null; + String address = null; + String photoUrl = null; + + if (lookupUrl.equals(LOOKUP_URL_UNITED_STATES)) { + String[] ret = parseNameWebsiteUnitedStates(); + name = ret[0]; + website = ret[1]; + phoneNumber = parseNumberUnitedStates(); + address = parseAddressUnitedStates(); + if (website != null) { + photoUrl = getPhotoUrl(website); + } + } else { + String[] ret = parseNameWebsiteCanada(); + name = ret[0]; + website = ret[1]; + phoneNumber = parseNumberCanada(); + address = parseAddressCanada(); + // AFAIK, Canada's YellowPages doesn't have photos + } + + info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = phoneNumber != null ? phoneNumber : number; + info.website = website; + info.photoUrl = photoUrl; + } + + public ContactInfo getContactInfo() throws IOException { + if (info == null) { + fetchPage(); + buildContactInfo(); + } + + return info; + } + + public static class ContactInfo { + String name; + String address; + String formattedNumber; + String website; + String photoUrl; + } +} diff --git a/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java b/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java new file mode 100644 index 000000000..5638df64c --- /dev/null +++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.yellowpages; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.LookupSettings; +import com.android.dialer.lookup.LookupUtils; +import com.android.dialer.lookup.ReverseLookup; + +import java.io.FileNotFoundException; +import java.io.IOException; + +public class YellowPagesReverseLookup extends ReverseLookup { + private static final String TAG = YellowPagesReverseLookup.class.getSimpleName(); + + private final String type; + + public YellowPagesReverseLookup(Context context, String type) { + this.type = type; + } + + /** + * Lookup image + * + * @param context The application context + * @param uri The image URI + */ + @Override + public Bitmap lookupImage(Context context, Uri uri) { + if (uri == null) { + throw new NullPointerException("URI is null"); + } + + Log.e(TAG, "Fetching " + uri); + + String scheme = uri.getScheme(); + + if (scheme.startsWith("http")) { + try { + byte[] response = LookupUtils.httpGetBytes(uri.toString(), null); + return BitmapFactory.decodeByteArray(response, 0, response.length); + } catch (IOException e) { + Log.e(TAG, "Failed to retrieve image", e); + } + } else if (scheme.equals(ContentResolver.SCHEME_CONTENT)) { + try { + ContentResolver cr = context.getContentResolver(); + return BitmapFactory.decodeStream(cr.openInputStream(uri)); + } catch (FileNotFoundException e) { + Log.e(TAG, "Failed to retrieve image", e); + } + } + + return null; + } + + /** + * Perform phone number lookup. + * + * @param context The application context + * @param normalizedNumber The normalized phone number + * @param formattedNumber The formatted phone number + * @return The phone number info object + */ + @Override + public ContactInfo lookupNumber(Context context, + String normalizedNumber, String formattedNumber) throws IOException { + String lookupUrl = type.equals(LookupSettings.RLP_YELLOWPAGES_CA) + ? YellowPagesApi.LOOKUP_URL_CANADA : YellowPagesApi.LOOKUP_URL_UNITED_STATES; + YellowPagesApi ypa = new YellowPagesApi(normalizedNumber, lookupUrl); + YellowPagesApi.ContactInfo info = ypa.getContactInfo(); + if (info.name == null) { + return null; + } + + ContactBuilder builder = ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber) + .setName(ContactBuilder.Name.createDisplayName(info.name)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber)) + .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website)); + + if (info.address != null) { + ContactBuilder.Address a = new ContactBuilder.Address(); + a.formattedAddress = info.address; + a.type = StructuredPostal.TYPE_WORK; + builder.addAddress(a); + } + + if (info.photoUrl != null) { + builder.setPhotoUri(info.photoUrl); + } else { + builder.setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS); + } + + return builder.build(); + } +} diff --git a/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java b/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java new file mode 100644 index 000000000..6118740bc --- /dev/null +++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.zabasearch; + +import android.text.TextUtils; + +import com.android.dialer.lookup.LookupUtils; + +import java.io.IOException; + +public class ZabaSearchApi { + private static final String TAG = ZabaSearchApi.class.getSimpleName(); + + private static final String LOOKUP_URL = "https://www.zabasearch.com/phone/"; + + private final String number; + public String output = null; + private ContactInfo info = null; + + public ZabaSearchApi(String number) { + this.number = number; + } + + private void fetchPage() throws IOException { + output = LookupUtils.httpGet(LOOKUP_URL + number, null); + } + + private void buildContactInfo() { + // Name + String name = LookupUtils.firstRegexResult(output, + "itemprop=\"?name\"?>([^<]+)<", true); + // Formatted phone number + String phoneNumber = LookupUtils.firstRegexResult(output, + "itemprop=\"?telephone\"?>([^<]+)<", true); + // Address + String addressStreet = LookupUtils.firstRegexResult(output, + "itemprop=\"?streetAddress\"?>([^<]+?)( )*<", true); + String addressCity = LookupUtils.firstRegexResult(output, + "itemprop=\"?addressLocality\"?>([^<]+)<", true); + String addressState = LookupUtils.firstRegexResult(output, + "itemprop=\"?addressRegion\"?>([^<]+)<", true); + String addressZip = LookupUtils.firstRegexResult(output, + "itemprop=\"?postalCode\"?>([^<]+)<", true); + + StringBuilder sb = new StringBuilder(); + + if (!TextUtils.isEmpty(addressStreet)) { + sb.append(addressStreet); + } + if (!TextUtils.isEmpty(addressCity)) { + sb.append(", "); + sb.append(addressCity); + } + if (!TextUtils.isEmpty(addressState)) { + sb.append(", "); + sb.append(addressState); + } + if (!TextUtils.isEmpty(addressZip)) { + sb.append(", "); + sb.append(addressZip); + } + + String address = sb.toString(); + if (address.isEmpty()) { + address = null; + } + + info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = number; + info.website = LOOKUP_URL + info.formattedNumber; + } + + public ContactInfo getContactInfo() throws IOException { + if (info == null) { + fetchPage(); + buildContactInfo(); + } + + return info; + } + + public static class ContactInfo { + String name; + String address; + String formattedNumber; + String website; + } +} diff --git a/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java b/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java new file mode 100644 index 000000000..5c6608bfe --- /dev/null +++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.zabasearch; + +import android.content.Context; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ReverseLookup; + +import java.io.IOException; + +public class ZabaSearchReverseLookup extends ReverseLookup { + private static final String TAG = ZabaSearchReverseLookup.class.getSimpleName(); + + public ZabaSearchReverseLookup(Context context) { + } + + /** + * Perform phone number lookup. + * + * @param context The application context + * @param normalizedNumber The normalized phone number + * @param formattedNumber The formatted phone number + * @return The phone number info object + */ + @Override + public ContactInfo lookupNumber(Context context, + String normalizedNumber, String formattedNumber) throws IOException { + ZabaSearchApi zsa = new ZabaSearchApi(normalizedNumber); + ZabaSearchApi.ContactInfo info = zsa.getContactInfo(); + if (info.name == null) { + return null; + } + + return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber) + .setName(ContactBuilder.Name.createDisplayName(info.name)) + .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber)) + .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website)) + .addAddress(ContactBuilder.Address.createFormattedHome(info.address)) + .build(); + } +} -- cgit v1.2.3