From fa4ae989a0738b9e6f05eefdab6cebd7c7c9c1bc Mon Sep 17 00:00:00 2001 From: Xiao-Long Chen Date: Mon, 12 Sep 2016 09:34:02 +0200 Subject: Re-add dialer lookup. BUGBASH-612: do not send phone numbers to non-ssl sites for reverse/forward/people lookups Change-Id: I677460ad5767b8698ee24d6d43ff159aee55387a --- Android.mk | 3 + .../dialer/app/calllog/ClearCallLogDialog.java | 2 + .../dialer/app/list/RegularSearchFragment.java | 4 +- .../dialer/app/list/RegularSearchListAdapter.java | 14 + .../app/settings/DialerSettingsActivity.java | 6 + .../dialer/binary/aosp/AospDialerApplication.java | 49 +- java/com/android/dialer/lookup/AndroidManifest.xml | 31 ++ java/com/android/dialer/lookup/ContactBuilder.java | 524 +++++++++++++++++++++ java/com/android/dialer/lookup/DirectoryId.java | 35 ++ java/com/android/dialer/lookup/ForwardLookup.java | 62 +++ java/com/android/dialer/lookup/LookupCache.java | 303 ++++++++++++ java/com/android/dialer/lookup/LookupProvider.java | 504 ++++++++++++++++++++ java/com/android/dialer/lookup/LookupSettings.java | 108 +++++ .../dialer/lookup/LookupSettingsFragment.java | 141 ++++++ java/com/android/dialer/lookup/LookupUtils.java | 168 +++++++ java/com/android/dialer/lookup/PeopleLookup.java | 55 +++ java/com/android/dialer/lookup/ReverseLookup.java | 103 ++++ .../dialer/lookup/ReverseLookupService.java | 215 +++++++++ .../dialer/lookup/auskunft/AuskunftApi.java | 125 +++++ .../lookup/auskunft/AuskunftPeopleLookup.java | 46 ++ .../lookup/auskunft/AuskunftReverseLookup.java | 47 ++ .../lookup/dastelefonbuch/TelefonbuchApi.java | 87 ++++ .../dastelefonbuch/TelefonbuchReverseLookup.java | 69 +++ .../dialer/lookup/google/GoogleForwardLookup.java | 259 ++++++++++ .../lookup/opencnam/OpenCnamReverseLookup.java | 109 +++++ .../openstreetmap/OpenStreetMapForwardLookup.java | 171 +++++++ .../ic_places_picture_180_holo_light.png | Bin 0 -> 698 bytes .../drawable-hdpi/ic_places_picture_holo_light.png | Bin 0 -> 424 bytes .../ic_places_picture_180_holo_light.png | Bin 0 -> 1140 bytes .../ic_places_picture_holo_light.png | Bin 0 -> 632 bytes .../ic_places_picture_180_holo_light.png | Bin 0 -> 1171 bytes .../ic_places_picture_holo_light.png | Bin 0 -> 636 bytes .../android/dialer/lookup/res/values/cm_arrays.xml | 53 +++ .../dialer/lookup/res/values/cm_strings.xml | 33 ++ .../dialer/lookup/res/xml/lookup_settings.xml | 69 +++ .../dialer/lookup/yellowpages/YellowPagesApi.java | 218 +++++++++ .../yellowpages/YellowPagesReverseLookup.java | 117 +++++ .../dialer/lookup/zabasearch/ZabaSearchApi.java | 106 +++++ .../lookup/zabasearch/ZabaSearchReverseLookup.java | 63 +++ .../dialer/phonenumbercache/ContactInfoHelper.java | 3 + 40 files changed, 3900 insertions(+), 2 deletions(-) create mode 100644 java/com/android/dialer/lookup/AndroidManifest.xml create mode 100644 java/com/android/dialer/lookup/ContactBuilder.java create mode 100644 java/com/android/dialer/lookup/DirectoryId.java create mode 100644 java/com/android/dialer/lookup/ForwardLookup.java create mode 100644 java/com/android/dialer/lookup/LookupCache.java create mode 100644 java/com/android/dialer/lookup/LookupProvider.java create mode 100644 java/com/android/dialer/lookup/LookupSettings.java create mode 100644 java/com/android/dialer/lookup/LookupSettingsFragment.java create mode 100644 java/com/android/dialer/lookup/LookupUtils.java create mode 100644 java/com/android/dialer/lookup/PeopleLookup.java create mode 100644 java/com/android/dialer/lookup/ReverseLookup.java create mode 100644 java/com/android/dialer/lookup/ReverseLookupService.java create mode 100644 java/com/android/dialer/lookup/auskunft/AuskunftApi.java create mode 100644 java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java create mode 100644 java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java create mode 100644 java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java create mode 100644 java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java create mode 100644 java/com/android/dialer/lookup/google/GoogleForwardLookup.java create mode 100644 java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java create mode 100644 java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java create mode 100644 java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png create mode 100644 java/com/android/dialer/lookup/res/values/cm_arrays.xml create mode 100644 java/com/android/dialer/lookup/res/values/cm_strings.xml create mode 100644 java/com/android/dialer/lookup/res/xml/lookup_settings.xml create mode 100644 java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java create mode 100644 java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java create mode 100644 java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java create mode 100644 java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java diff --git a/Android.mk b/Android.mk index 4c5e66360..3a5957efe 100644 --- a/Android.mk +++ b/Android.mk @@ -85,6 +85,7 @@ RES_DIRS := \ $(BASE_DIR)/dialer/dialpadview/res \ $(BASE_DIR)/dialer/enrichedcall/simulator/res \ $(BASE_DIR)/dialer/interactions/res \ + $(BASE_DIR)/dialer/lookup/res \ $(BASE_DIR)/dialer/main/impl/res \ $(BASE_DIR)/dialer/notification/res \ $(BASE_DIR)/dialer/oem/res \ @@ -145,6 +146,7 @@ DIALER_MANIFEST_FILES += \ $(BASE_DIR)/dialer/dialpadview/AndroidManifest.xml \ $(BASE_DIR)/dialer/enrichedcall/simulator/AndroidManifest.xml \ $(BASE_DIR)/dialer/interactions/AndroidManifest.xml \ + $(BASE_DIR)/dialer/lookup/AndroidManifest.xml \ $(BASE_DIR)/dialer/main/impl/AndroidManifest.xml \ $(BASE_DIR)/dialer/notification/AndroidManifest.xml \ $(BASE_DIR)/dialer/oem/AndroidManifest.xml \ @@ -300,6 +302,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ libphonenumber \ okhttp \ volley \ + org.lineageos.platform.sdk LOCAL_STATIC_ANDROID_LIBRARIES := \ android-support-design \ diff --git a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java index 5c3d4d9fa..68357e77b 100644 --- a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java +++ b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java @@ -32,6 +32,7 @@ import android.provider.CallLog.Calls; import android.support.annotation.NonNull; import com.android.dialer.app.R; import com.android.dialer.common.Assert; +import com.android.dialer.lookup.LookupCache; import com.android.dialer.phonenumbercache.CachedNumberLookupService; import com.android.dialer.phonenumbercache.PhoneNumberCache; @@ -70,6 +71,7 @@ public class ClearCallLogDialog extends DialogFragment { if (cachedNumberLookupService != null) { cachedNumberLookupService.clearAllCacheEntries(context); } + LookupCache.deleteCachedContacts(context); return null; } diff --git a/java/com/android/dialer/app/list/RegularSearchFragment.java b/java/com/android/dialer/app/list/RegularSearchFragment.java index 728948bfc..133b88da7 100644 --- a/java/com/android/dialer/app/list/RegularSearchFragment.java +++ b/java/com/android/dialer/app/list/RegularSearchFragment.java @@ -27,6 +27,7 @@ import com.android.contacts.common.list.PinnedHeaderListView; import com.android.dialer.app.R; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.common.LogUtil; +import com.android.dialer.lookup.LookupCache; import com.android.dialer.phonenumbercache.CachedNumberLookupService; import com.android.dialer.phonenumbercache.PhoneNumberCache; import com.android.dialer.util.PermissionsUtil; @@ -71,11 +72,12 @@ public class RegularSearchFragment extends SearchFragment protected void cacheContactInfo(int position) { CachedNumberLookupService cachedNumberLookupService = PhoneNumberCache.get(getContext()).getCachedNumberLookupService(); + final RegularSearchListAdapter adapter = (RegularSearchListAdapter) getAdapter(); if (cachedNumberLookupService != null) { - final RegularSearchListAdapter adapter = (RegularSearchListAdapter) getAdapter(); cachedNumberLookupService.addContact( getContext(), adapter.getContactInfo(cachedNumberLookupService, position)); } + LookupCache.cacheContact(getContext(), adapter.getLookupContactInfo(position)); } @Override diff --git a/java/com/android/dialer/app/list/RegularSearchListAdapter.java b/java/com/android/dialer/app/list/RegularSearchListAdapter.java index 94544d2db..9bb38d329 100644 --- a/java/com/android/dialer/app/list/RegularSearchListAdapter.java +++ b/java/com/android/dialer/app/list/RegularSearchListAdapter.java @@ -39,6 +39,20 @@ public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter { setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, false); } + public ContactInfo getLookupContactInfo(int position) { + ContactInfo info = new ContactInfo(); + final Cursor item = (Cursor) getItem(position); + if (item != null) { + info.name = item.getString(PhoneQuery.DISPLAY_NAME); + info.type = item.getInt(PhoneQuery.PHONE_TYPE); + info.label = item.getString(PhoneQuery.PHONE_LABEL); + info.number = item.getString(PhoneQuery.PHONE_NUMBER); + final String photoUriStr = item.getString(PhoneQuery.PHOTO_URI); + info.photoUri = photoUriStr == null ? null : Uri.parse(photoUriStr); + } + return info; + } + public CachedContactInfo getContactInfo(CachedNumberLookupService lookupService, int position) { ContactInfo info = new ContactInfo(); CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info); diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java index 6036c85ce..e0637a7b1 100644 --- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java +++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java @@ -36,6 +36,7 @@ import com.android.dialer.about.AboutPhoneFragment; import com.android.dialer.app.R; import com.android.dialer.blocking.FilteredNumberCompat; import com.android.dialer.common.LogUtil; +import com.android.dialer.lookup.LookupSettingsFragment; import com.android.dialer.proguard.UsedByReflection; import com.android.voicemail.VoicemailClient; import com.android.voicemail.VoicemailComponent; @@ -89,6 +90,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..9061f231d 100644 --- a/java/com/android/dialer/binary/aosp/AospDialerApplication.java +++ b/java/com/android/dialer/binary/aosp/AospDialerApplication.java @@ -16,15 +16,30 @@ 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.contacts.common.list.DirectoryPartition; import com.android.dialer.binary.common.DialerApplication; import com.android.dialer.inject.ContextModule; +import com.android.dialer.lookup.LookupProvider; +import com.android.dialer.lookup.ReverseLookupService; +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 PhoneDirectoryExtenderFactory, InCallUiBindingsFactory { /** Returns a new instance of the root component for the AOSP Dialer. */ @Override @@ -32,4 +47,36 @@ 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 List getExtendedDirectories(Context context) { + return LookupProvider.getExtendedDirectories(context); + } + + @Override + public boolean isEnabled(Context context) { + return false; + } + + @Override + @Nullable + public Uri getContentUri() { + return null; + } + }; + } + + @Override + public InCallUiBindings newInCallUiBindings() { + return new InCallUiBindingsStub() { + @Override + @Nullable + public PhoneNumberService newPhoneNumberService(Context context) { + return new ReverseLookupService(context); + } + }; + } } diff --git a/java/com/android/dialer/lookup/AndroidManifest.xml b/java/com/android/dialer/lookup/AndroidManifest.xml new file mode 100644 index 000000000..0a278db15 --- /dev/null +++ b/java/com/android/dialer/lookup/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/java/com/android/dialer/lookup/ContactBuilder.java b/java/com/android/dialer/lookup/ContactBuilder.java new file mode 100644 index 000000000..12d4206b9 --- /dev/null +++ b/java/com/android/dialer/lookup/ContactBuilder.java @@ -0,0 +1,524 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.ContentResolver; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.DisplayNameSources; +import android.text.TextUtils; +import android.util.Log; + +import com.android.contacts.common.util.Constants; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.R; + +import java.sql.Struct; +import java.util.ArrayList; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.w3c.dom.Text; + +public class ContactBuilder { + private static final String TAG = + ContactBuilder.class.getSimpleName(); + + private static final boolean DEBUG = false; + + /** Used to choose the proper directory ID */ + public static final int FORWARD_LOOKUP = 0; + public static final int PEOPLE_LOOKUP = 1; + public static final int REVERSE_LOOKUP = 2; + + /** 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 ArrayList
mAddresses = new ArrayList
(); + private ArrayList mPhoneNumbers + = new ArrayList(); + private ArrayList mWebsites + = new ArrayList(); + + private int mDirectoryType; + private long mDirectoryId = DirectoryId.DEFAULT; + + private Name mName; + + private String mNormalizedNumber; + private String mFormattedNumber; + private int mDisplayNameSource = DisplayNameSources.ORGANIZATION; + private Uri mPhotoUri; + + private boolean mIsBusiness; + + public ContactBuilder(int directoryType, String normalizedNumber, + String formattedNumber) { + mDirectoryType = directoryType; + mNormalizedNumber = normalizedNumber; + mFormattedNumber = formattedNumber; + } + + public ContactBuilder(Uri encodedContactUri) throws JSONException { + String jsonData = encodedContactUri.getEncodedFragment(); + String directoryId = + encodedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); + if (!TextUtils.isEmpty(directoryId)) { + try { + mDirectoryId = Long.parseLong(directoryId); + } catch (NumberFormatException e) { + Log.e(TAG, "Error parsing directory id of uri " + encodedContactUri, e); + } + } + try { + // name + JSONObject json = new JSONObject(jsonData); + JSONObject contact = json.optJSONObject(Contacts.CONTENT_ITEM_TYPE); + JSONObject nameObj = contact.optJSONObject(StructuredName.CONTENT_ITEM_TYPE); + mName = 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 phoneNumbers; + if (phoneObject instanceof JSONObject) { + phoneNumbers = new JSONArray(); + phoneNumbers.put(phoneObject); + } else { + phoneNumbers = contact.getJSONArray(Phone.CONTENT_ITEM_TYPE); + } + for (int i = 0; i < phoneNumbers.length(); ++i) { + JSONObject phoneObj = phoneNumbers.getJSONObject(i); + mPhoneNumbers.add(new PhoneNumber(phoneObj)); + } + } + + // address + if (contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) { + JSONArray addresses = contact.getJSONArray(StructuredPostal.CONTENT_ITEM_TYPE); + for (int i = 0; i < addresses.length(); ++i) { + JSONObject addrObj = addresses.getJSONObject(i); + mAddresses.add(new Address(addrObj)); + } + } + + // websites + if (contact.has(Website.CONTENT_ITEM_TYPE)) { + JSONArray websites = contact.getJSONArray(Website.CONTENT_ITEM_TYPE); + for (int i = 0; i < websites.length(); ++i) { + JSONObject websiteObj = websites.getJSONObject(i); + final WebsiteUrl websiteUrl = new WebsiteUrl(websiteObj); + if (!TextUtils.isEmpty(websiteUrl.url)) { + mWebsites.add(new WebsiteUrl(websiteObj)); + } + } + } + } + + } + catch(JSONException e) { + Log.e(TAG, "Error parsing encoded fragment of uri " + encodedContactUri, e); + throw e; + } + } + + public void addAddress(Address address) { + if (DEBUG) Log.d(TAG, "Adding address"); + if (address != null) { + mAddresses.add(address); + } + } + + public Address[] getAddresses() { + return mAddresses.toArray(new Address[mAddresses.size()]); + } + + public void addPhoneNumber(PhoneNumber phoneNumber) { + if (DEBUG) Log.d(TAG, "Adding phone number"); + if (phoneNumber != null) { + mPhoneNumbers.add(phoneNumber); + } + } + + public PhoneNumber[] getPhoneNumbers() { + return mPhoneNumbers.toArray( + new PhoneNumber[mPhoneNumbers.size()]); + } + + public void addWebsite(WebsiteUrl website) { + if (DEBUG) Log.d(TAG, "Adding website"); + if (website != null) { + mWebsites.add(website); + } + } + + public WebsiteUrl[] getWebsites() { + return mWebsites.toArray(new WebsiteUrl[mWebsites.size()]); + } + + public void setName(Name name) { + if (DEBUG) Log.d(TAG, "Setting name"); + if (name != null) { + mName = name; + } + } + + public Name getName() { + return mName; + } + + public void setPhotoUri(String photoUri) { + if (photoUri != null) { + setPhotoUri(Uri.parse(photoUri)); + } + } + + public void setPhotoUri(Uri photoUri) { + if (DEBUG) Log.d(TAG, "Setting photo URI"); + mPhotoUri = photoUri; + } + + public Uri getPhotoUri() { + return mPhotoUri; + } + + public long getDirectoryId() { + return mDirectoryId; + } + + public void setIsBusiness(boolean isBusiness) { + if (DEBUG) Log.d(TAG, "Setting isBusiness to " + isBusiness); + mIsBusiness = isBusiness; + } + + public boolean isBusiness() { + return mIsBusiness; + } + + public ContactInfo build() { + if (mName == null) { + throw new IllegalStateException("Name has not been set"); + } + + if (mDirectoryType != FORWARD_LOOKUP + && mDirectoryType != PEOPLE_LOOKUP + && mDirectoryType != REVERSE_LOOKUP) { + throw new IllegalStateException("Invalid directory type"); + } + + // 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 (mPhoneNumbers.size() == 0) { + PhoneNumber pn = new PhoneNumber(); + // Use the formatted number where possible + pn.number = mFormattedNumber != null + ? mFormattedNumber : mNormalizedNumber; + pn.type = Phone.TYPE_MAIN; + addPhoneNumber(pn); + } + + try { + JSONObject contact = new JSONObject(); + + // Insert the name + contact.put(StructuredName.CONTENT_ITEM_TYPE, + mName.getJsonObject()); + + // Insert phone numbers + JSONArray phoneNumbers = new JSONArray(); + for (int i = 0; i < mPhoneNumbers.size(); i++) { + phoneNumbers.put(mPhoneNumbers.get(i).getJsonObject()); + } + contact.put(Phone.CONTENT_ITEM_TYPE, phoneNumbers); + + // Insert addresses if there are any + if (mAddresses.size() > 0) { + JSONArray addresses = new JSONArray(); + for (int i = 0; i < mAddresses.size(); i++) { + addresses.put(mAddresses.get(i).getJsonObject()); + } + contact.put(StructuredPostal.CONTENT_ITEM_TYPE, addresses); + } + + // Insert websites if there are any + if (mWebsites.size() > 0) { + JSONArray websites = new JSONArray(); + for (int i = 0; i < mWebsites.size(); i++) { + websites.put(mWebsites.get(i).getJsonObject()); + } + contact.put(Website.CONTENT_ITEM_TYPE, websites); + } + + ContactInfo info = new ContactInfo(); + info.name = mName.displayName; + info.normalizedNumber = mNormalizedNumber; + info.number = mPhoneNumbers.get(0).number; + info.type = mPhoneNumbers.get(0).type; + info.label = mPhoneNumbers.get(0).label; + info.photoUri = mPhotoUri != null ? mPhotoUri : null; + + String json = new JSONObject() + .put(Contacts.DISPLAY_NAME, mName.displayName) + .put(Contacts.DISPLAY_NAME_SOURCE, mDisplayNameSource) + .put(Directory.EXPORT_SUPPORT, + Directory.EXPORT_SUPPORT_ANY_ACCOUNT) + .put(Contacts.CONTENT_ITEM_TYPE, contact) + .toString(); + + if (json != null) { + long directoryId = -1; + switch (mDirectoryType) { + case FORWARD_LOOKUP: + directoryId = DirectoryId.NEARBY; + break; + case PEOPLE_LOOKUP: + directoryId = DirectoryId.PEOPLE; + break; + case REVERSE_LOOKUP: + // use null directory to be backwards compatible with old code + directoryId = DirectoryId.NULL; + break; + } + + 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) { + 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) { + 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..9749dc440 --- /dev/null +++ b/java/com/android/dialer/lookup/DirectoryId.java @@ -0,0 +1,35 @@ +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..f65b3209b --- /dev/null +++ b/java/com/android/dialer/lookup/ForwardLookup.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.google.GoogleForwardLookup; +import com.android.dialer.lookup.openstreetmap.OpenStreetMapForwardLookup; + +import android.content.Context; +import android.location.Location; +import android.util.Log; + +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 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..e6d5cc697 --- /dev/null +++ b/java/com/android/dialer/lookup/LookupCache.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.phonenumbercache.ContactInfo; + +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.util.DialerUtils; + +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; + +import org.json.JSONException; +import org.json.JSONObject; + +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 PHOTO_URI = "PhotoURI"; + 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() + + File.separator + "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 void 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; + } + + File image = getImagePath(context, normalizedNumber); + + FileOutputStream out = null; + + try { + out = new FileOutputStream(image); + bmp.compress(Bitmap.CompressFormat.WEBP, 100, out); + } catch (Exception e) { + e.printStackTrace(); + } finally { + DialerUtils.closeQuietly(out); + } + } + + 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) { + String countryIso = ((TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE)).getSimCountryIso().toUpperCase(); + return PhoneNumberUtils.formatNumberToE164(number, countryIso); + } + + private static File getFilePath(Context context, String normalizedNumber) { + File dir = new File(context.getCacheDir() + + File.separator + "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() + + File.separator + "lookup"); + + if (!dir.exists()) { + dir.mkdirs(); + } + + return new File(dir, normalizedNumber + ".webp"); + } +} diff --git a/java/com/android/dialer/lookup/LookupProvider.java b/java/com/android/dialer/lookup/LookupProvider.java new file mode 100644 index 000000000..b62a94af1 --- /dev/null +++ b/java/com/android/dialer/lookup/LookupProvider.java @@ -0,0 +1,504 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.Contacts; +import android.provider.Settings; +import android.util.Log; + +import com.android.contacts.common.list.DirectoryPartition; +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.R; + +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; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class LookupProvider extends ContentProvider { + private static final String TAG = LookupProvider.class.getSimpleName(); + + private static final boolean DEBUG = false; + + public static List getExtendedDirectories(Context context) { + ArrayList list = new ArrayList(); + + // The directories are shown in reverse order, so insert forward lookup + // last to make it show up at the top + + if (LookupSettings.isPeopleLookupEnabled(context)) { + DirectoryPartition dp = new DirectoryPartition(false, true); + dp.setContentUri(PEOPLE_LOOKUP_URI.toString()); + dp.setLabel(context.getString(R.string.people)); + dp.setPriorityDirectory(false); + dp.setPhotoSupported(true); + dp.setDisplayNumber(false); + dp.setResultLimit(3); + list.add(dp); + } else { + Log.i(TAG, "Forward lookup (people) is disabled"); + } + + if (LookupSettings.isForwardLookupEnabled(context)) { + DirectoryPartition dp = new DirectoryPartition(false, true); + dp.setContentUri(NEARBY_LOOKUP_URI.toString()); + dp.setLabel(context.getString(R.string.nearby_places)); + dp.setPriorityDirectory(false); + dp.setPhotoSupported(true); + dp.setDisplayNumber(false); + dp.setResultLimit(3); + list.add(dp); + } else { + Log.i(TAG, "Forward lookup (nearby places) is disabled"); + } + + return list; + } + + 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 IMAGE_CACHE_URI = + Uri.withAppendedPath(AUTHORITY_URI, "images"); + + private static final UriMatcher sURIMatcher = new UriMatcher(-1); + private final LinkedList mActiveTasks = + new LinkedList(); + + private static final int NEARBY = 0; + private static final int PEOPLE = 1; + private static final int IMAGE = 2; + + static { + sURIMatcher.addURI(AUTHORITY, "nearby/*", NEARBY); + sURIMatcher.addURI(AUTHORITY, "people/*", PEOPLE); + sURIMatcher.addURI(AUTHORITY, "images/*", IMAGE); + } + + private class FutureCallable implements Callable { + private final Callable mCallable; + private volatile FutureTask mFuture; + + public FutureCallable(Callable callable) { + mFuture = null; + mCallable = callable; + } + + public T call() throws Exception { + Log.v(TAG, "Future called for " + Thread.currentThread().getName()); + + T result = mCallable.call(); + if (mFuture == null) { + return result; + } + + synchronized (mActiveTasks) { + mActiveTasks.remove(mFuture); + } + + mFuture = null; + return result; + } + + public void setFuture(FutureTask future) { + mFuture = 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 = sURIMatcher.match(uri); + + switch (match) { + case NEARBY: + if (!PermissionsUtil.hasLocationPermissions(getContext())) { + Log.v(TAG, "Location permission is missing, ignoring query."); + return null; + } + if (!isLocationEnabled()) { + Log.v(TAG, "Location settings is disabled, ignoring query."); + return null; + } + lastLocation = getLastLocation(); + if (lastLocation == null) { + Log.v(TAG, "No location available, ignoring query."); + return null; + } + // fall through to the actual query + + case PEOPLE: + final String filter = Uri.encode(uri.getLastPathSegment()); + String limit = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY); + + int maxResults = -1; + + try { + if (limit != null) { + maxResults = Integer.parseInt(limit); + } + } catch (NumberFormatException e) { + Log.e(TAG, "query: invalid limit parameter: '" + limit + "'"); + } + + final Location finalLastLocation = lastLocation; + final int finalMaxResults = maxResults; + + return execute(new Callable() { + @Override + public Cursor call() { + return handleFilter(match, projection, filter, + finalMaxResults, finalLastLocation); + } + }, "FilterThread"); + } + + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("insert() not supported"); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + throw new UnsupportedOperationException("update() not supported"); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("delete() not supported"); + } + + @Override + public String getType(Uri uri) { + int match = sURIMatcher.match(uri); + + switch (match) { + case NEARBY: + case PEOPLE: + return Contacts.CONTENT_ITEM_TYPE; + + default: + return null; + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + switch (sURIMatcher.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 = (LocationManager) + getContext().getSystemService(Context.LOCATION_SERVICE); + + 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) { + try { + filter = URLDecoder.decode(filter, "UTF-8"); + } catch (UnsupportedEncodingException e) { + } + + ContactInfo[] results = null; + if (type == NEARBY) { + ForwardLookup fl = ForwardLookup.getInstance(getContext()); + results = fl.lookup(getContext(), filter, lastLocation); + } else if (type == PEOPLE) { + PeopleLookup pl = PeopleLookup.getInstance(getContext()); + results = pl.lookup(getContext(), filter); + } + + if (results == null || results.length == 0) { + if (DEBUG) Log.v(TAG, "handleFilter(" + filter + "): No results"); + return null; + } + + Cursor cur = null; + try { + cur = buildResultCursor(projection, results, maxResults); + + if (DEBUG) Log.v(TAG, "handleFilter(" + filter + "): " + + cur.getCount() + " matches"); + } catch (JSONException e) { + Log.e(TAG, "JSON failure", e); + } + + return cur; + } + + return null; + } + + /** + * 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, + ContactInfo[] results, int maxResults) + throws JSONException { + // Extended directories always use this projection + MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + + int id = 1; + + for (int i = 0; i < results.length; i++) { + Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; + + row[PhoneQuery.PHONE_ID] = id; + row[PhoneQuery.PHONE_TYPE] = results[i].type; + row[PhoneQuery.PHONE_LABEL] = getAddress(results[i]); + row[PhoneQuery.PHONE_NUMBER] = results[i].number; + row[PhoneQuery.CONTACT_ID] = id; + row[PhoneQuery.LOOKUP_KEY] = results[i].lookupUri.getEncodedFragment(); + row[PhoneQuery.PHOTO_ID] = 0; + row[PhoneQuery.DISPLAY_NAME] = results[i].name; + row[PhoneQuery.PHOTO_URI] = results[i].photoUri; + + cursor.addRow(row); + + if (maxResults != -1 && cursor.getCount() >= maxResults) { + break; + } + + id++; + } + + return cursor; + } + + private String getAddress(ContactInfo info) { + // Hack: Show city or address for phone label, so they appear in + // the results list + + String city = null; + String address = null; + + try { + String jsonString = info.lookupUri.getEncodedFragment(); + JSONObject json = new JSONObject(jsonString); + JSONObject contact = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE); + + if (!contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) { + return null; + } + + JSONArray addresses = contact.getJSONArray( + StructuredPostal.CONTENT_ITEM_TYPE); + + if (addresses.length() == 0) { + return null; + } + + JSONObject addressEntry = addresses.getJSONObject(0); + + if (addressEntry.has(StructuredPostal.CITY)) { + city = addressEntry.getString(StructuredPostal.CITY); + } + if (addressEntry.has(StructuredPostal.FORMATTED_ADDRESS)) { + address = addressEntry.getString( + StructuredPostal.FORMATTED_ADDRESS); + } + } catch (JSONException e) { + Log.e(TAG, "Failed to get address", e); + } + + if (city != null) { + return city; + } else if (address != null) { + return address; + } else { + return null; + } + } + + /** + * Execute thread that is killed after a specified amount of time. + * + * @param callable The thread + * @param name Name of the thread + * @return Instance of the thread + */ + private T execute(Callable callable, String name) { + FutureCallable futureCallable = new FutureCallable(callable); + FutureTask future = new FutureTask(futureCallable); + futureCallable.setFuture(future); + + synchronized (mActiveTasks) { + mActiveTasks.addLast(future); + Log.v(TAG, "Currently running tasks: " + mActiveTasks.size()); + + while (mActiveTasks.size() > 8) { + Log.w(TAG, "Too many tasks, canceling one"); + mActiveTasks.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..bed395dee --- /dev/null +++ b/java/com/android/dialer/lookup/LookupSettings.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.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, 1) != 0; + } + + public static boolean isPeopleLookupEnabled(Context context) { + return LineageSettings.System.getInt(context.getContentResolver(), + LineageSettings.System.ENABLE_PEOPLE_LOOKUP, 1) != 0; + } + + public static boolean isReverseLookupEnabled(Context context) { + return LineageSettings.System.getInt(context.getContentResolver(), + LineageSettings.System.ENABLE_REVERSE_LOOKUP, 1) != 0; + } + + public static String getForwardLookupProvider(Context context) { + String provider = getLookupProvider(context, + LineageSettings.System.FORWARD_LOOKUP_PROVIDER, FLP_DEFAULT); + + return provider; + } + + public static String getPeopleLookupProvider(Context context) { + String provider = getLookupProvider(context, + LineageSettings.System.PEOPLE_LOOKUP_PROVIDER, PLP_DEFAULT); + + return provider; + } + + 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..8ec1028ec --- /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 mEnableForwardLookup; + private SwitchPreference mEnablePeopleLookup; + private SwitchPreference mEnableReverseLookup; + private ListPreference mForwardLookupProvider; + private ListPreference mPeopleLookupProvider; + private ListPreference mReverseLookupProvider; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.lookup_settings); + + mEnableForwardLookup = (SwitchPreference) findPreference(KEY_ENABLE_FORWARD_LOOKUP); + mEnablePeopleLookup = (SwitchPreference) findPreference(KEY_ENABLE_PEOPLE_LOOKUP); + mEnableReverseLookup = (SwitchPreference) findPreference(KEY_ENABLE_REVERSE_LOOKUP); + + mEnableForwardLookup.setOnPreferenceChangeListener(this); + mEnablePeopleLookup.setOnPreferenceChangeListener(this); + mEnableReverseLookup.setOnPreferenceChangeListener(this); + + mForwardLookupProvider = (ListPreference) findPreference(KEY_FORWARD_LOOKUP_PROVIDER); + mPeopleLookupProvider = (ListPreference) findPreference(KEY_PEOPLE_LOOKUP_PROVIDER); + mReverseLookupProvider = (ListPreference) findPreference(KEY_REVERSE_LOOKUP_PROVIDER); + + mForwardLookupProvider.setOnPreferenceChangeListener(this); + mPeopleLookupProvider.setOnPreferenceChangeListener(this); + mReverseLookupProvider.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 == mEnableForwardLookup) { + LineageSettings.System.putInt(cr, LineageSettings.System.ENABLE_FORWARD_LOOKUP, + ((Boolean) newValue) ? 1 : 0); + } else if (preference == mEnablePeopleLookup) { + LineageSettings.System.putInt(cr, LineageSettings.System.ENABLE_PEOPLE_LOOKUP, + ((Boolean) newValue) ? 1 : 0); + } else if (preference == mEnableReverseLookup) { + LineageSettings.System.putInt(cr, LineageSettings.System.ENABLE_REVERSE_LOOKUP, + ((Boolean) newValue) ? 1 : 0); + } else if (preference == mForwardLookupProvider) { + LineageSettings.System.putString(cr, LineageSettings.System.FORWARD_LOOKUP_PROVIDER, + (String) newValue); + } else if (preference == mPeopleLookupProvider) { + LineageSettings.System.putString(cr, LineageSettings.System.PEOPLE_LOOKUP_PROVIDER, + (String) newValue); + } else if (preference == mReverseLookupProvider) { + LineageSettings.System.putString(cr, LineageSettings.System.REVERSE_LOOKUP_PROVIDER, + (String) newValue); + } + + return true; + } + + private void restoreLookupProviderSwitches() { + final ContentResolver cr = getActivity().getContentResolver(); + mEnableForwardLookup.setChecked(LineageSettings.System.getInt(cr, + LineageSettings.System.ENABLE_FORWARD_LOOKUP, 1) != 0); + mEnablePeopleLookup.setChecked(LineageSettings.System.getInt(cr, + LineageSettings.System.ENABLE_PEOPLE_LOOKUP, 1) != 0); + mEnableReverseLookup.setChecked(LineageSettings.System.getInt(cr, + LineageSettings.System.ENABLE_REVERSE_LOOKUP, 1) != 0); + } + + private void restoreLookupProviders() { + restoreLookupProvider(mForwardLookupProvider, + LineageSettings.System.FORWARD_LOOKUP_PROVIDER); + restoreLookupProvider(mPeopleLookupProvider, + LineageSettings.System.PEOPLE_LOOKUP_PROVIDER); + restoreLookupProvider(mReverseLookupProvider, + 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..f13a33e54 --- /dev/null +++ b/java/com/android/dialer/lookup/LookupUtils.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.text.Html; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LookupUtils { + private static final String USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0"; + + private static HttpURLConnection prepareHttpConnection(String url, Map headers) + throws IOException { + // open connection + HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); + // set user agent (default value is null) + urlConnection.setRequestProperty("User-Agent", USER_AGENT); + // set all other headers if not null + if (headers != null) { + for (Map.Entry header : headers.entrySet()) { + urlConnection.setRequestProperty(header.getKey(), header.getValue()); + } + } + + return urlConnection; + } + + private static byte[] httpFetch(HttpURLConnection urlConnection) throws IOException { + // query url, read and return buffered response body + // we want to make sure that the connection gets closed here + InputStream is = new BufferedInputStream(urlConnection.getInputStream()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] result = null; + try { + byte[] partial = new byte[4096]; + int read; + while ((read = is.read(partial, 0, 4096)) != -1) { + baos.write(partial, 0, read); + } + result = baos.toByteArray(); + } finally { + is.close(); + baos.close(); + } + return result; + } + + private static Charset determineCharset(HttpURLConnection connection) { + String contentType = connection.getContentType(); + if (contentType != null) { + String[] split = contentType.split(";"); + for (int i = 0; i < split.length; i++) { + String trimmed = split[i].trim(); + if (trimmed.startsWith("charset=")) { + try { + return Charset.forName(trimmed.substring(8)); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + // we don't know about this charset -> ignore + } + } + } + } + return Charset.defaultCharset(); + } + + public static String httpGet(String url, Map headers) throws IOException { + HttpURLConnection connection = prepareHttpConnection(url, headers); + try { + byte[] response = httpFetch(connection); + return new String(response, determineCharset(connection)); + } finally { + connection.disconnect(); + } + } + + public static byte[] httpGetBytes(String url, Map headers) throws IOException { + HttpURLConnection connection = prepareHttpConnection(url, headers); + try { + return httpFetch(connection); + } finally { + connection.disconnect(); + } + } + + public static String httpPost(String url, Map headers, String postData) + throws IOException { + HttpURLConnection connection = prepareHttpConnection(url, headers); + + try { + // write postData to buffered output stream + if (postData != null) { + connection.setDoOutput(true); + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( + connection.getOutputStream())); + try { + bw.write(postData, 0, postData.length()); + // close connection and re-throw exception + } finally { + bw.close(); + } + } + byte[] response = httpFetch(connection); + return new String(response, determineCharset(connection)); + } finally { + connection.disconnect(); + } + } + + public static List allRegexResults(String input, String regex, boolean dotall) { + if (input == null) { + return null; + } + Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0); + Matcher matcher = pattern.matcher(input); + + List regexResults = new ArrayList(); + while (matcher.find()) { + regexResults.add(matcher.group(1).trim()); + } + return regexResults; + } + + public static String firstRegexResult(String input, String regex, boolean dotall) { + if (input == null) { + return null; + } + Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0); + Matcher m = pattern.matcher(input); + return m.find() ? m.group(1).trim() : null; + } + + public static String fromHtml(String input) { + if (input == null) { + return null; + } + return Html.fromHtml(input).toString().trim(); + } +} diff --git a/java/com/android/dialer/lookup/PeopleLookup.java b/java/com/android/dialer/lookup/PeopleLookup.java new file mode 100644 index 000000000..28efbed95 --- /dev/null +++ b/java/com/android/dialer/lookup/PeopleLookup.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.auskunft.AuskunftPeopleLookup; + +import android.content.Context; +import android.util.Log; + +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 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..505692634 --- /dev/null +++ b/java/com/android/dialer/lookup/ReverseLookup.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import 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 android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Log; + +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); + } 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..fe4614a2e --- /dev/null +++ b/java/com/android/dialer/lookup/ReverseLookupService.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; + +import com.android.dialer.location.GeoUtil; +import com.android.dialer.logging.ContactLookupResult; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.incallui.bindings.PhoneNumberService; + +import java.io.IOException; + +public class ReverseLookupService implements PhoneNumberService, Handler.Callback { + private final HandlerThread mBackgroundThread; + private final Handler mBackgroundHandler; + private final Handler mHandler; + private final Context mContext; + private final TelephonyManager mTelephonyManager; + + private static final int MSG_LOOKUP = 1; + private static final int MSG_NOTIFY_NUMBER = 2; + private static final int MSG_NOTIFY_IMAGE = 3; + + public ReverseLookupService(Context context) { + mContext = context; + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + + // TODO: stop after a while? + mBackgroundThread = new HandlerThread("ReverseLookup"); + mBackgroundThread.start(); + + mBackgroundHandler = new Handler(mBackgroundThread.getLooper(), this); + mHandler = new Handler(this); + } + + @Override + public void getPhoneNumberInfo(String phoneNumber, NumberLookupListener numberListener, + ImageLookupListener imageListener, boolean isIncoming) { + if (!LookupSettings.isReverseLookupEnabled(mContext)) { + LookupCache.deleteCachedContacts(mContext); + return; + } + + String countryIso = mTelephonyManager.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(mContext)); + request.numberListener = numberListener; + request.imageListener = imageListener; + + mBackgroundHandler.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) { + mHandler.obtainMessage(MSG_NOTIFY_NUMBER, request).sendToTarget(); + if (request.imageListener != null && request.contactInfo.photoUri != null) { + request.photo = fetchImage(request, request.contactInfo.photoUri); + if (request.photo != null) { + mHandler.obtainMessage(MSG_NOTIFY_IMAGE, 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; + } + case MSG_NOTIFY_IMAGE: + // main thread + LookupRequest request = (LookupRequest) msg.obj; + if (request.imageListener != null) { + request.imageListener.onImageFetchComplete(request.photo); + } + break; + } + + return true; + } + + private ContactInfo doLookup(LookupRequest request) { + final String number = request.normalizedNumber; + + if (LookupCache.hasCachedContact(mContext, number)) { + ContactInfo info = LookupCache.getCachedContact(mContext, 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(mContext, number); + } + } + + try { + ContactInfo info = ReverseLookup.getInstance(mContext).lookupNumber(mContext, + number, request.formattedNumber); + if (info != null && !info.equals(ContactInfo.EMPTY)) { + LookupCache.cacheContact(mContext, info); + return info; + } + } catch (IOException e) { + // ignored + } + + return null; + } + + private Bitmap fetchImage(LookupRequest request, Uri uri) { + if (!LookupCache.hasCachedImage(mContext, request.normalizedNumber)) { + Bitmap bmp = ReverseLookup.getInstance(mContext).lookupImage(mContext, uri); + if (bmp != null) { + LookupCache.cacheImage(mContext, request.normalizedNumber, bmp); + } + } + + return LookupCache.getCachedImage(mContext, request.normalizedNumber); + } + + private static class LookupRequest { + String normalizedNumber; + String formattedNumber; + NumberLookupListener numberListener; + ImageLookupListener imageListener; + ContactInfo contactInfo; + Bitmap photo; + } + + private static class LookupNumberInfo implements PhoneNumberInfo { + private ContactInfo mInfo; + private LookupNumberInfo(ContactInfo info) { + mInfo = info; + } + + @Override + public String getDisplayName() { + return mInfo.name; + } + @Override + public String getNumber() { + return mInfo.number; + } + @Override + public int getPhoneType() { + return mInfo.type; + } + @Override + public String getPhoneLabel() { + return mInfo.label; + } + @Override + public String getNormalizedNumber() { + return mInfo.normalizedNumber; + } + @Override + public String getImageUrl() { + return mInfo.photoUri != null ? mInfo.photoUri.toString() : null; + } + @Override + public boolean isBusiness() { + // FIXME + return false; + } + @Override + public String getLookupKey() { + return mInfo.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..4a0a1c0a5 --- /dev/null +++ b/java/com/android/dialer/lookup/auskunft/AuskunftApi.java @@ -0,0 +1,125 @@ +/** + * 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.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.LookupUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public final class AuskunftApi { + private static final String TAG = AuskunftApi.class.getSimpleName(); + + private static final String PEOPLE_LOOKUP_URL = + "https://auskunft.at/suche"; + + private static final String SEARCH_RESULTS_REGEX = + "(?i)(.*?)(.*?) query(String filter, int lookupType, String normalizedNumber, + String formattedNumber) throws IOException { + // build URI + Uri uri = Uri.parse(PEOPLE_LOOKUP_URL) + .buildUpon() + .appendQueryParameter("query", filter) + .build(); + + // get all search entry sections + List entries = LookupUtils.allRegexResults(LookupUtils.httpGet(uri.toString(), + null), SEARCH_RESULTS_REGEX, true); + + // abort lookup if nothing found + if (entries == null || entries.isEmpty()) { + Log.w(TAG, "nothing found"); + return null; + } + + // build response by iterating through the search entries and parsing their HTML data + List infos = new ArrayList(); + for (String entry : entries) { + // parse wanted data and replace null values + String name = replaceNullResult(LookupUtils.firstRegexResult(entry, NAME_REGEX, true)); + String address = replaceNullResult(LookupUtils.firstRegexResult( + entry, ADDRESS_REGEX, true)); + String number = replaceNullResult(LookupUtils.firstRegexResult( + entry, NUMBER_REGEX, true)); + // ignore entry if name or number is empty (should not occur) + // missing addresses won't be a problem (but do occur) + if (name.isEmpty() || number.isEmpty()) { + continue; + } + // figure out if we have a business contact + boolean isBusiness = name.contains(BUSINESS_IDENTIFIER); + // cleanup results + name = cleanupResult(name); + number = cleanupResult(number); + address = cleanupResult(address); + // set normalized and formatted number if we're not doing a reverse lookup + if (lookupType != ContactBuilder.REVERSE_LOOKUP) { + normalizedNumber = formattedNumber = number; + } + // build contact and add to list + ContactBuilder builder = new ContactBuilder(lookupType, normalizedNumber, + formattedNumber); + builder.setName(ContactBuilder.Name.createDisplayName(name)); + builder.addPhoneNumber( + ContactBuilder.PhoneNumber.createMainNumber(number)); + builder.addWebsite(ContactBuilder.WebsiteUrl.createProfile(uri.toString())); + builder.addAddress(ContactBuilder.Address.createFormattedHome(address)); + builder.setIsBusiness(isBusiness); + infos.add(builder.build()); + } + 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; + } +} 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..c51e54348 --- /dev/null +++ b/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java @@ -0,0 +1,46 @@ +/** + * 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.List; + +public class AuskunftPeopleLookup extends PeopleLookup { + private static final String TAG = AuskunftPeopleLookup.class.getSimpleName(); + + public AuskunftPeopleLookup(Context context) { + } + + @Override + public ContactInfo[] lookup(Context context, String filter) { + List infos = null; + try { + infos = AuskunftApi.query(filter, ContactBuilder.PEOPLE_LOOKUP, null, null); + } catch (IOException e) { + Log.e(TAG, "People lookup failed", e); + } + return (infos != null && !infos.isEmpty()) + ? infos.toArray(new ContactInfo[infos.size()]) : 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..cc046de55 --- /dev/null +++ b/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java @@ -0,0 +1,47 @@ +/** + * 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 { + private static final String TAG = AuskunftReverseLookup.class.getSimpleName(); + + public AuskunftReverseLookup(Context context) { + } + + @Override + public ContactInfo lookupNumber(Context context, String normalizedNumber, + String formattedNumber) throws IOException { + // only Austrian numbers are supported + if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+43")) { + return null; + } + + // query the API and return null if nothing found or general error + List infos = AuskunftApi.query(normalizedNumber, ContactBuilder.REVERSE_LOOKUP, + normalizedNumber, formattedNumber); + return (infos != null && !infos.isEmpty()) ? infos.get(0) : null; + } +} 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..ff2fd4b1d --- /dev/null +++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 Danny Baumann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.dastelefonbuch; + +import android.content.Context; +import android.net.Uri; + +import com.android.dialer.lookup.LookupUtils; + +import java.io.IOException; + +public class TelefonbuchApi { + private static final String TAG = TelefonbuchApi.class.getSimpleName(); + + private static final String REVERSE_LOOKUP_URL = + "https://www.dastelefonbuch.de/?s=a20000" + + "&cmd=search&sort_ok=0&sp=55&vert_ok=0&aktion=23"; + + private static String NAME_REGEX ="\\s*\n?(.*?)\n?\\s*"; + private static String NUMBER_REGEX = ".*(.*?)
"; + private static String ADDRESS_REGEX = "\n?(.*?)
"; + + private TelefonbuchApi() { + } + + public static ContactInfo reverseLookup(Context context, String number) + throws IOException { + Uri uri = Uri.parse(REVERSE_LOOKUP_URL) + .buildUpon() + .appendQueryParameter("kw", number) + .build(); + // Cut out everything we're not interested in (scripts etc.) to + // speed up the subsequent matching. + String output = LookupUtils.firstRegexResult( + LookupUtils.httpGet(uri.toString(), null), + ": Treffer(.*)Ende Treffer", true); + + String name = parseValue(output, NAME_REGEX, true, false); + if (name == null) { + return null; + } + + String phoneNumber = parseValue(output, NUMBER_REGEX, false, true); + String address = parseValue(output, ADDRESS_REGEX, true, true); + + ContactInfo info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = phoneNumber != null ? phoneNumber : number; + info.website = uri.toString(); + + return info; + } + + private static String parseValue(String output, String regex, + boolean dotall, boolean removeSpans) { + String result = LookupUtils.firstRegexResult(output, regex, dotall); + if (result != null && removeSpans) { + // completely remove hidden spans (including contents) ... + result = result.replaceAll("", ""); + // ... and remove span wrappers around data content + result = result.replaceAll("", ""); + } + return LookupUtils.fromHtml(result); + } + + public static class ContactInfo { + String name; + String address; + String formattedNumber; + String website; + } +} diff --git a/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java new file mode 100644 index 000000000..88c159c69 --- /dev/null +++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 Danny Baumann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.dastelefonbuch; + +import android.content.Context; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ReverseLookup; + +import java.io.IOException; + +public class TelefonbuchReverseLookup extends ReverseLookup { + private static final String TAG = TelefonbuchReverseLookup.class.getSimpleName(); + + public TelefonbuchReverseLookup(Context context) { + } + + /** + * Perform phone number lookup. + * + * @param context The application context + * @param normalizedNumber The normalized phone number + * @param formattedNumber The formatted phone number + * @return The phone number info object + */ + 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; + } + + ContactBuilder builder = new ContactBuilder( + ContactBuilder.REVERSE_LOOKUP, + normalizedNumber, formattedNumber); + builder.setName(ContactBuilder.Name.createDisplayName(info.name)); + builder.addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber)); + builder.addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website)); + if (info.address != null) { + builder.addAddress(ContactBuilder.Address.createFormattedHome(info.address)); + } + + return builder.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..fb19ec387 --- /dev/null +++ b/java/com/android/dialer/lookup/google/GoogleForwardLookup.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.google; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ForwardLookup; +import com.android.dialer.lookup.LookupUtils; + +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 java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +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 String mUserAgent = ""; + + 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); + mUserAgent = sb.toString(); + } catch (PackageManager.NameNotFoundException e) { + } + } + + @Override + public 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 = builder.appendQueryParameter(QUERY_FILTER, filter); + + // Language + builder = builder.appendQueryParameter(QUERY_LANGUAGE, + context.getResources().getConfiguration() + .locale.getLanguage()); + + // Location (latitude and longitude) + builder = builder.appendQueryParameter(QUERY_LOCATION, + String.format("%f,%f", + lastLocation.getLatitude(), + lastLocation.getLongitude())); + + // Radius distance + builder = builder.appendQueryParameter(QUERY_RADIUS, + Integer.toString(RADIUS)); + + // Random string (not really required) + builder = builder.appendQueryParameter(QUERY_RANDOM, + getRandomNoiseString()); + + Map headers = new HashMap(); + headers.put("User-Agent", mUserAgent); + 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 ContactInfo[] getEntries(JSONArray results) + throws JSONException { + ArrayList details = + new ArrayList(); + + JSONArray entries = results.getJSONArray(1); + + for (int i = 0; i < entries.length(); i++) { + try { + JSONArray entry = entries.getJSONArray(i); + + String displayName = decodeHtml(entry.getString(0)); + + JSONObject params = entry.getJSONObject(3); + + String phoneNumber = decodeHtml( + params.getString(RESULT_NUMBER)); + + String address = decodeHtml(params.getString(RESULT_ADDRESS)); + String city = decodeHtml(params.getString(RESULT_CITY)); + + String profileUrl = params.optString(RESULT_WEBSITE, null); + String photoUri = params.optString(RESULT_PHOTO_URI, null); + + ContactBuilder builder = new ContactBuilder( + ContactBuilder.FORWARD_LOOKUP, null, phoneNumber); + builder.setName(ContactBuilder.Name.createDisplayName(displayName)); + builder.addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber)); + builder.addWebsite(ContactBuilder.WebsiteUrl.createProfile(profileUrl)); + + ContactBuilder.Address a = new ContactBuilder.Address(); + a.formattedAddress = address; + a.city = city; + a.type = StructuredPostal.TYPE_WORK; + builder.addAddress(a); + + if (photoUri != null) { + builder.setPhotoUri(photoUri); + } else { + builder.setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS); + } + + details.add(builder.build()); + } catch (JSONException e) { + Log.e(TAG, "Skipping the suggestions at index " + i, e); + } + } + + if (details.size() > 0) { + return details.toArray(new ContactInfo[details.size()]); + } else { + return null; + } + } + + /** + * 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..0e379d437 --- /dev/null +++ b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.opencnam; + +import android.content.Context; +import android.net.Uri; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.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 + */ + 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; + + ContactBuilder builder = new ContactBuilder( + ContactBuilder.REVERSE_LOOKUP, + normalizedNumber, formattedNumber); + builder.setName(ContactBuilder.Name.createDisplayName(displayName)); + builder.addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(number)); + builder.setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS); + + return builder.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..72ca04f5f --- /dev/null +++ b/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 The OmniROM Project + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Partially based on OmniROM's implementation + +package com.android.dialer.lookup.openstreetmap; + +import android.content.Context; +import android.location.Location; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.Log; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ForwardLookup; +import com.android.dialer.lookup.LookupUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.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 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 ContactInfo[] getEntries(JSONObject results) + throws JSONException { + ArrayList details = + new ArrayList(); + + JSONArray elements = results.getJSONArray(RESULT_ELEMENTS); + + for (int i = 0; i < elements.length(); i++) { + try { + JSONObject element = elements.getJSONObject(i); + JSONObject tags = element.getJSONObject(RESULT_TAGS); + + String displayName = tags.getString(TAG_NAME); + String phoneNumber = tags.getString(TAG_PHONE); + + // Take the first number if there are multiple + if (phoneNumber.contains(";")) { + phoneNumber = phoneNumber.split(";")[0]; + phoneNumber = phoneNumber.trim(); + } + + // The address is split + String addressHouseNumber = + tags.optString(TAG_HOUSENUMBER, null); + String addressStreet = tags.optString(TAG_STREET, null); + String addressCity = tags.optString(TAG_CITY, null); + String addressPostCode = tags.optString(TAG_POSTCODE, null); + + String address = String.format( + "%s %s, %s %s", + addressHouseNumber != null ? addressHouseNumber : "", + addressStreet != null ? addressStreet : "", + addressCity != null ? addressCity : "", + addressPostCode != null ? addressPostCode : ""); + + address = address.trim().replaceAll("\\s+", " "); + + if (address.length() == 0) { + address = null; + } + + String website = tags.optString(TAG_WEBSITE, null); + + ContactBuilder builder = new ContactBuilder( + ContactBuilder.FORWARD_LOOKUP, null, phoneNumber); + + builder.setName(ContactBuilder.Name.createDisplayName(displayName)); + builder.addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber)); + + 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); + + ContactBuilder.WebsiteUrl w = new ContactBuilder.WebsiteUrl(); + w.url = website; + w.type = Website.TYPE_HOMEPAGE; + builder.addWebsite(w); + + builder.setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS); + + details.add(builder.build()); + } catch (JSONException e) { + Log.e(TAG, "Skipping the suggestions at index " + i, e); + } + } + + if (details.size() > 0) { + return details.toArray(new ContactInfo[details.size()]); + } else { + return null; + } + } +} diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png new file mode 100644 index 000000000..f0bbe7345 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png new file mode 100644 index 000000000..f70e8e711 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png new file mode 100644 index 000000000..6409ab185 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png new file mode 100644 index 000000000..7c92a6030 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png new file mode 100644 index 000000000..97b982257 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png new file mode 100644 index 000000000..43029bd81 Binary files /dev/null and b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png differ diff --git a/java/com/android/dialer/lookup/res/values/cm_arrays.xml b/java/com/android/dialer/lookup/res/values/cm_arrays.xml new file mode 100644 index 000000000..a566727e3 --- /dev/null +++ b/java/com/android/dialer/lookup/res/values/cm_arrays.xml @@ -0,0 +1,53 @@ + + + + + Google + OpenStreetMap + + + + Google + OpenStreetMap + + + + Auskunft + + + + Auskunft (AT) + + + + Auskunft + DasTelefonbuch + OpenCnam + YellowPages + YellowPages_CA + ZabaSearch + + + + Auskunft (AT) + Das Telefonbuch (DE) + OpenCnam (US) + YellowPages (US) + YellowPages (CA) + ZabaSearch (US) + + diff --git a/java/com/android/dialer/lookup/res/values/cm_strings.xml b/java/com/android/dialer/lookup/res/values/cm_strings.xml new file mode 100644 index 000000000..28af84428 --- /dev/null +++ b/java/com/android/dialer/lookup/res/values/cm_strings.xml @@ -0,0 +1,33 @@ + + + + + Nearby places + People + + + Phone number lookup + Forward lookup + Show nearby places when searching in the dialer + People lookup + Show online results for people when searching in the dialer + Reverse lookup + Look up information about the person or place for unknown numbers on incoming calls + Forward lookup provider + People lookup provider + Reverse lookup provider + 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..5149b6324 --- /dev/null +++ b/java/com/android/dialer/lookup/res/xml/lookup_settings.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + 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..01f017ba9 --- /dev/null +++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.yellowpages; + +import android.content.Context; +import android.text.TextUtils; + +import com.android.dialer.lookup.LookupSettings; +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(); + + private static final String LOOKUP_URL_UNITED_STATES = + "https://www.yellowpages.com/phone?phone_search_terms="; + private static final String LOOKUP_URL_CANADA = + "https://www.yellowpages.ca/search/si/1/"; + + private String mProvider = null; + private String mNumber = null; + private String mOutput = null; + private ContactInfo mInfo = null; + private String mLookupUrl = null; + + public YellowPagesApi(Context context, String number) { + mProvider = LookupSettings.getReverseLookupProvider(context); + mNumber = number; + + if (mProvider.equals(LookupSettings.RLP_YELLOWPAGES)) { + mLookupUrl = LOOKUP_URL_UNITED_STATES; + } else if (mProvider.equals(LookupSettings.RLP_YELLOWPAGES_CA)) { + mLookupUrl = LOOKUP_URL_CANADA; + } + } + + private void fetchPage() throws IOException { + mOutput = LookupUtils.httpGet(mLookupUrl + mNumber, null); + } + + private String getPhotoUrl(String website) throws IOException { + String output = LookupUtils.httpGet(website, null); + String galleryRef = LookupUtils.firstRegexResult(output, + "href=\"([^\"]+gallery\\?lid=[^\"]+)\"", true); + if (galleryRef == null) { + return null; + } + + // Get first image + return LookupUtils.firstRegexResult( + LookupUtils.httpGet("https://www.yellowpages.com" + galleryRef, null), + "\"type\":\"image\",\"src\":\"([^\"]+)\"", true); + } + + private String[] parseNameWebsiteUnitedStates() { + Pattern regexNameAndWebsite = Pattern.compile( + "]+?)\"[^>]+?class=\"url[^>]+?>([^<]+)", + Pattern.DOTALL); + String name = null; + String website = null; + + Matcher m = regexNameAndWebsite.matcher(mOutput); + if (m.find()) { + website = m.group(1).trim(); + name = m.group(2).trim(); + } + + return new String[] { name, website }; + } + + private String[] parseNameWebsiteCanada() { + Pattern regexNameAndWebsite = Pattern.compile( + "class=\"ypgListingTitleLink utagLink\".*?href=\"(.*?)\">" + + "(.*?)", + Pattern.DOTALL); + String name = null; + String website = null; + + Matcher m = regexNameAndWebsite.matcher(mOutput); + 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(mOutput, + "business-phone.*?>\n*([^\n<]+)\n*<", true); + } + + private String parseNumberCanada() { + return LookupUtils.firstRegexResult(mOutput, + "(.*?)", true); + } + + private String parseAddressUnitedStates() { + String addressStreet = LookupUtils.firstRegexResult(mOutput, + "street-address.*?>\n*([^\n<]+)\n*<", true); + if (addressStreet != null && addressStreet.endsWith(",")) { + addressStreet = addressStreet.substring(0, addressStreet.length() - 1); + } + + String addressCity = LookupUtils.firstRegexResult(mOutput, + "locality.*?>\n*([^\n<]+)\n*<", true); + String addressState = LookupUtils.firstRegexResult(mOutput, + "region.*?>\n*([^\n<]+)\n*<", true); + String addressZip = LookupUtils.firstRegexResult(mOutput, + "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(); + if (address.length() == 0) { + address = null; + } + + return address; + } + + private String parseAddressCanada() { + String address = LookupUtils.firstRegexResult(mOutput, + "(.*?)", 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 (mProvider.equals(LookupSettings.RLP_YELLOWPAGES)) { + String[] ret = parseNameWebsiteUnitedStates(); + name = ret[0]; + website = ret[1]; + phoneNumber = parseNumberUnitedStates(); + address = parseAddressUnitedStates(); + if (website != null) { + photoUrl = getPhotoUrl(website); + } + } else if (mProvider.equals(LookupSettings.RLP_YELLOWPAGES_CA)) { + String[] ret = parseNameWebsiteCanada(); + name = ret[0]; + website = ret[1]; + phoneNumber = parseNumberCanada(); + address = parseAddressCanada(); + // AFAIK, Canada's YellowPages doesn't have photos + } + + ContactInfo info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = phoneNumber != null ? phoneNumber : mNumber; + info.website = website; + info.photoUrl = photoUrl; + mInfo = info; + } + + public ContactInfo getContactInfo() throws IOException { + if (mInfo == null) { + fetchPage(); + + buildContactInfo(); + } + + return mInfo; + } + + 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..4f7eb2420 --- /dev/null +++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.yellowpages; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.LookupUtils; +import com.android.dialer.lookup.ReverseLookup; + +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 java.io.FileNotFoundException; +import java.io.IOException; + +public class YellowPagesReverseLookup extends ReverseLookup { + private static final String TAG = + YellowPagesReverseLookup.class.getSimpleName(); + + public YellowPagesReverseLookup(Context context) { + } + + /** + * Lookup image + * + * @param context The application context + * @param uri The image URI + */ + 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(); + Bitmap bmp = BitmapFactory.decodeStream(cr.openInputStream(uri)); + return bmp; + } 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 + */ + public ContactInfo lookupNumber(Context context, + String normalizedNumber, String formattedNumber) throws IOException { + YellowPagesApi ypa = new YellowPagesApi(context, normalizedNumber); + YellowPagesApi.ContactInfo info = ypa.getContactInfo(); + + if (info.name == null) { + return null; + } + + ContactBuilder builder = new ContactBuilder( + ContactBuilder.REVERSE_LOOKUP, + normalizedNumber, formattedNumber); + + builder.setName(ContactBuilder.Name.createDisplayName(info.name)); + builder.addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber)); + builder.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..eeedb1289 --- /dev/null +++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.zabasearch; + +import android.text.TextUtils; + +import com.android.dialer.lookup.LookupUtils; + +import java.io.IOException; + +public class ZabaSearchApi { + private static final String TAG = ZabaSearchApi.class.getSimpleName(); + + private static final String LOOKUP_URL = "https://www.zabasearch.com/phone/"; + + private String mNumber = null; + public String mOutput = null; + private ContactInfo mInfo = null; + + public ZabaSearchApi(String number) { + mNumber = number; + } + + private void fetchPage() throws IOException { + mOutput = LookupUtils.httpGet(LOOKUP_URL + mNumber, null); + } + + private void buildContactInfo() { + // Name + String name = LookupUtils.firstRegexResult(mOutput, + "itemprop=\"?name\"?>([^<]+)<", true); + // Formatted phone number + String phoneNumber = LookupUtils.firstRegexResult(mOutput, + "itemprop=\"?telephone\"?>([^<]+)<", true); + // Address + String addressStreet = LookupUtils.firstRegexResult(mOutput, + "itemprop=\"?streetAddress\"?>([^<]+?)( )*<", true); + String addressCity = LookupUtils.firstRegexResult(mOutput, + "itemprop=\"?addressLocality\"?>([^<]+)<", true); + String addressState = LookupUtils.firstRegexResult(mOutput, + "itemprop=\"?addressRegion\"?>([^<]+)<", true); + String addressZip = LookupUtils.firstRegexResult(mOutput, + "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.length() == 0) { + address = null; + } + + ContactInfo info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = mNumber; + info.website = LOOKUP_URL + info.formattedNumber; + mInfo = info; + } + + public ContactInfo getContactInfo() throws IOException { + if (mInfo == null) { + fetchPage(); + + buildContactInfo(); + } + + return mInfo; + } + + 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..2b53628fc --- /dev/null +++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.zabasearch; + +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ReverseLookup; + +import android.content.Context; + +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 + */ + 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; + } + + ContactBuilder builder = new ContactBuilder( + ContactBuilder.REVERSE_LOOKUP, + normalizedNumber, formattedNumber); + + builder.setName(ContactBuilder.Name.createDisplayName(info.name)); + builder.addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber)); + builder.addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website)); + if (info.address != null) { + builder.addAddress(ContactBuilder.Address.createFormattedHome(info.address)); + } + + return builder.build(); + } +} diff --git a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java index 4fa3147eb..e91b6b163 100644 --- a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java +++ b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java @@ -42,6 +42,7 @@ import com.android.contacts.common.util.UriUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.logging.ContactSource; +import com.android.dialer.lookup.LookupCache; import com.android.dialer.oem.CequintCallerIdManager; import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact; import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo; @@ -433,6 +434,8 @@ public class ContactInfoHelper { // Contact found in the extended directory specified by directoryId info.sourceType = ContactSource.Type.SOURCE_TYPE_EXTENDED; } + } else if (LookupCache.hasCachedContact(mContext, number)) { + info = LookupCache.getCachedContact(mContext, number); } else if (mCachedNumberLookupService != null) { CachedContactInfo cacheInfo = mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number); -- cgit v1.2.3