summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXiao-Long Chen <chenxiaolong@cxl.epac.to>2016-09-12 09:34:02 +0200
committerMichael Bestas <mkbestas@lineageos.org>2019-12-11 20:08:21 +0200
commit8b7ecf2e3cd2232b38047a7f15fad90814b5848c (patch)
tree1d861b8d35f115d05c0e47031c3ff4a38278f9fc
parent0fcaf0b24bf99654e4573d1870f0dc52db23515c (diff)
downloadandroid_packages_apps_Dialer-8b7ecf2e3cd2232b38047a7f15fad90814b5848c.tar.gz
android_packages_apps_Dialer-8b7ecf2e3cd2232b38047a7f15fad90814b5848c.tar.bz2
android_packages_apps_Dialer-8b7ecf2e3cd2232b38047a7f15fad90814b5848c.zip
Re-add dialer lookup.
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to> 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 <joey@lineageos.org> 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 <joey@lineageos.org> Change-Id: I4ff90a678618fa8c7b5970dff3dd246b0c87135c
-rw-r--r--Android.mk1
-rw-r--r--java/com/android/dialer/app/settings/DialerSettingsActivity.java6
-rw-r--r--java/com/android/dialer/binary/aosp/AospDialerApplication.java60
-rw-r--r--java/com/android/dialer/lookup/AndroidManifest.xml31
-rw-r--r--java/com/android/dialer/lookup/ContactBuilder.java475
-rw-r--r--java/com/android/dialer/lookup/DirectoryId.java33
-rw-r--r--java/com/android/dialer/lookup/ForwardLookup.java63
-rw-r--r--java/com/android/dialer/lookup/LookupCache.java296
-rw-r--r--java/com/android/dialer/lookup/LookupCacheService.java111
-rw-r--r--java/com/android/dialer/lookup/LookupProvider.java462
-rw-r--r--java/com/android/dialer/lookup/LookupSettings.java103
-rw-r--r--java/com/android/dialer/lookup/LookupSettingsFragment.java141
-rw-r--r--java/com/android/dialer/lookup/LookupUtils.java168
-rw-r--r--java/com/android/dialer/lookup/PeopleLookup.java56
-rw-r--r--java/com/android/dialer/lookup/ReverseLookup.java103
-rw-r--r--java/com/android/dialer/lookup/ReverseLookupService.java197
-rw-r--r--java/com/android/dialer/lookup/auskunft/AuskunftApi.java114
-rw-r--r--java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java57
-rw-r--r--java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java54
-rw-r--r--java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java85
-rw-r--r--java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java65
-rw-r--r--java/com/android/dialer/lookup/google/GoogleForwardLookup.java237
-rw-r--r--java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java104
-rw-r--r--java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java156
-rw-r--r--java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.pngbin0 -> 698 bytes
-rw-r--r--java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.pngbin0 -> 424 bytes
-rw-r--r--java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.pngbin0 -> 1140 bytes
-rw-r--r--java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.pngbin0 -> 632 bytes
-rw-r--r--java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.pngbin0 -> 1171 bytes
-rw-r--r--java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.pngbin0 -> 636 bytes
-rw-r--r--java/com/android/dialer/lookup/res/values/cm_arrays.xml53
-rw-r--r--java/com/android/dialer/lookup/res/values/cm_strings.xml36
-rw-r--r--java/com/android/dialer/lookup/res/xml/lookup_settings.xml70
-rw-r--r--java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java204
-rw-r--r--java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java119
-rw-r--r--java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java104
-rw-r--r--java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java57
37 files changed, 3820 insertions, 1 deletions
diff --git a/Android.mk b/Android.mk
index dd3784772..174ce6027 100644
--- a/Android.mk
+++ b/Android.mk
@@ -133,6 +133,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
libbackup \
libphonenumber \
volley \
+ org.lineageos.platform.internal
LOCAL_STATIC_ANDROID_LIBRARIES := \
android-support-core-ui \
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 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source 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
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.lookup">
+
+ <uses-sdk android:minSdkVersion="23"/>
+ <uses-permission android:name="lineageos.permission.WRITE_SETTINGS"/>
+
+ <application>
+ <provider android:name="com.android.dialer.lookup.LookupProvider"
+ android:authorities="com.android.dialer.lookup"
+ android:exported="false"
+ android:multiprocess="false" />
+
+ </application>
+</manifest>
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 <chillermillerlong@hotmail.com>
+ *
+ * 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<Address> addresses = new ArrayList<>();
+ private final ArrayList<PhoneNumber> phoneNumbers = new ArrayList<>();
+ private final ArrayList<WebsiteUrl> 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 <chillermillerlong@hotmail.com>
+ *
+ * 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<ContactInfo> 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 <chenxiaolong@cxl.epac.to>
+ *
+ * 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..b7f8ef13f
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupProvider.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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<FutureTask> 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<T> implements Callable<T> {
+ private final Callable<T> callable;
+ private volatile FutureTask<T> future;
+
+ public FutureCallable(Callable<T> 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<T> 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<Cursor>() {
+ @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.getLastLocation();
+ } 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<ContactInfo> results = null;
+ if ((type == NEARBY || type == NEARBY_AND_PEOPLE) && lastLocation != null) {
+ ForwardLookup fl = ForwardLookup.getInstance(getContext());
+ List<ContactInfo> 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<ContactInfo> 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<ContactInfo> 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> T execute(Callable<T> callable, String name) {
+ FutureCallable<T> futureCallable = new FutureCallable<T>(callable);
+ FutureTask<T> future = new FutureTask<T>(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..b1d224df0
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupSettings.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.provider.Settings;
+
+import lineageos.providers.LineageSettings;
+
+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;
+
+ private LookupSettings() {
+ }
+
+ public static boolean isForwardLookupEnabled(Context context) {
+ return LineageSettings.System.getInt(context.getContentResolver(),
+ LineageSettings.System.ENABLE_FORWARD_LOOKUP, 0) != 0;
+ }
+
+ public static boolean isPeopleLookupEnabled(Context context) {
+ return LineageSettings.System.getInt(context.getContentResolver(),
+ LineageSettings.System.ENABLE_PEOPLE_LOOKUP, 0) != 0;
+ }
+
+ public static boolean isReverseLookupEnabled(Context context) {
+ return LineageSettings.System.getInt(context.getContentResolver(),
+ LineageSettings.System.ENABLE_REVERSE_LOOKUP, 0) != 0;
+ }
+
+ public static String getForwardLookupProvider(Context context) {
+ return getLookupProvider(context,
+ LineageSettings.System.FORWARD_LOOKUP_PROVIDER, FLP_DEFAULT);
+ }
+
+ public static String getPeopleLookupProvider(Context context) {
+ return getLookupProvider(context,
+ LineageSettings.System.PEOPLE_LOOKUP_PROVIDER, PLP_DEFAULT);
+ }
+
+ public static String getReverseLookupProvider(Context context) {
+ String provider = getLookupProvider(context,
+ LineageSettings.System.REVERSE_LOOKUP_PROVIDER, RLP_DEFAULT);
+
+ if ("Google".equals(provider)) {
+ LineageSettings.System.putString(context.getContentResolver(),
+ LineageSettings.System.REVERSE_LOOKUP_PROVIDER, RLP_DEFAULT);
+ provider = RLP_DEFAULT;
+ }
+
+ return provider;
+ }
+
+ private static String getLookupProvider(Context context, String key, String defaultValue) {
+ ContentResolver cr = context.getContentResolver();
+ String provider = LineageSettings.System.getString(cr, key);
+
+ if (provider == null) {
+ LineageSettings.System.putString(cr, key, defaultValue);
+ return defaultValue;
+ }
+
+ return provider;
+ }
+}
diff --git a/java/com/android/dialer/lookup/LookupSettingsFragment.java b/java/com/android/dialer/lookup/LookupSettingsFragment.java
new file mode 100644
index 000000000..5cbcc72be
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupSettingsFragment.java
@@ -0,0 +1,141 @@
+/*
+ * 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.ContentResolver;
+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 lineageos.providers.LineageSettings;
+
+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) {
+ final ContentResolver cr = getActivity().getContentResolver();
+
+ if (preference == enableForwardLookup) {
+ LineageSettings.System.putInt(cr, LineageSettings.System.ENABLE_FORWARD_LOOKUP,
+ ((Boolean) newValue) ? 1 : 0);
+ } else if (preference == enablePeopleLookup) {
+ LineageSettings.System.putInt(cr, LineageSettings.System.ENABLE_PEOPLE_LOOKUP,
+ ((Boolean) newValue) ? 1 : 0);
+ } else if (preference == enableReverseLookup) {
+ LineageSettings.System.putInt(cr, LineageSettings.System.ENABLE_REVERSE_LOOKUP,
+ ((Boolean) newValue) ? 1 : 0);
+ } else if (preference == forwardLookupProvider) {
+ LineageSettings.System.putString(cr, LineageSettings.System.FORWARD_LOOKUP_PROVIDER,
+ (String) newValue);
+ } else if (preference == peopleLookupProvider) {
+ LineageSettings.System.putString(cr, LineageSettings.System.PEOPLE_LOOKUP_PROVIDER,
+ (String) newValue);
+ } else if (preference == reverseLookupProvider) {
+ LineageSettings.System.putString(cr, LineageSettings.System.REVERSE_LOOKUP_PROVIDER,
+ (String) newValue);
+ }
+
+ return true;
+ }
+
+ private void restoreLookupProviderSwitches() {
+ final ContentResolver cr = getActivity().getContentResolver();
+ enableForwardLookup.setChecked(LineageSettings.System.getInt(cr,
+ LineageSettings.System.ENABLE_FORWARD_LOOKUP, 0) != 0);
+ enablePeopleLookup.setChecked(LineageSettings.System.getInt(cr,
+ LineageSettings.System.ENABLE_PEOPLE_LOOKUP, 0) != 0);
+ enableReverseLookup.setChecked(LineageSettings.System.getInt(cr,
+ LineageSettings.System.ENABLE_REVERSE_LOOKUP, 0) != 0);
+ }
+
+ private void restoreLookupProviders() {
+ restoreLookupProvider(forwardLookupProvider,
+ LineageSettings.System.FORWARD_LOOKUP_PROVIDER);
+ restoreLookupProvider(peopleLookupProvider,
+ LineageSettings.System.PEOPLE_LOOKUP_PROVIDER);
+ restoreLookupProvider(reverseLookupProvider,
+ LineageSettings.System.REVERSE_LOOKUP_PROVIDER);
+ }
+
+ private void restoreLookupProvider(ListPreference pref, String key) {
+ if (pref.getEntries().length < 1) {
+ pref.setEnabled(false);
+ return;
+ }
+
+ final ContentResolver cr = getActivity().getContentResolver();
+ String provider = LineageSettings.System.getString(cr, key);
+ if (provider == null) {
+ pref.setValueIndex(0);
+ LineageSettings.System.putString(cr, key, 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<String, String> 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<String, String> 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<String, String> 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<String, String> headers) throws IOException {
+ HttpURLConnection connection = prepareHttpConnection(url, headers);
+ try {
+ return httpFetch(connection);
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ public static String httpPost(String url, Map<String, String> 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<String> 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<String> regexResults = new ArrayList<String>();
+ 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 <chillermillerlong@hotmail.com>
+ *
+ * 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<ContactInfo> 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 <chillermillerlong@hotmail.com>
+ *
+ * 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 <chillermillerlong@hotmail.com>
+ *
+ * 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)<section[\\s]+class=[\"']?search-entry(.*?)?</section";
+ private static final String NAME_REGEX =
+ "(?i)<h1[\\s]+itemprop=[\"']?name[\"']?>(.*?)</h1";
+ private static final String NUMBER_REGEX =
+ "(?i)phone[\"'][\\s]+?href=[\"']{1}tel:(.*?)[\"']{1}";
+ private static final String ADDRESS_REGEX =
+ "(?i)<span[\\s]+itemprop=[\"']?streetAddress[\"']?>(.*?)</a";
+
+ private static final String BUSINESS_IDENTIFIER = "(Firma)";
+
+ private AuskunftApi() {
+ }
+
+ public static List<ContactInfo> 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<String> 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<ContactInfo> infos = new ArrayList<ContactInfo>();
+ 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<ContactInfo> lookup(Context context, String filter) {
+ try {
+ List<AuskunftApi.ContactInfo> infos = AuskunftApi.query(filter);
+ if (infos != null) {
+ List<ContactInfo> 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<AuskunftApi.ContactInfo> 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 <dannybaumann@web.de>
+ *
+ * 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 ="<a id=\"name0.*?>\\s*\n?(.*?)\n?\\s*</a>";
+ private static String NUMBER_REGEX = "<span\\s+class=\"ico fon.*>.*<span>(.*?)</span><br/>";
+ private static String ADDRESS_REGEX = "<address.*?>\n?(.*?)</address>";
+
+ 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("<span class=\"hide\".*?\\/span>", "");
+ // ... and remove span wrappers around data content
+ result = result.replaceAll("</?span.*?>", "");
+ }
+ 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 <dannybaumann@web.de>
+ *
+ * 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 <chillermillerlong@hotmail.com>
+ *
+ * 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<ContactInfo> 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<String, String> 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<ContactInfo> getEntries(JSONArray results) throws JSONException {
+ ArrayList<ContactInfo> 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..75bc7b5f4
--- /dev/null
+++ b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.LookupUtils;
+import com.android.dialer.lookup.ReverseLookup;
+
+import lineageos.providers.LineageSettings;
+
+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 = LineageSettings.System.getString(
+ context.getContentResolver(),
+ LineageSettings.System.DIALER_OPENCNAM_ACCOUNT_SID);
+ String authToken = LineageSettings.System.getString(
+ context.getContentResolver(),
+ LineageSettings.System.DIALER_OPENCNAM_AUTH_TOKEN);
+
+ 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 <chillermillerlong@hotmail.com>
+ *
+ * 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<ContactInfo> 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<ContactInfo> getEntries(JSONObject results) throws JSONException {
+ ArrayList<ContactInfo> 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
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png
Binary files 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="forward_lookup_providers" translatable="false">
+ <item>Google</item>
+ <item>OpenStreetMap</item>
+ </string-array>
+
+ <string-array name="forward_lookup_provider_names" translatable="false">
+ <item>Google</item>
+ <item>OpenStreetMap</item>
+ </string-array>
+
+ <string-array name="people_lookup_providers" translatable="false">
+ <item>Auskunft</item>
+ </string-array>
+
+ <string-array name="people_lookup_provider_names" translatable="false">
+ <item>Auskunft (AT)</item>
+ </string-array>
+
+ <string-array name="reverse_lookup_providers" translatable="false">
+ <item>Auskunft</item>
+ <item>DasTelefonbuch</item>
+ <item>OpenCnam</item>
+ <item>YellowPages</item>
+ <item>YellowPages_CA</item>
+ <item>ZabaSearch</item>
+ </string-array>
+
+ <string-array name="reverse_lookup_provider_names" translatable="false">
+ <item>Auskunft (AT)</item>
+ <item>Das Telefonbuch (DE)</item>
+ <item>OpenCnam (US)</item>
+ <item>YellowPages (US)</item>
+ <item>YellowPages (CA)</item>
+ <item>ZabaSearch (US)</item>
+ </string-array>
+</resources>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Forward lookup -->
+ <string name="nearby_places">Nearby places</string>
+ <string name="people">People</string>
+
+ <!-- Number lookup -->
+ <string name="lookup_settings_label">Phone number lookup</string>
+ <string name="enable_forward_lookup_title">Forward lookup</string>
+ <string name="enable_forward_lookup_summary">Show nearby places when searching in the dialer</string>
+ <string name="enable_people_lookup_title">People lookup</string>
+ <string name="enable_people_lookup_summary">Show online results for people when searching in the dialer</string>
+ <string name="enable_reverse_lookup_title">Reverse lookup</string>
+ <string name="enable_reverse_lookup_summary">Look up information about the person or place for unknown numbers on incoming calls</string>
+ <string name="forward_lookup_provider_title">Forward lookup provider</string>
+ <string name="people_lookup_provider_title">People lookup provider</string>
+ <string name="reverse_lookup_provider_title">Reverse lookup provider</string>
+
+ <!-- Disclaimer -->
+ <string name="lookup_disclaimer">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</string>
+</resources>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ 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
+ -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ <SwitchPreference
+ android:key="enable_forward_lookup"
+ android:title="@string/enable_forward_lookup_title"
+ android:summary="@string/enable_forward_lookup_summary"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <ListPreference
+ android:key="forward_lookup_provider"
+ android:title="@string/forward_lookup_provider_title"
+ android:entries="@array/forward_lookup_provider_names"
+ android:entryValues="@array/forward_lookup_providers"
+ android:dependency="enable_forward_lookup"
+ android:summary="%s"
+ android:persistent="false" />
+
+ <SwitchPreference
+ android:key="enable_people_lookup"
+ android:title="@string/enable_people_lookup_title"
+ android:summary="@string/enable_people_lookup_summary"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <ListPreference
+ android:key="people_lookup_provider"
+ android:title="@string/people_lookup_provider_title"
+ android:entries="@array/people_lookup_provider_names"
+ android:entryValues="@array/people_lookup_providers"
+ android:summary="%s"
+ android:dependency="enable_people_lookup"
+ android:persistent="false" />
+
+ <SwitchPreference
+ android:key="enable_reverse_lookup"
+ android:title="@string/enable_reverse_lookup_title"
+ android:summary="@string/enable_reverse_lookup_summary"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <ListPreference
+ android:key="reverse_lookup_provider"
+ android:title="@string/reverse_lookup_provider_title"
+ android:entries="@array/reverse_lookup_provider_names"
+ android:entryValues="@array/reverse_lookup_providers"
+ android:dependency="enable_reverse_lookup"
+ android:summary="%s"
+ android:persistent="false" />
+
+ <Preference android:summary="@string/lookup_disclaimer" />
+</PreferenceScreen>
+
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 <chillermillerlong@hotmail.com>
+ *
+ * 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(
+ "<a href=\"([^>]+?)\"[^>]+?class=\"url[^>]+?>([^<]+)</a>",
+ 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=\"(.*?)\">"
+ + "(<span\\s+class=\"listingTitle\">.*?</span>)",
+ 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,
+ "<div\\s+class=\"phoneNumber\">(.*?)</div>", 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,
+ "<div\\s+class=\"address\">(.*?)</div>", 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 <chillermillerlong@hotmail.com>
+ *
+ * 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 <chillermillerlong@hotmail.com>
+ *
+ * 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\"?>([^<]+?)(&nbsp;)*<", 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 <chillermillerlong@hotmail.com>
+ *
+ * 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();
+ }
+}