diff options
author | Xiao-Long Chen <chenxiaolong@cxl.epac.to> | 2014-11-12 17:01:47 -0800 |
---|---|---|
committer | Steve Kondik <steve@cyngn.com> | 2015-03-21 15:42:25 -0700 |
commit | a3e67ace55a08e2d5da5d24d24d67c5b3af45bac (patch) | |
tree | 3b14e9b4e2d47c6f09d8ca62289627898e37e0a5 | |
parent | b8de1270fa595593fa4a39bef41de8ccff9c895a (diff) | |
download | android_packages_apps_Dialer-a3e67ace55a08e2d5da5d24d24d67c5b3af45bac.tar.gz android_packages_apps_Dialer-a3e67ace55a08e2d5da5d24d24d67c5b3af45bac.tar.bz2 android_packages_apps_Dialer-a3e67ace55a08e2d5da5d24d24d67c5b3af45bac.zip |
Dialer lookup.
Squashed commit of the following:
commit 934fbc79312a7cb0a4bd821d80af3b87f27c5beb
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Sun Feb 23 14:33:41 2014 -0500
New API
Change-Id: I0822407bb808382da56146ecf7e52cacb7cb8613
commit 4e81f04c716fead182cb453791d4a77f7eb54a89
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Sat Feb 15 21:04:15 2014 -0500
My old email is gone
Change-Id: Icfeccd195b9fbd269dc3400194dd42f215859049
commit 8a01ae35f9696275f1f156ea0486b1164a8d205e
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Sat Feb 15 15:12:55 2014 -0500
Dialer: Upgrade path for Google reverse lookup
Change-Id: I9e02beff958ed529f2520ac7023730392500606a
commit c0f2fae26eea774c1a5e5eae7d6ba97be32f16cc
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Sat Feb 15 13:38:56 2014 -0500
[1/2] Remove Google Reverse Lookup: Auth may violate terms of service
Change-Id: I2055770a43163cb2020daec6707fd45f0577584b
commit 8804367536eb38eb917c61172d9e4b6406b59c44
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Tue Feb 11 19:11:10 2014 -0500
[2/2] Dialer: Add YellowPages Canada reverse lookup provider
Change-Id: I07ca3932d024a5c834c25e23b2e16094ed1f974e
commit cdf5c18588b4b0e2ed0f12b5912c7ae90b51120a
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Tue Feb 11 12:27:53 2014 -0500
Nitpicks.
Change-Id: If8ce155246beb5395d81db8ab432046181b6d97f
commit 79aeb53db82c75adbb90f8f152e5d66d95706314
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Tue Feb 11 00:20:50 2014 -0500
[2/2] Dialer: Add WhitePages Canada reverse lookup provider
Change-Id: Ie4d5302945c39efca9f4b5fbf6dee9a63ec24184
commit c6fe12ffe357f209c2723c2c16aa6b853494a477
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Mon Feb 10 12:02:59 2014 -0500
Dialer: Use PackageManager to detect of Google Play Services is installed
Change-Id: I7b39867c0e8ec79c6c02c731ac27f78663358910
commit 9787c0b312df649840b55fbd35f5a6d87fceb5bb
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Fri Jan 31 22:41:09 2014 -0500
Dialer: Add support for forward and reverse lookups
Change-Id: I848db0bbbe6a648064d1eace4993814a16aa9fa0
Change-Id: Iec295b05d72680f770367fcaf1dae9ec68b3d6e9
commit 71b121d082d777e54f7fc02338b609cbfc31ff9c
Author: Xiao-Long Chen <chenxiaolong@cxl.epac.to>
Date: Sat Mar 22 15:32:10 2014 -0400
Dialer: Add support for OpenCNAM paid accounts
Change-Id: I09c67eed706311a40569617980585a357f1d74aa
commit 50cc4c8615d99e92a481981289ff9a7ec83b4c04
Author: Ricardo Cerqueira <cyanogenmod@cerqueira.org>
Date: Mon Jun 2 13:31:15 2014 -0700
ReverseLookup: Don't return OpenCNAM errors as caller IDs
Skip the API entirely for non-US numbers, and ignore any non-200
bodies to avoid names of the "Limit Exceeded" variety
PS2: Use constant from HttpStatus and don't check for null on normalizedNumber
Change-Id: I1ea107ba828e6dba44fa0828ad5fedecb8faf4de
commit 7fde43587525973b2172123c8696d06f2fd67e37
Author: Dan Pasanen <dan.pasanen@gmail.com>
Date: Mon Jun 2 17:08:18 2014 -0500
OpenCnameReverseLookup: fix import class
Change-Id: Ib10877aecbdc1e07f8912e1ce35df6e8a112f131
commit 6605b6292df97b66785a06927d7bcd28c217d121
Author: Adnan <adnan@cyngn.com>
Date: Tue Jun 10 19:02:33 2014 -0700
Chinese Location Lookup [1/2] Dialer: Chinese Reverse Lookup
PS6: Add static integers for reference
Change-Id: I22ede59cfa8785ac04ad1b1f19d1c69e24b9fb89
commit cff06b01e0a4c4487d69be7f21de1ddb7f663f38
Author: Danny Baumann <dannybaumann@web.de>
Date: Wed Aug 27 09:04:14 2014 +0200
Add reverse lookup provider "Das Telefonbuch" (DE).
Change-Id: I0ad72bb3e57da3d27fb1c50c58c112234c39b585
commit 887609d1f6d00d883783e9d03bf16b24e716e234
Author: Danny Baumann <dannybaumann@web.de>
Date: Wed Sep 3 09:09:19 2014 +0200
Refine regexes for Das Telefonbuch.
Looks like private and business numbers have a slightly differing
format.
Change-Id: I7eae982fe81cf890686f49b88417e604ba0171a0
commit e64d747e3ae87cc6ddf83b96882e7bcad6ebcd31
Author: Danny Baumann <dannybaumann@web.de>
Date: Thu Oct 2 12:55:23 2014 +0200
Code cleanup.
Avoid repetetive boilerplate code by using helper methods.
Change-Id: Id3ce8d07d35d59c08cbc67fa35bbf6daa67c9608
commit c10bafdd9bab3af4e9dca4fae749fd0ab2202acd
Author: Adnan <adnan@cyngn.com>
Date: Wed Oct 1 17:13:14 2014 -0700
Dialer: Scrub debug logging for identifiable information.
Change-Id: I2b45ec7c37a7f69c972feb3dde1eac87306259f6
Change-Id: I4445ac7a3aa7f60a912fe33dde1eac87306259f2
39 files changed, 3746 insertions, 4 deletions
diff --git a/Android.mk b/Android.mk index 6c32c32c0..c7323411f 100644 --- a/Android.mk +++ b/Android.mk @@ -19,6 +19,7 @@ res_dirs := res \ LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs)) LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) +LOCAL_ASSET_DIR := $(LOCAL_PATH)/assets LOCAL_AAPT_FLAGS := \ --auto-add-overlay \ diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 591f4615b..0a2be776b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -302,5 +302,11 @@ <action android:name="android.telecom.InCallService"/> </intent-filter> </service> + + <provider android:name="com.android.dialer.lookup.LookupProvider" + android:authorities="com.android.dialer.provider" + android:exported="false" + android:multiprocess="false" /> + </application> </manifest> diff --git a/assets/contacts_extensions.properties b/assets/contacts_extensions.properties new file mode 100644 index 000000000..535253f4a --- /dev/null +++ b/assets/contacts_extensions.properties @@ -0,0 +1 @@ +extendedPhoneDirectories=com.android.dialer.lookup.ExtendedLookupDirectories diff --git a/proguard.flags b/proguard.flags index 38d4050de..d1a0057ee 100644 --- a/proguard.flags +++ b/proguard.flags @@ -12,4 +12,9 @@ @com.android.dialer.NeededForReflection *; } +# Keep ExtendedLookupDirectories for assets/contacts_extensions.properties +-keep class com.android.dialer.lookup.ExtendedLookupDirectories extends * { + *; +} + -verbose diff --git a/res/drawable-hdpi/ic_places_picture_180_holo_light.png b/res/drawable-hdpi/ic_places_picture_180_holo_light.png Binary files differnew file mode 100644 index 000000000..f0bbe7345 --- /dev/null +++ b/res/drawable-hdpi/ic_places_picture_180_holo_light.png diff --git a/res/drawable-hdpi/ic_places_picture_holo_light.png b/res/drawable-hdpi/ic_places_picture_holo_light.png Binary files differnew file mode 100644 index 000000000..f70e8e711 --- /dev/null +++ b/res/drawable-hdpi/ic_places_picture_holo_light.png diff --git a/res/drawable-xhdpi/ic_places_picture_180_holo_light.png b/res/drawable-xhdpi/ic_places_picture_180_holo_light.png Binary files differnew file mode 100644 index 000000000..6409ab185 --- /dev/null +++ b/res/drawable-xhdpi/ic_places_picture_180_holo_light.png diff --git a/res/drawable-xhdpi/ic_places_picture_holo_light.png b/res/drawable-xhdpi/ic_places_picture_holo_light.png Binary files differnew file mode 100644 index 000000000..7c92a6030 --- /dev/null +++ b/res/drawable-xhdpi/ic_places_picture_holo_light.png diff --git a/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png b/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png Binary files differnew file mode 100644 index 000000000..97b982257 --- /dev/null +++ b/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png diff --git a/res/drawable-xxhdpi/ic_places_picture_holo_light.png b/res/drawable-xxhdpi/ic_places_picture_holo_light.png Binary files differnew file mode 100644 index 000000000..43029bd81 --- /dev/null +++ b/res/drawable-xxhdpi/ic_places_picture_holo_light.png diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml new file mode 100644 index 000000000..6c0a5596a --- /dev/null +++ b/res/values/cm_strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013-2014 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Forward lookup --> + <string name="nearby_places">Nearby places</string> + <string name="people">People</string> +</resources> diff --git a/src/com/android/dialer/calllog/ClearCallLogDialog.java b/src/com/android/dialer/calllog/ClearCallLogDialog.java index bef5010ec..ec28aec62 100644 --- a/src/com/android/dialer/calllog/ClearCallLogDialog.java +++ b/src/com/android/dialer/calllog/ClearCallLogDialog.java @@ -31,6 +31,7 @@ import android.os.Bundle; import android.provider.CallLog.Calls; import com.android.dialer.R; +import com.android.dialer.lookup.LookupCache; import com.android.dialer.service.CachedNumberLookupService; import com.android.dialerbind.ObjectFactory; @@ -65,6 +66,7 @@ public class ClearCallLogDialog extends DialogFragment { if (mCachedNumberLookupService != null) { mCachedNumberLookupService.clearAllCacheEntries(context); } + LookupCache.deleteCachedContacts(context); return null; } @Override diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java index bef3520e3..68654f28c 100644 --- a/src/com/android/dialer/calllog/ContactInfoHelper.java +++ b/src/com/android/dialer/calllog/ContactInfoHelper.java @@ -30,6 +30,7 @@ import android.text.TextUtils; import com.android.contacts.common.util.Constants; import com.android.contacts.common.util.PhoneNumberHelper; import com.android.contacts.common.util.UriUtils; +import com.android.dialer.lookup.LookupCache; import com.android.dialer.service.CachedNumberLookupService; import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; import com.android.dialerbind.ObjectFactory; @@ -245,6 +246,8 @@ public class ContactInfoHelper { ContactInfo info = lookupContactFromUri(uri); if (info != null && info != ContactInfo.EMPTY) { info.formattedNumber = formatPhoneNumber(number, null, countryIso); + } else if (LookupCache.hasCachedContact(mContext, number)) { + info = LookupCache.getCachedContact(mContext, number); } else if (mCachedNumberLookupService != null) { CachedContactInfo cacheInfo = mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number); diff --git a/src/com/android/dialer/database/DialerDatabaseHelper.java b/src/com/android/dialer/database/DialerDatabaseHelper.java index 5bdd95d81..737999707 100644 --- a/src/com/android/dialer/database/DialerDatabaseHelper.java +++ b/src/com/android/dialer/database/DialerDatabaseHelper.java @@ -77,7 +77,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { * 0-98 KitKat * </pre> */ - public static final int DATABASE_VERSION = 4; + public static final int DATABASE_VERSION = 70004; public static final String DATABASE_NAME = "dialer.db"; /** @@ -437,7 +437,10 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { Log.e(TAG, "Malformed database version..recreating database"); } - if (oldVersion < 4) { + int base = 70000; + db.execSQL("DROP TABLE IF EXISTS " + "cached_number_contacts"); + if (oldVersion <= (DATABASE_VERSION - base) + || (oldVersion >= base && oldVersion < DATABASE_VERSION)) { setupTables(db); return; } diff --git a/src/com/android/dialer/list/RegularSearchFragment.java b/src/com/android/dialer/list/RegularSearchFragment.java index 9f4e6bec9..c4af4f570 100644 --- a/src/com/android/dialer/list/RegularSearchFragment.java +++ b/src/com/android/dialer/list/RegularSearchFragment.java @@ -21,6 +21,7 @@ import android.view.ViewGroup; import com.android.contacts.common.list.ContactEntryListAdapter; import com.android.contacts.common.list.PinnedHeaderListView; import com.android.dialerbind.ObjectFactory; +import com.android.dialer.lookup.LookupCache; import com.android.dialer.service.CachedNumberLookupService; public class RegularSearchFragment extends SearchFragment { @@ -54,11 +55,13 @@ public class RegularSearchFragment extends SearchFragment { @Override protected void cacheContactInfo(int position) { - if (mCachedNumberLookupService != null) { - final RegularSearchListAdapter adapter = + final RegularSearchListAdapter adapter = (RegularSearchListAdapter) getAdapter(); + if (mCachedNumberLookupService != null) { mCachedNumberLookupService.addContact(getContext(), adapter.getContactInfo(mCachedNumberLookupService, position)); } + LookupCache.cacheContact(getActivity(), + adapter.getLookupContactInfo(position)); } } diff --git a/src/com/android/dialer/list/RegularSearchListAdapter.java b/src/com/android/dialer/list/RegularSearchListAdapter.java index 7f45a68b7..f6adce915 100644 --- a/src/com/android/dialer/list/RegularSearchListAdapter.java +++ b/src/com/android/dialer/list/RegularSearchListAdapter.java @@ -37,6 +37,20 @@ public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter { super(context); } + 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(); diff --git a/src/com/android/dialer/lookup/ContactBuilder.java b/src/com/android/dialer/lookup/ContactBuilder.java new file mode 100644 index 000000000..069045db2 --- /dev/null +++ b/src/com/android/dialer/lookup/ContactBuilder.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.contacts.common.util.Constants; +import com.android.dialer.R; +import com.android.dialer.calllog.ContactInfo; + +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.util.Log; + +import java.util.ArrayList; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +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<Address> mAddresses = new ArrayList<Address>(); + private ArrayList<PhoneNumber> mPhoneNumbers + = new ArrayList<PhoneNumber>(); + private ArrayList<WebsiteUrl> mWebsites + = new ArrayList<WebsiteUrl>(); + + private int mDirectoryType; + + 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 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 Website[] getWebsites() { + return mWebsites.toArray(new Website[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) { + 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 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; + if (mDirectoryType == FORWARD_LOOKUP + || mDirectoryType == PEOPLE_LOOKUP) { + directoryId = ContactsContract.Directory.DEFAULT; + } else if (mDirectoryType == REVERSE_LOOKUP) { + directoryId = Long.MAX_VALUE; + } + + 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 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 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 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 String toString() { + return "url: " + url + "; " + + "type: " + type + "; " + + "label: " + label; + } + } +} diff --git a/src/com/android/dialer/lookup/ExtendedLookupDirectories.java b/src/com/android/dialer/lookup/ExtendedLookupDirectories.java new file mode 100644 index 000000000..4a505603a --- /dev/null +++ b/src/com/android/dialer/lookup/ExtendedLookupDirectories.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.R; + +import com.android.contacts.common.extensions.ExtendedPhoneDirectoriesManager; +import com.android.contacts.common.list.DirectoryPartition; + +import android.content.Context; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class ExtendedLookupDirectories + implements ExtendedPhoneDirectoriesManager { + public static final String TAG = + ExtendedLookupDirectories.class.getSimpleName(); + + /** + * Return a list of extended directories to add. May return null if no directories are to be + * added. + */ + @Override + public List<DirectoryPartition> getExtendedDirectories(Context context) { + ArrayList<DirectoryPartition> list = new ArrayList<DirectoryPartition>(); + + // 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(LookupProvider.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(LookupProvider.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; + } +} diff --git a/src/com/android/dialer/lookup/ForwardLookup.java b/src/com/android/dialer/lookup/ForwardLookup.java new file mode 100644 index 000000000..d67d00094 --- /dev/null +++ b/src/com/android/dialer/lookup/ForwardLookup.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.calllog.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/src/com/android/dialer/lookup/LookupCache.java b/src/com/android/dialer/lookup/LookupCache.java new file mode 100644 index 000000000..e0dc66c15 --- /dev/null +++ b/src/com/android/dialer/lookup/LookupCache.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chenxiaolong@cxl.epac.to> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.calllog.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 libcore.io.IoUtils; + +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 { + IoUtils.closeQuietly(writer); + IoUtils.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 { + IoUtils.closeQuietly(reader); + IoUtils.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 { + IoUtils.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/src/com/android/dialer/lookup/LookupProvider.java b/src/com/android/dialer/lookup/LookupProvider.java new file mode 100644 index 000000000..4906d3232 --- /dev/null +++ b/src/com/android/dialer/lookup/LookupProvider.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.R; + +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 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.LinkedList; + +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 final String AUTHORITY = "com.android.dialer.provider"; + 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<FutureTask> mActiveTasks = + new LinkedList<FutureTask>(); + + 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<T> implements Callable<T> { + private final Callable<T> mCallable; + private volatile FutureTask<T> mFuture; + + public FutureCallable(Callable<T> 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<T> 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); + + final int match = sURIMatcher.match(uri); + + switch (match) { + case NEARBY: + case PEOPLE: + Context context = getContext(); + if (!isLocationEnabled()) { + Log.v(TAG, "Location settings is disabled, ignoring query."); + return null; + } + + final Location lastLocation = getLastLocation(); + if (lastLocation == null) { + Log.v(TAG, "No location available, ignoring query."); + return null; + } + + 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 int finalMaxResults = maxResults; + + return execute(new Callable<Cursor>() { + @Override + public Cursor call() { + return handleFilter(match, projection, filter, + finalMaxResults, lastLocation); + } + }, "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> T execute(Callable<T> callable, String name) { + FutureCallable<T> futureCallable = new FutureCallable<T>(callable); + FutureTask<T> future = new FutureTask<T>(futureCallable); + futureCallable.setFuture(future); + + synchronized (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/src/com/android/dialer/lookup/LookupSettings.java b/src/com/android/dialer/lookup/LookupSettings.java new file mode 100644 index 000000000..3d969a32d --- /dev/null +++ b/src/com/android/dialer/lookup/LookupSettings.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.provider.Settings; + +import 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_WHITEPAGES = "WhitePages"; + public static final String PLP_DEFAULT = PLP_WHITEPAGES; + + /** Reverse lookup providers */ + public static final String RLP_OPENCNAM = "OpenCnam"; + public static final String RLP_WHITEPAGES = "WhitePages"; + public static final String RLP_WHITEPAGES_CA = "WhitePages_CA"; + 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_DEFAULT = RLP_OPENCNAM; + + private LookupSettings() { + } + + public static boolean isForwardLookupEnabled(Context context) { + return Settings.System.getInt(context.getContentResolver(), + Settings.System.ENABLE_FORWARD_LOOKUP, 1) != 0; + } + + public static boolean isPeopleLookupEnabled(Context context) { + return Settings.System.getInt(context.getContentResolver(), + Settings.System.ENABLE_PEOPLE_LOOKUP, 1) != 0; + } + + public static boolean isReverseLookupEnabled(Context context) { + return Settings.System.getInt(context.getContentResolver(), + Settings.System.ENABLE_REVERSE_LOOKUP, 1) != 0; + } + + public static String getForwardLookupProvider(Context context) { + String provider = getLookupProvider(context, + Settings.System.FORWARD_LOOKUP_PROVIDER, FLP_DEFAULT); + + return provider; + } + + public static String getPeopleLookupProvider(Context context) { + String provider = getLookupProvider(context, + Settings.System.PEOPLE_LOOKUP_PROVIDER, PLP_DEFAULT); + + return provider; + } + + public static String getReverseLookupProvider(Context context) { + String provider = getLookupProvider(context, + Settings.System.REVERSE_LOOKUP_PROVIDER, RLP_DEFAULT); + + if ("Google".equals(provider)) { + Settings.System.putString(context.getContentResolver(), + Settings.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 = Settings.System.getString(cr, key); + + if (provider == null) { + Settings.System.putString(cr, key, defaultValue); + return defaultValue; + } + + return provider; + } +} diff --git a/src/com/android/dialer/lookup/LookupUtils.java b/src/com/android/dialer/lookup/LookupUtils.java new file mode 100644 index 000000000..c0b84dc38 --- /dev/null +++ b/src/com/android/dialer/lookup/LookupUtils.java @@ -0,0 +1,84 @@ +/* + * 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 org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +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:26.0) Gecko/20100101 Firefox/26.0"; + + public static String httpGet(HttpGet request) throws IOException { + HttpClient client = new DefaultHttpClient(); + + request.setHeader("User-Agent", USER_AGENT); + + HttpResponse response = client.execute(request); + int status = response.getStatusLine().getStatusCode(); + + // Android's org.apache.http doesn't have the RedirectStrategy class + if (status == HttpStatus.SC_MOVED_PERMANENTLY + || status == HttpStatus.SC_MOVED_TEMPORARILY) { + Header[] headers = response.getHeaders("Location"); + + if (headers != null && headers.length != 0) { + HttpGet newGet = new HttpGet(headers[headers.length - 1].getValue()); + for (Header header : request.getAllHeaders()) { + newGet.addHeader(header); + } + return httpGet(newGet); + } else { + throw new IOException("Empty redirection header"); + } + } + + if (status != HttpStatus.SC_OK) { + throw new IOException("HTTP failure (status " + status + ")"); + } + + return EntityUtils.toString(response.getEntity()); + } + + 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/src/com/android/dialer/lookup/PeopleLookup.java b/src/com/android/dialer/lookup/PeopleLookup.java new file mode 100644 index 000000000..08c3d7dc5 --- /dev/null +++ b/src/com/android/dialer/lookup/PeopleLookup.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.whitepages.WhitePagesPeopleLookup; + +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_WHITEPAGES)) { + INSTANCE = new WhitePagesPeopleLookup(context); + } + } + + return INSTANCE; + } + + private static boolean isInstance(String provider) { + if (provider.equals(LookupSettings.PLP_WHITEPAGES) + && INSTANCE instanceof WhitePagesPeopleLookup) { + return true; + } else { + return false; + } + } + + public abstract ContactInfo[] lookup(Context context, + String filter); +} diff --git a/src/com/android/dialer/lookup/ReverseLookup.java b/src/com/android/dialer/lookup/ReverseLookup.java new file mode 100644 index 000000000..79ee96ebd --- /dev/null +++ b/src/com/android/dialer/lookup/ReverseLookup.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.cyngn.CyngnChineseReverseLookup; +import com.android.dialer.lookup.dastelefonbuch.TelefonbuchReverseLookup; +import com.android.dialer.lookup.opencnam.OpenCnamReverseLookup; +import com.android.dialer.lookup.whitepages.WhitePagesReverseLookup; +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_WHITEPAGES) + || provider.equals(LookupSettings.RLP_WHITEPAGES_CA)) { + INSTANCE = new WhitePagesReverseLookup(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_CYNGN_CHINESE)) { + INSTANCE = new CyngnChineseReverseLookup(context); + } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH)) { + INSTANCE = new TelefonbuchReverseLookup(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_WHITEPAGES) + || provider.equals(LookupSettings.RLP_WHITEPAGES_CA)) + && INSTANCE instanceof WhitePagesReverseLookup) { + 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_CYNGN_CHINESE) + && INSTANCE instanceof CyngnChineseReverseLookup) { + return true; + } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH) + && INSTANCE instanceof TelefonbuchReverseLookup) { + 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/src/com/android/dialer/lookup/ReverseLookupThread.java b/src/com/android/dialer/lookup/ReverseLookupThread.java new file mode 100644 index 000000000..31c344265 --- /dev/null +++ b/src/com/android/dialer/lookup/ReverseLookupThread.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import com.android.contacts.common.GeoUtil; +import com.android.dialer.calllog.ContactInfo; +import com.android.incallui.ContactInfoCache; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +public class ReverseLookupThread extends Thread { + private static final String TAG = ReverseLookupThread.class.getSimpleName(); + + private static final ExecutorService mExecutorService = + Executors.newFixedThreadPool(2); + private static final Handler mHandler = new Handler(Looper.getMainLooper()); + + private final Context mContext; + private final ContactInfoCache.ReverseLookupListener mListener; + private final String mNormalizedNumber; + private final String mFormattedNumber; + + public static void performLookup(Context context, String number, + ContactInfoCache.ReverseLookupListener listener) { + try { + mExecutorService.execute( + new ReverseLookupThread(context, number, listener)); + } catch (Exception e) { + Log.e(TAG, "Failed to perform reverse lookup", e); + } + } + + private ReverseLookupThread(Context context, String number, + ContactInfoCache.ReverseLookupListener listener) { + mContext = context; + mListener = listener; + String countryIso = ((TelephonyManager) mContext.getSystemService( + Context.TELEPHONY_SERVICE)).getSimCountryIso().toUpperCase(); + mNormalizedNumber = PhoneNumberUtils + .formatNumberToE164(number, countryIso); + mFormattedNumber = PhoneNumberUtils.formatNumber(number, + mNormalizedNumber, GeoUtil.getCurrentCountryIso(mContext)); + } + + @Override + public void run() { + if (!LookupSettings.isReverseLookupEnabled(mContext)) { + LookupCache.deleteCachedContacts(mContext); + return; + } + + // Can't do reverse lookup without a number + if (mNormalizedNumber == null || mFormattedNumber == null) { + return; + } + + ContactInfo info = null; + + if (LookupCache.hasCachedContact(mContext, mNormalizedNumber)) { + info = LookupCache.getCachedContact(mContext, mNormalizedNumber); + + if (ContactInfo.EMPTY.equals(info)) { + // If we have an empty cached contact, remove it and redo lookup + LookupCache.deleteCachedContact(mContext, mNormalizedNumber); + info = null; + } + } + + // Lookup contact if it's not cached + if (info == null) { + try { + info = ReverseLookup.getInstance(mContext).lookupNumber(mContext, + mNormalizedNumber, mFormattedNumber); + } catch (IOException e) { + // ignored, we'll return below + } + + if (info == null) { + return; + } + + // Put in cache only if the contact is valid + if (info.equals(ContactInfo.EMPTY)) { + return; + } else if (info.name != null) { + LookupCache.cacheContact(mContext, info); + } + } + + final ContactInfo infoFinal = info; + + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onLookupComplete(infoFinal); + } + }); + + if (info.photoUri != null) { + if (!LookupCache.hasCachedImage(mContext, mNormalizedNumber)) { + Bitmap bmp = ReverseLookup.getInstance(mContext).lookupImage( + mContext, info.photoUri); + + if (bmp != null) { + LookupCache.cacheImage(mContext, mNormalizedNumber, bmp); + } + } + + final Bitmap bmp = LookupCache.getCachedImage( + mContext, mNormalizedNumber); + + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onImageFetchComplete(bmp); + } + }); + } + } +} diff --git a/src/com/android/dialer/lookup/cyngn/CyngnChineseReverseLookup.java b/src/com/android/dialer/lookup/cyngn/CyngnChineseReverseLookup.java new file mode 100644 index 000000000..355208a18 --- /dev/null +++ b/src/com/android/dialer/lookup/cyngn/CyngnChineseReverseLookup.java @@ -0,0 +1,99 @@ +/* + * 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.cyngn; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.util.Log; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ReverseLookup; + +public class CyngnChineseReverseLookup extends ReverseLookup { + + private static final String TAG = CyngnChineseReverseLookup.class.getSimpleName(); + + private static final int COMMON_CHINESE_PHONE_NUMBER_LENGTH = 11; + private static final int COMMON_CHINESE_PHONE_NUMBER_AREANO_START = 2; + private static final int COMMON_CHINESE_PHONE_NUMBER_AREANO_END = 5; + + private static final boolean DEBUG = false; + private static final Uri PROVIDER_URI = + Uri.parse("content://com.cyngn.chineselocationlookup.provider"); + + public CyngnChineseReverseLookup(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) { + String displayName = queryProvider(context, normalizedNumber); + if (displayName == null) { + return null; + } + + if (DEBUG) Log.d(TAG, "Reverse lookup returned name: " + displayName); + + 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 queryProvider(Context context, String normalizedNumber) { + if (normalizedNumber.length() < COMMON_CHINESE_PHONE_NUMBER_LENGTH) { + return null; + } + + //trim carrier code, and get area prefix + String areaPrefix = normalizedNumber.substring(COMMON_CHINESE_PHONE_NUMBER_AREANO_START, + COMMON_CHINESE_PHONE_NUMBER_AREANO_END); + + ContentResolver resolver = context.getContentResolver(); + Cursor cursor = context.getContentResolver().query(PROVIDER_URI, + null, null, new String[] { areaPrefix }, null); + if (cursor == null) { + return null; + } + + try { + if (cursor.moveToFirst()) { + return cursor.getString(2); + } + } finally { + cursor.close(); + } + return null; + } +} diff --git a/src/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java b/src/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java new file mode 100644 index 000000000..2d9ed3e5f --- /dev/null +++ b/src/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2014 Danny Baumann <dannybaumann@web.de> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.dastelefonbuch; + +import android.content.Context; +import android.net.Uri; + +import com.android.dialer.lookup.LookupUtils; + +import org.apache.http.client.methods.HttpGet; + +import java.io.IOException; + +public class TelefonbuchApi { + private static final String TAG = TelefonbuchApi.class.getSimpleName(); + + private static final String REVERSE_LOOKUP_URL = + "http://www.dastelefonbuch.de/?s=a20000" + + "&cmd=search&sort_ok=0&sp=55&vert_ok=0&aktion=23"; + + private static String NAME_REGEX ="<a id=\"name0.*?>\\s*\n?(.*?)\n?\\s*</a>"; + private static String NUMBER_REGEX = "<span\\s+class=\"ico fon.*>.*<span>(.*?)</span><br/>"; + private static String ADDRESS_REGEX = "<address.*?>\n?(.*?)</address>"; + + private TelefonbuchApi() { + } + + public static ContactInfo reverseLookup(Context context, String number) + throws IOException { + Uri uri = Uri.parse(REVERSE_LOOKUP_URL) + .buildUpon() + .appendQueryParameter("kw", number) + .build(); + // Cut out everything we're not interested in (scripts etc.) to + // speed up the subsequent matching. + String output = LookupUtils.firstRegexResult( + LookupUtils.httpGet(new HttpGet(uri.toString())), + ": 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) { + result = result.replaceAll("</?span.*?>", ""); + } + return LookupUtils.fromHtml(result); + } + + public static class ContactInfo { + String name; + String address; + String formattedNumber; + String website; + } +} diff --git a/src/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java b/src/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java new file mode 100644 index 000000000..a96a77260 --- /dev/null +++ b/src/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 Danny Baumann <dannybaumann@web.de> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.dastelefonbuch; + +import android.content.Context; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; + +import com.android.dialer.calllog.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/src/com/android/dialer/lookup/google/GoogleForwardLookup.java b/src/com/android/dialer/lookup/google/GoogleForwardLookup.java new file mode 100644 index 000000000..215cbfd3b --- /dev/null +++ b/src/com/android/dialer/lookup/google/GoogleForwardLookup.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.google; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ForwardLookup; + +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.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; + +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()); + + String httpResponse = httpGetRequest( + builder.build().toString()); + + JSONArray results = new JSONArray(httpResponse); + + 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<ContactInfo> details = + new ArrayList<ContactInfo>(); + + 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); + } + + /** + * Fetch a URL and return the response as a string encoded in either + * UTF-8 or the charset specified in the Content-Type header. + * + * @param url URL + * @return Response from server + */ + private String httpGetRequest(String url) throws IOException { + HttpClient client = new DefaultHttpClient(); + HttpGet request = new HttpGet(url.toString()); + + request.setHeader("User-Agent", mUserAgent); + + HttpResponse response = client.execute(request); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.getEntity().writeTo(out); + + String charset = getCharsetFromContentType( + response.getEntity().getContentType().getValue()); + + return new String(out.toByteArray(), charset); + } + + /** + * Extract the content encoding from the HTTP 'Content-Type' header. + * + * @param contentType The 'Content-Type' header + * @return The charset or "UTF-8" + */ + private static String getCharsetFromContentType(String contentType) { + String[] split = contentType.split(";"); + + for (int i = 0; i < split.length; i++) { + String trimmed = split[i].trim(); + if (trimmed.startsWith("charset=")) { + return trimmed.substring(8); + } + } + + return "UTF-8"; + } + + /** + * 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/src/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java b/src/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java new file mode 100644 index 000000000..9dafab785 --- /dev/null +++ b/src/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.opencnam; + +import android.content.Context; +import android.net.Uri; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.LookupUtils; +import com.android.dialer.lookup.ReverseLookup; + +import org.apache.http.client.methods.HttpGet; + +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 = Settings.System.getString( + context.getContentResolver(), + Settings.System.DIALER_OPENCNAM_ACCOUNT_SID); + String authToken = Settings.System.getString( + context.getContentResolver(), + Settings.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(new HttpGet(builder.build().toString())); + } +} diff --git a/src/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java b/src/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java new file mode 100644 index 000000000..5bd5e723f --- /dev/null +++ b/src/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2014 The OmniROM Project + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Partially based on OmniROM's implementation + +package com.android.dialer.lookup.openstreetmap; + +import android.content.Context; +import android.location.Location; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.Log; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ForwardLookup; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; + +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 = + "http://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 { + String httpResponse = httpPostRequest(request); + + JSONObject results = new JSONObject(httpResponse); + + 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; + } + + private ContactInfo[] getEntries(JSONObject results) + throws JSONException { + ArrayList<ContactInfo> details = + new ArrayList<ContactInfo>(); + + 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; + } + } + + private String httpPostRequest(String query) throws IOException { + HttpClient client = new DefaultHttpClient(); + HttpPost post = new HttpPost(LOOKUP_URL); + + post.setEntity(new StringEntity(query)); + + return EntityUtils.toString(client.execute(post).getEntity()); + } +} diff --git a/src/com/android/dialer/lookup/whitepages/WhitePagesApi.java b/src/com/android/dialer/lookup/whitepages/WhitePagesApi.java new file mode 100644 index 000000000..5b266bf9c --- /dev/null +++ b/src/com/android/dialer/lookup/whitepages/WhitePagesApi.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chenxiaolong@cxl.epac.to> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.whitepages; + +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import com.android.dialer.lookup.LookupSettings; +import com.android.dialer.lookup.LookupUtils; + +import org.apache.http.client.methods.HttpGet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WhitePagesApi { + private static final String TAG = WhitePagesApi.class.getSimpleName(); + + public static final int UNITED_STATES = 0; + public static final int CANADA = 1; + + private static final String NEARBY_URL_UNITED_STATES = + "http://www.whitepages.com/search/ReversePhone?full_phone="; + private static final String NEARBY_URL_CANADA = + "http://www.whitepages.ca/search/ReversePhone?full_phone="; + + private static final String PEOPLE_URL_UNITED_STATES = + "http://whitepages.com/search/FindPerson"; + + private static final String USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0"; + private static final String COOKIE_REGEX = "distil_RID=([A-Za-z0-9\\-]+)"; + private static final String COOKIE = "D_UID"; + + private static String mCookie; + + private WhitePagesApi() { + } + + public static ContactInfo[] peopleLookup(Context context, String name, + int maxResults) throws IOException { + String provider = LookupSettings.getPeopleLookupProvider(context); + + if (LookupSettings.PLP_WHITEPAGES.equals(provider)) { + Uri.Builder builder = Uri.parse(PEOPLE_URL_UNITED_STATES) + .buildUpon(); + builder.appendQueryParameter("who", name); + String lookupUrl = builder.build().toString(); + String output = httpGet(lookupUrl); + return parseOutputUnitedStates(output, maxResults); + } + // no-op + return null; + } + + private static ContactInfo[] parseOutputUnitedStates(String output, + int maxResults) throws IOException { + ArrayList<ContactInfo> people = new ArrayList<ContactInfo>(); + + Pattern regex = Pattern.compile( + "<li\\s[^>]+?http:\\/\\/schema\\.org\\/Person", Pattern.DOTALL); + Matcher m = regex.matcher(output); + + while (m.find()) { + if (people.size() == maxResults) { + break; + } + + // Find section of HTML with contact information + String section = extractXmlTag(output, m.start(), m.end(), "li"); + + // Skip entries with no phone number + if (section.contains("has-no-phone-icon")) { + continue; + } + + String name = LookupUtils.fromHtml(extractXmlRegex(section, + "<span[^>]+?itemprop=\"name\">", "span")); + + if (name == null) { + continue; + } + + // Address + String addrCountry = LookupUtils.fromHtml(extractXmlRegex(section, + "<span[^>]+?itemprop=\"addressCountry\">", "span")); + String addrState = LookupUtils.fromHtml(extractXmlRegex(section, + "<span[^>]+?itemprop=\"addressRegion\">", "span")); + String addrCity = LookupUtils.fromHtml(extractXmlRegex(section, + "<span[^>]+?itemprop=\"addressLocality\">", "span")); + + StringBuilder sb = new StringBuilder(); + + if (addrCity != null) { + sb.append(addrCity); + } + if (addrState != null) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(addrState); + } + if (addrCountry != null) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(addrCountry); + } + + // Website + Pattern p = Pattern.compile("href=\"(.+?)\""); + Matcher m2 = p.matcher(section); + String website = null; + if (m2.find()) { + website = "http://www.whitepages.com" + m2.group(1); + } + + // Phone number is on profile page, so skip if we can't get the + // website + if (website == null) { + continue; + } + + String profile = httpGet(website); + String phoneNumber = LookupUtils.fromHtml(extractXmlRegex(profile, + "<li[^>]+?class=\"no-overflow tel\">", "li")); + String address = parseAddressUnitedStates(profile); + + if (phoneNumber == null) { + Log.e(TAG, "Phone number is null. Either cookie is bad or regex is broken"); + continue; + } + + ContactInfo info = new ContactInfo(); + info.name = name; + info.city = sb.toString(); + info.address = address; + info.formattedNumber = phoneNumber; + info.website = website; + + people.add(info); + } + + return people.toArray(new ContactInfo[people.size()]); + } + + private static String extractXmlRegex(String str, String regex, String tag) { + Pattern p = Pattern.compile(regex, Pattern.DOTALL); + Matcher m = p.matcher(str); + if (m.find()) { + return extractXmlTag(str, m.start(), m.end(), tag); + } + return null; + } + + private static String extractXmlTag(String str, int realBegin, int begin, + String tag) { + int end = begin; + int tags = 1; + int maxLoop = 30; + + while (tags > 0) { + end = str.indexOf(tag, end + 1); + if (end < 0 || maxLoop < 0) { + break; + } + + if (str.charAt(end - 1) == '/' + && str.charAt(end + tag.length()) == '>') { + tags--; + } else if (str.charAt(end - 1) == '<') { + tags++; + } + + maxLoop--; + } + + int realEnd = str.indexOf(">", end) + 1; + + if (tags != 0) { + Log.e(TAG, "Failed to extract tag <" + tag + "> from XML/HTML"); + return null; + } + + return str.substring(realBegin, realEnd); + } + + public static ContactInfo reverseLookup(Context context, String number) + throws IOException { + String provider = LookupSettings.getReverseLookupProvider(context); + + String lookupUrl = null; + if (LookupSettings.RLP_WHITEPAGES.equals(provider)) { + lookupUrl = NEARBY_URL_UNITED_STATES; + } else if (LookupSettings.RLP_WHITEPAGES_CA.equals(provider)) { + lookupUrl = NEARBY_URL_CANADA; + } + String newLookupUrl = lookupUrl + number; + + String output = httpGet(newLookupUrl); + + // + + String name = null; + String phoneNumber = null; + String address = null; + + if (LookupSettings.RLP_WHITEPAGES.equals(provider)) { + name = parseNameUnitedStates(output); + phoneNumber = parseNumberUnitedStates(output); + address = parseAddressUnitedStates(output); + } else if (LookupSettings.RLP_WHITEPAGES_CA.equals(provider)) { + name = parseNameCanada(output); + // Canada's WhitePages does not provide a formatted number + address = parseAddressCanada(output); + } + + ContactInfo info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = phoneNumber != null ? phoneNumber : number; + info.website = lookupUrl + info.formattedNumber; + + return info; + } + + private static String httpGet(String url) throws IOException { + HttpGet get = new HttpGet(url); + + if (mCookie != null) { + get.setHeader("Cookie", COOKIE + "=" + mCookie); + } + + String output = LookupUtils.httpGet(get); + // If we can find a new cookie, use it + Pattern p = Pattern.compile(COOKIE_REGEX, Pattern.DOTALL); + Matcher m = p.matcher(output); + if (m.find()) { + mCookie = m.group(1).trim(); + Log.v(TAG, "Got new cookie"); + } + + // If we hit a page with a <meta> refresh and the error URL, reload. If + // this results in infinite recursion, then whatever. The thread is + // killed after 10 seconds. + p = Pattern.compile("<meta[^>]+http-equiv=\"refresh\"", Pattern.DOTALL); + m = p.matcher(output); + if (m.find() && output.contains("distil_r_captcha.html")) { + Log.w(TAG, "Got <meta> refresh. Reloading..."); + return httpGet(url); + } + + return output; + } + + private static String parseNameUnitedStates(String output) { + String name = LookupUtils.firstRegexResult(output, + "<h2.*?>Send (.*?)'s details to phone</h2>", true); + + // Use summary if name doesn't exist + if (name == null) { + name = LookupUtils.firstRegexResult(output, + "<span\\s*class=\"subtitle.*?>\\s*\n?(.*?)\n?\\s*</span>", true); + } + + if (name != null) { + name = name.replaceAll("&", "&"); + } + + return name; + } + + private static String parseNameCanada(String output) { + String name = LookupUtils.firstRegexResult(output, + "(<li\\s+class=\"listing_info\">.*?</li>)", true); + return LookupUtils.fromHtml(name); + } + + private static String parseNumberUnitedStates(String output) { + return LookupUtils.firstRegexResult(output, + "Full Number:</span>([0-9\\-\\+\\(\\)]+)</li>", true); + } + + private static String parseAddressUnitedStates(String output) { + String regexBase = "<span\\s+class=\"%s[^\"]+\"\\s*>([^<]*)</span>"; + + String addressPrimary = LookupUtils.firstRegexResult(output, + String.format(regexBase, "address-primary"), true); + String addressSecondary = LookupUtils.firstRegexResult(output, + String.format(regexBase, "address-secondary"), true); + String addressLocation = LookupUtils.firstRegexResult(output, + String.format(regexBase, "address-location"), true); + + StringBuilder sb = new StringBuilder(); + + if (!TextUtils.isEmpty(addressPrimary)) { + sb.append(addressPrimary); + } + if (!TextUtils.isEmpty(addressSecondary)) { + sb.append(", "); + sb.append(addressSecondary); + } + if (!TextUtils.isEmpty(addressLocation)) { + sb.append(", "); + sb.append(addressLocation); + } + + String address = sb.toString(); + if (address.length() == 0) { + address = null; + } + + return address; + } + + private static String parseAddressCanada(String output) { + String address = LookupUtils.firstRegexResult(output, + "<ol class=\"result people_result\">.*?(<li\\s+class=\"col_location\">.*?</li>)" + + ".*?</ol>", true); + + if (address != null) { + address = LookupUtils.fromHtml(address).replace("\n", ", "); + } + + return address; + } + + public static class ContactInfo { + String name; + String city; + String address; + String formattedNumber; + String website; + } +} diff --git a/src/com/android/dialer/lookup/whitepages/WhitePagesPeopleLookup.java b/src/com/android/dialer/lookup/whitepages/WhitePagesPeopleLookup.java new file mode 100644 index 000000000..b237327b4 --- /dev/null +++ b/src/com/android/dialer/lookup/whitepages/WhitePagesPeopleLookup.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chenxiaolong@cxl.epac.to> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.whitepages; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.PeopleLookup; + +import android.content.Context; +import android.location.Location; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; + +public class WhitePagesPeopleLookup extends PeopleLookup { + private static final String TAG = + WhitePagesPeopleLookup.class.getSimpleName(); + + public WhitePagesPeopleLookup(Context context) { + } + + @Override + public ContactInfo[] lookup(Context context, String filter) { + WhitePagesApi.ContactInfo[] infos = null; + + try { + infos = WhitePagesApi.peopleLookup(context, filter, 3); + } catch (IOException e) { + Log.e(TAG, "People lookup failed", e); + } + + if (infos == null || infos.length == 0) { + return null; + } + + ContactInfo[] details = new ContactInfo[infos.length]; + for (int i = 0; i < infos.length; i++) { + WhitePagesApi.ContactInfo info = infos[i]; + ContactBuilder builder = new ContactBuilder( + ContactBuilder.PEOPLE_LOOKUP, null, info.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 || info.city != null) { + ContactBuilder.Address a = new ContactBuilder.Address(); + a.city = info.city; + a.formattedAddress = info.address; + a.type = StructuredPostal.TYPE_HOME; + builder.addAddress(a); + } + + details[i] = builder.build(); + } + + return details; + } +} diff --git a/src/com/android/dialer/lookup/whitepages/WhitePagesReverseLookup.java b/src/com/android/dialer/lookup/whitepages/WhitePagesReverseLookup.java new file mode 100644 index 000000000..375c63f35 --- /dev/null +++ b/src/com/android/dialer/lookup/whitepages/WhitePagesReverseLookup.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.whitepages; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +import com.android.dialer.lookup.ReverseLookup; + +import android.content.Context; + +import java.io.IOException; + +public class WhitePagesReverseLookup extends ReverseLookup { + private static final String TAG = + WhitePagesReverseLookup.class.getSimpleName(); + + public WhitePagesReverseLookup(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 { + WhitePagesApi.ContactInfo info = WhitePagesApi.reverseLookup(context, normalizedNumber); + if (info == null || 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/src/com/android/dialer/lookup/yellowpages/YellowPagesApi.java b/src/com/android/dialer/lookup/yellowpages/YellowPagesApi.java new file mode 100644 index 000000000..b52a67b1f --- /dev/null +++ b/src/com/android/dialer/lookup/yellowpages/YellowPagesApi.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.yellowpages; + +import android.content.Context; +import android.text.TextUtils; + +import com.android.dialer.lookup.LookupSettings; +import com.android.dialer.lookup.LookupUtils; + +import org.apache.http.client.methods.HttpGet; + +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 = + "http://www.yellowpages.com/phone?phone_search_terms="; + private static final String LOOKUP_URL_CANADA = + "http://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(new HttpGet(mLookupUrl + mNumber)); + } + + private String getPhotoUrl(String website) throws IOException { + String output = LookupUtils.httpGet(new HttpGet(website)); + String galleryRef = LookupUtils.firstRegexResult(output, + "href=\"([^\"]+gallery\\?lid=[^\"]+)\"", true); + if (galleryRef == null) { + return null; + } + + // Get first image + HttpGet get = new HttpGet("http://www.yellowpages.com" + galleryRef); + output = LookupUtils.httpGet(get); + + return LookupUtils.firstRegexResult(output, + "\"type\":\"image\",\"src\":\"([^\"]+)\"", true); + } + + private String[] parseNameWebsiteUnitedStates() { + Pattern regexNameAndWebsite = Pattern.compile( + "<a href=\"([^>]+?)\"[^>]+?class=\"url[^>]+?>([^<]+)</a>", + Pattern.DOTALL); + String name = null; + String website = null; + + Matcher m = regexNameAndWebsite.matcher(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=\"(.*?)\">" + + "(<span\\s+class=\"listingTitle\">.*?</span>)", + 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 = "http://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, + "<div\\s+class=\"phoneNumber\">(.*?)</div>", 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, + "<div\\s+class=\"address\">(.*?)</div>", true); + return LookupUtils.fromHtml(address); + } + + private void buildContactInfo() throws IOException { + Matcher m; + + String name = null; + String website = null; + String phoneNumber = null; + String address = null; + String photoUrl = null; + + if (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/src/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java b/src/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java new file mode 100644 index 000000000..eaaee5779 --- /dev/null +++ b/src/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.yellowpages; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ContactBuilder; +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 org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; + +import java.io.ByteArrayOutputStream; +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")) { + HttpClient client = new DefaultHttpClient(); + HttpGet request = new HttpGet(uri.toString()); + + try { + HttpResponse response = client.execute(request); + + int responseCode = response.getStatusLine().getStatusCode(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.getEntity().writeTo(out); + byte[] responseBytes = out.toByteArray(); + + if (responseCode == HttpStatus.SC_OK) { + Bitmap bmp = BitmapFactory.decodeByteArray( + responseBytes, 0, responseBytes.length); + return bmp; + } + } 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/src/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java b/src/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java new file mode 100644 index 000000000..2e8b65abd --- /dev/null +++ b/src/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.zabasearch; + +import android.text.TextUtils; + +import com.android.dialer.lookup.LookupUtils; + +import org.apache.http.client.methods.HttpGet; + +import java.io.IOException; + +public class ZabaSearchApi { + private static final String TAG = ZabaSearchApi.class.getSimpleName(); + + private static final String LOOKUP_URL = "http://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(new HttpGet(LOOKUP_URL + mNumber)); + } + + 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/src/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java b/src/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java new file mode 100644 index 000000000..afe9961ac --- /dev/null +++ b/src/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.zabasearch; + +import com.android.dialer.calllog.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(); + } +} |