diff options
48 files changed, 4347 insertions, 10 deletions
diff --git a/Android.mk b/Android.mk index a9481d5ad..d6679479a 100644 --- a/Android.mk +++ b/Android.mk @@ -20,6 +20,7 @@ res_dirs := res \ LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs)) LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) \ frameworks/support/v7/cardview/res frameworks/support/v7/recyclerview/res +LOCAL_ASSET_DIR += $(LOCAL_PATH)/assets LOCAL_AAPT_FLAGS := \ --auto-add-overlay \ @@ -30,7 +31,8 @@ LOCAL_AAPT_FLAGS := \ --extra-packages com.android.phone.common LOCAL_JAVA_LIBRARIES := telephony-common \ - ims-common + ims-common \ + org.apache.http.legacy LOCAL_STATIC_JAVA_LIBRARIES := \ android-common \ @@ -41,7 +43,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ com.android.services.telephony.common \ com.android.vcard \ guava \ - libphonenumber + libphonenumber \ + org.cyanogenmod.platform.sdk LOCAL_PACKAGE_NAME := Dialer LOCAL_CERTIFICATE := shared diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4055b4441..06182b10a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -60,6 +60,7 @@ start requests, even if they happen immediately after the user presses home. --> <uses-permission android:name="android.permission.STOP_APP_SWITCHES" /> + <uses-permission android:name="cyanogenmod.permission.WRITE_SETTINGS" /> <application android:name="DialerApplication" @@ -73,6 +74,8 @@ <meta-data android:name="com.google.android.backup.api_key" android:value="AEdPqrEAAAAIBXgtCEKQ6W0PXVnW-ZVia2KmlV2AxsTw3GjAeQ" /> + <uses-library android:name="org.apache.http.legacy" /> + <!-- The entrance point for Phone UI. stateAlwaysHidden is set to suppress keyboard show up on dialpad screen. --> @@ -306,5 +309,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..2bf5215c9 100644 --- a/proguard.flags +++ b/proguard.flags @@ -12,4 +12,7 @@ @com.android.dialer.NeededForReflection *; } +# Keep ExtendedLookupDirectories for assets/contacts_extensions.properties +-keep class com.android.dialer.lookup.ExtendedLookupDirectories { *; } + -verbose diff --git a/res/color/setting_primary_color.xml b/res/color/setting_primary_color.xml new file mode 100644 index 000000000..6d083e2f9 --- /dev/null +++ b/res/color/setting_primary_color.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2008 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="0.26" + android:color="@color/dialtacts_primary_text_color"/> + <item android:color="@color/dialtacts_primary_text_color"/> +</selector> diff --git a/res/color/setting_secondary_color.xml b/res/color/setting_secondary_color.xml new file mode 100644 index 000000000..9ebbc4e42 --- /dev/null +++ b/res/color/setting_secondary_color.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2008 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="0.26" + android:color="@color/dialtacts_secondary_text_color"/> + <item android:color="@color/dialtacts_secondary_text_color"/> +</selector> 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_arrays.xml b/res/values/cm_arrays.xml new file mode 100644 index 000000000..c62060024 --- /dev/null +++ b/res/values/cm_arrays.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <string-array name="forward_lookup_providers" translatable="false"> + <item>Google</item> + <item>OpenStreetMap</item> + </string-array> + + <string-array name="forward_lookup_provider_names" translatable="false"> + <item>Google</item> + <item>OpenStreetMap</item> + </string-array> + + <string-array name="people_lookup_providers" translatable="false"> + <item>WhitePages</item> + </string-array> + + <string-array name="people_lookup_provider_names" translatable="false"> + <item>WhitePages (US)</item> + </string-array> + + <string-array name="reverse_lookup_providers" translatable="false"> + <item>OpenCnam</item> + <item>DasTelefonbuch</item> + <item>Gebeld</item> + <item>WhitePages</item> + <item>WhitePages_CA</item> + <item>YellowPages</item> + <item>YellowPages_CA</item> + <item>ZabaSearch</item> + </string-array> + + <string-array name="reverse_lookup_provider_names" translatable="false"> + <item>OpenCnam (US)</item> + <item>Das Telefonbuch (DE)</item> + <item>Gebeld (NL)</item> + <item>WhitePages (US)</item> + <item>WhitePages (CA)</item> + <item>YellowPages (US)</item> + <item>YellowPages (CA)</item> + <item>ZabaSearch (US)</item> + </string-array> + +</resources> diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml new file mode 100644 index 000000000..445c1465f --- /dev/null +++ b/res/values/cm_strings.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013-2014 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Forward lookup --> + <string name="nearby_places">Nearby places</string> + <string name="people">People</string> + + <!-- Number lookup --> + <string name="lookup_settings_label">Phone number lookup</string> + <string name="lookup_settings_description">Lookup of unknown phone numbers</string> + <string name="enable_forward_lookup_title">Forward lookup</string> + <string name="enable_forward_lookup_summary">Show nearby places when searching in the dialer</string> + <string name="enable_people_lookup_title">People lookup</string> + <string name="enable_people_lookup_summary">Show online results for people when searching in the dialer</string> + <string name="enable_reverse_lookup_title">Reverse lookup</string> + <string name="enable_reverse_lookup_summary">Look up information about the person or place for unknown numbers on incoming calls</string> + <string name="forward_lookup_provider_title">Forward lookup provider</string> + <string name="people_lookup_provider_title">People lookup provider</string> + <string name="reverse_lookup_provider_title">Reverse lookup provider</string> + + <!-- Chinese Reverse Lookup Provider --> + <string name="cyngn_reverse_lookup_provider_package" translatable="false">com.cyngn.chineselocationlookup</string> + <string name="cyngn_reverse_lookup_provider_name">Cyngn Chinese (CN)</string> + <string name="cyngn_reverse_lookup_provider_value" translatable="false">CyngnChinese</string> + +</resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index 8ce3c17ba..f72ad3bd5 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -22,10 +22,6 @@ <color name="dialer_red_highlight_color">#ff1744</color> <color name="dialer_green_highlight_color">#00c853</color> - <!-- Color for the setting text. --> - <color name="setting_primary_color">@color/dialtacts_primary_text_color</color> - <!-- Color for the setting description text. --> - <color name="setting_secondary_color">@color/dialtacts_secondary_text_color</color> <color name="setting_disabled_color">#aaaaaa</color> <color name="setting_background_color">#ffffff</color> <color name="setting_button_color">#eee</color> diff --git a/res/xml/lookup_settings.xml b/res/xml/lookup_settings.xml new file mode 100644 index 000000000..14d2de2d2 --- /dev/null +++ b/res/xml/lookup_settings.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2014 The CyanogenMod Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> + +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + <SwitchPreference + android:key="enable_forward_lookup" + android:title="@string/enable_forward_lookup_title" + android:summary="@string/enable_forward_lookup_summary" + android:defaultValue="false" + android:persistent="false" /> + + <ListPreference + android:key="forward_lookup_provider" + android:title="@string/forward_lookup_provider_title" + android:entries="@array/forward_lookup_provider_names" + android:entryValues="@array/forward_lookup_providers" + android:dependency="enable_forward_lookup" + android:summary="%s" + android:persistent="false" /> + + <SwitchPreference + android:key="enable_people_lookup" + android:title="@string/enable_people_lookup_title" + android:summary="@string/enable_people_lookup_summary" + android:defaultValue="false" + android:persistent="false" /> + + <ListPreference + android:key="people_lookup_provider" + android:title="@string/people_lookup_provider_title" + android:entries="@array/people_lookup_provider_names" + android:entryValues="@array/people_lookup_providers" + android:summary="%s" + android:dependency="enable_people_lookup" + android:persistent="false" /> + + <SwitchPreference + android:key="enable_reverse_lookup" + android:title="@string/enable_reverse_lookup_title" + android:summary="@string/enable_reverse_lookup_summary" + android:defaultValue="false" + android:persistent="false" /> + + <ListPreference + android:key="reverse_lookup_provider" + android:title="@string/reverse_lookup_provider_title" + android:dependency="enable_reverse_lookup" + android:summary="%s" + android:persistent="false" /> + +</PreferenceScreen> + 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 2e07a03b1..4e2fe5c1d 100644 --- a/src/com/android/dialer/calllog/ContactInfoHelper.java +++ b/src/com/android/dialer/calllog/ContactInfoHelper.java @@ -33,6 +33,7 @@ import com.android.contacts.common.util.Constants; import com.android.contacts.common.util.PermissionsUtil; 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.dialer.util.TelecomUtil; @@ -252,6 +253,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 eec24f5bc..60179f6b9 100644 --- a/src/com/android/dialer/database/DialerDatabaseHelper.java +++ b/src/com/android/dialer/database/DialerDatabaseHelper.java @@ -73,7 +73,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"; /** @@ -409,7 +409,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 b7e26d690..902797462 100644 --- a/src/com/android/dialer/list/RegularSearchFragment.java +++ b/src/com/android/dialer/list/RegularSearchFragment.java @@ -30,6 +30,7 @@ import com.android.contacts.commonbind.analytics.AnalyticsUtil; import com.android.dialerbind.ObjectFactory; import com.android.dialer.R; +import com.android.dialer.lookup.LookupCache; import com.android.dialer.service.CachedNumberLookupService; import com.android.dialer.widget.EmptyContentView; import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; @@ -74,12 +75,14 @@ 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)); } @Override diff --git a/src/com/android/dialer/list/RegularSearchListAdapter.java b/src/com/android/dialer/list/RegularSearchListAdapter.java index 2be8a1dd7..944fec64f 100644 --- a/src/com/android/dialer/list/RegularSearchListAdapter.java +++ b/src/com/android/dialer/list/RegularSearchListAdapter.java @@ -39,6 +39,20 @@ public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter { setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, false); } + public ContactInfo getLookupContactInfo(int position) { + ContactInfo info = new ContactInfo(); + final Cursor item = (Cursor) getItem(position); + if (item != null) { + info.name = item.getString(PhoneQuery.DISPLAY_NAME); + info.type = item.getInt(PhoneQuery.PHONE_TYPE); + info.label = item.getString(PhoneQuery.PHONE_LABEL); + info.number = item.getString(PhoneQuery.PHONE_NUMBER); + final String photoUriStr = item.getString(PhoneQuery.PHOTO_URI); + info.photoUri = photoUriStr == null ? null : Uri.parse(photoUriStr); + } + return info; + } + public CachedContactInfo getContactInfo( CachedNumberLookupService lookupService, int position) { ContactInfo info = new ContactInfo(); 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..8e0e18cae --- /dev/null +++ b/src/com/android/dialer/lookup/LookupSettings.java @@ -0,0 +1,110 @@ +/* + * 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 cyanogenmod.providers.CMSettings; + +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_GEBELD = "Gebeld"; + public static final String RLP_DEFAULT = RLP_OPENCNAM; + + private LookupSettings() { + } + + public static boolean isForwardLookupEnabled(Context context) { + return CMSettings.System.getInt(context.getContentResolver(), + CMSettings.System.ENABLE_FORWARD_LOOKUP, 1) != 0; + } + + public static boolean isPeopleLookupEnabled(Context context) { + return CMSettings.System.getInt(context.getContentResolver(), + CMSettings.System.ENABLE_PEOPLE_LOOKUP, 1) != 0; + } + + public static boolean isReverseLookupEnabled(Context context) { + return CMSettings.System.getInt(context.getContentResolver(), + CMSettings.System.ENABLE_REVERSE_LOOKUP, 1) != 0; + } + + public static String getForwardLookupProvider(Context context) { + String provider = getLookupProvider(context, + CMSettings.System.FORWARD_LOOKUP_PROVIDER, FLP_DEFAULT); + + return provider; + } + + public static String getPeopleLookupProvider(Context context) { + String provider = getLookupProvider(context, + CMSettings.System.PEOPLE_LOOKUP_PROVIDER, PLP_DEFAULT); + + return provider; + } + + public static String getReverseLookupProvider(Context context) { + String provider = getLookupProvider(context, + CMSettings.System.REVERSE_LOOKUP_PROVIDER, RLP_DEFAULT); + + if ("Google".equals(provider)) { + CMSettings.System.putString(context.getContentResolver(), + CMSettings.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 = CMSettings.System.getString(cr, key); + + if (provider == null) { + CMSettings.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..32da8ae02 --- /dev/null +++ b/src/com/android/dialer/lookup/ReverseLookup.java @@ -0,0 +1,117 @@ +/* + * 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.gebeld.GebeldReverseLookup; +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); + } else if (provider.equals(LookupSettings.RLP_GEBELD)) { + INSTANCE = new GebeldReverseLookup(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 if (provider.equals(LookupSettings.RLP_GEBELD) + && INSTANCE instanceof GebeldReverseLookup) { + 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/ReverseLookupService.java b/src/com/android/dialer/lookup/ReverseLookupService.java new file mode 100644 index 000000000..25c3f4a85 --- /dev/null +++ b/src/com/android/dialer/lookup/ReverseLookupService.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; + +import com.android.contacts.common.GeoUtil; +import com.android.dialer.calllog.ContactInfo; +import com.android.incallui.service.PhoneNumberService; + +import java.io.IOException; + +public class ReverseLookupService implements PhoneNumberService, Handler.Callback { + private final HandlerThread mBackgroundThread; + private final Handler mBackgroundHandler; + private final Handler mHandler; + private final Context mContext; + private final TelephonyManager mTelephonyManager; + + private static final int MSG_LOOKUP = 1; + private static final int MSG_NOTIFY_NUMBER = 2; + private static final int MSG_NOTIFY_IMAGE = 3; + + public ReverseLookupService(Context context) { + mContext = context; + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + + // TODO: stop after a while? + mBackgroundThread = new HandlerThread("ReverseLookup"); + mBackgroundThread.start(); + + mBackgroundHandler = new Handler(mBackgroundThread.getLooper(), this); + mHandler = new Handler(this); + } + + @Override + public void getPhoneNumberInfo(String phoneNumber, NumberLookupListener numberListener, + ImageLookupListener imageListener, boolean isIncoming) { + if (!LookupSettings.isReverseLookupEnabled(mContext)) { + LookupCache.deleteCachedContacts(mContext); + return; + } + + // Can't do reverse lookup without a number + if (phoneNumber == null) { + return; + } + + LookupRequest request = new LookupRequest(); + String countryIso = mTelephonyManager.getSimCountryIso().toUpperCase(); + request.normalizedNumber = PhoneNumberUtils.formatNumberToE164(phoneNumber, countryIso); + request.formattedNumber = PhoneNumberUtils.formatNumber(phoneNumber, + request.normalizedNumber, GeoUtil.getCurrentCountryIso(mContext)); + request.numberListener = numberListener; + request.imageListener = imageListener; + + mBackgroundHandler.obtainMessage(MSG_LOOKUP, request).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOOKUP: { + // background thread + LookupRequest request = (LookupRequest) msg.obj; + request.contactInfo = doLookup(request); + if (request.contactInfo != null) { + mHandler.obtainMessage(MSG_NOTIFY_NUMBER, request).sendToTarget(); + if (request.imageListener != null && request.contactInfo.photoUri != null) { + request.photo = fetchImage(request, request.contactInfo.photoUri); + if (request.photo != null) { + mHandler.obtainMessage(MSG_NOTIFY_IMAGE, request).sendToTarget(); + } + } + } + break; + } + case MSG_NOTIFY_NUMBER: { + // main thread + LookupRequest request = (LookupRequest) msg.obj; + if (request.numberListener != null) { + LookupNumberInfo info = new LookupNumberInfo(request.contactInfo); + request.numberListener.onPhoneNumberInfoComplete(info); + } + break; + } + case MSG_NOTIFY_IMAGE: + // main thread + LookupRequest request = (LookupRequest) msg.obj; + if (request.imageListener != null) { + request.imageListener.onImageFetchComplete(request.photo); + } + break; + } + + return true; + } + + private ContactInfo doLookup(LookupRequest request) { + final String number = request.normalizedNumber; + + if (LookupCache.hasCachedContact(mContext, number)) { + ContactInfo info = LookupCache.getCachedContact(mContext, number); + if (!ContactInfo.EMPTY.equals(info)) { + return info; + } else if (info != null) { + // If we have an empty cached contact, remove it and redo lookup + LookupCache.deleteCachedContact(mContext, number); + } + } + + try { + ContactInfo info = ReverseLookup.getInstance(mContext).lookupNumber(mContext, + number, request.formattedNumber); + if (info != null && !info.equals(ContactInfo.EMPTY)) { + LookupCache.cacheContact(mContext, info); + return info; + } + } catch (IOException e) { + // ignored + } + + return null; + } + + private Bitmap fetchImage(LookupRequest request, Uri uri) { + if (!LookupCache.hasCachedImage(mContext, request.normalizedNumber)) { + Bitmap bmp = ReverseLookup.getInstance(mContext).lookupImage(mContext, uri); + if (bmp != null) { + LookupCache.cacheImage(mContext, request.normalizedNumber, bmp); + } + } + + return LookupCache.getCachedImage(mContext, request.normalizedNumber); + } + + private static class LookupRequest { + String normalizedNumber; + String formattedNumber; + NumberLookupListener numberListener; + ImageLookupListener imageListener; + ContactInfo contactInfo; + Bitmap photo; + } + + private static class LookupNumberInfo implements PhoneNumberInfo { + private ContactInfo mInfo; + private LookupNumberInfo(ContactInfo info) { + mInfo = info; + } + + @Override + public String getDisplayName() { + return mInfo.name; + } + @Override + public String getNumber() { + return mInfo.number; + } + @Override + public int getPhoneType() { + return mInfo.type; + } + @Override + public String getPhoneLabel() { + return mInfo.label; + } + @Override + public String getNormalizedNumber() { + return mInfo.normalizedNumber; + } + @Override + public String getImageUrl() { + return mInfo.photoUri != null ? mInfo.photoUri.toString() : null; + } + @Override + public boolean isBusiness() { + // FIXME + return false; + } + } +} 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/gebeld/GebeldApi.java b/src/com/android/dialer/lookup/gebeld/GebeldApi.java new file mode 100644 index 000000000..effeb784b --- /dev/null +++ b/src/com/android/dialer/lookup/gebeld/GebeldApi.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.gebeld; + +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; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GebeldApi { + private static final String TAG = GebeldApi.class.getSimpleName(); + + private static final String REVERSE_LOOKUP_URL = + "http://www.gebeld.nl/zoeken.asp?Page=4,%201" + + "&searchfield1=fullnumber&action=Zoeken"; + + private static String REGEX = "\n?\\s*(.*)<(?:(?:BR|br)\\s*/?)>"; + + private GebeldApi() { + } + + public static ContactInfo reverseLookup(Context context, String number) + throws IOException { + String phoneNumber = number.replace("+31", "0"); + Uri uri = Uri.parse(REVERSE_LOOKUP_URL) + .buildUpon() + .appendQueryParameter("queryfield1", phoneNumber) + .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())), + "<div class=\"small-12 large-4 columns information\">(.*?)</div>", true); + + String name = null; + String address = null; + + if (output == null) { + return null; + } else { + Pattern pattern = Pattern.compile(REGEX, 0); + Matcher m = pattern.matcher(output); + + if (m.find()) { + name = LookupUtils.fromHtml(m.group(1).trim()); + + if (m.find()) { + address = LookupUtils.fromHtml(m.group(1).trim()); + + if (m.find()) { + address += "\n" + LookupUtils.fromHtml(m.group(1).trim()); + } + } + } + } + + if (name == null) { + return null; + } + + ContactInfo info = new ContactInfo(); + info.name = name; + info.address = address; + info.formattedNumber = phoneNumber != null ? phoneNumber : number; + info.website = uri.toString(); + + return info; + } + + public static class ContactInfo { + String name; + String address; + String formattedNumber; + String website; + } +} diff --git a/src/com/android/dialer/lookup/gebeld/GebeldReverseLookup.java b/src/com/android/dialer/lookup/gebeld/GebeldReverseLookup.java new file mode 100644 index 000000000..9f1be2e48 --- /dev/null +++ b/src/com/android/dialer/lookup/gebeld/GebeldReverseLookup.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.lookup.gebeld; + +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 GebeldReverseLookup extends ReverseLookup { + private static final String TAG = GebeldReverseLookup.class.getSimpleName(); + + public GebeldReverseLookup(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("+31")) { + // Only handle Dutch numbers + return null; + } + + GebeldApi.ContactInfo info = GebeldApi.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..ad8507a3a --- /dev/null +++ b/src/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.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.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 cyanogenmod.providers.CMSettings; + +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 = CMSettings.System.getString( + context.getContentResolver(), + CMSettings.System.DIALER_OPENCNAM_ACCOUNT_SID); + String authToken = CMSettings.System.getString( + context.getContentResolver(), + CMSettings.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(); + } +} diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java index 35eba4beb..e9c09095e 100644 --- a/src/com/android/dialer/settings/DialerSettingsActivity.java +++ b/src/com/android/dialer/settings/DialerSettingsActivity.java @@ -59,6 +59,12 @@ public class DialerSettingsActivity extends PreferenceActivity { TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + final Header lookupSettingsHeader = new Header(); + lookupSettingsHeader.titleRes = R.string.lookup_settings_label; + lookupSettingsHeader.summaryRes = R.string.lookup_settings_description; + lookupSettingsHeader.fragment = LookupSettingsFragment.class.getName(); + target.add(lookupSettingsHeader); + // Only show call setting menus if the current user is the primary/owner user. if (isPrimaryUser()) { // Show "Call Settings" if there is one SIM and "Phone Accounts" if there are more. diff --git a/src/com/android/dialer/settings/LookupSettingsFragment.java b/src/com/android/dialer/settings/LookupSettingsFragment.java new file mode 100644 index 000000000..1621c923a --- /dev/null +++ b/src/com/android/dialer/settings/LookupSettingsFragment.java @@ -0,0 +1,166 @@ +/* + * 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.settings; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.ListPreference; +import android.preference.PreferenceFragment; +import android.preference.SwitchPreference; + +import com.android.dialer.R; + +import cyanogenmod.providers.CMSettings; + +import java.util.Arrays; + +public class LookupSettingsFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener { + + private static final String KEY_ENABLE_FORWARD_LOOKUP = "enable_forward_lookup"; + private static final String KEY_ENABLE_PEOPLE_LOOKUP = "enable_people_lookup"; + private static final String KEY_ENABLE_REVERSE_LOOKUP = "enable_reverse_lookup"; + private static final String KEY_FORWARD_LOOKUP_PROVIDER = "forward_lookup_provider"; + private static final String KEY_PEOPLE_LOOKUP_PROVIDER = "people_lookup_provider"; + private static final String KEY_REVERSE_LOOKUP_PROVIDER = "reverse_lookup_provider"; + + private SwitchPreference mEnableForwardLookup; + private SwitchPreference mEnablePeopleLookup; + private SwitchPreference mEnableReverseLookup; + private ListPreference mForwardLookupProvider; + private ListPreference mPeopleLookupProvider; + private ListPreference mReverseLookupProvider; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.lookup_settings); + + mEnableForwardLookup = (SwitchPreference) findPreference(KEY_ENABLE_FORWARD_LOOKUP); + mEnablePeopleLookup = (SwitchPreference) findPreference(KEY_ENABLE_PEOPLE_LOOKUP); + mEnableReverseLookup = (SwitchPreference) findPreference(KEY_ENABLE_REVERSE_LOOKUP); + + mEnableForwardLookup.setOnPreferenceChangeListener(this); + mEnablePeopleLookup.setOnPreferenceChangeListener(this); + mEnableReverseLookup.setOnPreferenceChangeListener(this); + + mForwardLookupProvider = (ListPreference) findPreference(KEY_FORWARD_LOOKUP_PROVIDER); + mPeopleLookupProvider = (ListPreference) findPreference(KEY_PEOPLE_LOOKUP_PROVIDER); + mReverseLookupProvider = (ListPreference) findPreference(KEY_REVERSE_LOOKUP_PROVIDER); + + mForwardLookupProvider.setOnPreferenceChangeListener(this); + mPeopleLookupProvider.setOnPreferenceChangeListener(this); + mReverseLookupProvider.setOnPreferenceChangeListener(this); + + updateReverseLookupProviderList(); + } + + @Override + public void onResume() { + super.onResume(); + + restoreLookupProviderSwitches(); + restoreLookupProviders(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final ContentResolver cr = getActivity().getContentResolver(); + + if (preference == mEnableForwardLookup) { + CMSettings.System.putInt(cr, CMSettings.System.ENABLE_FORWARD_LOOKUP, + ((Boolean) newValue) ? 1 : 0); + } else if (preference == mEnablePeopleLookup) { + CMSettings.System.putInt(cr, CMSettings.System.ENABLE_PEOPLE_LOOKUP, + ((Boolean) newValue) ? 1 : 0); + } else if (preference == mEnableReverseLookup) { + CMSettings.System.putInt(cr, CMSettings.System.ENABLE_REVERSE_LOOKUP, + ((Boolean) newValue) ? 1 : 0); + } else if (preference == mForwardLookupProvider) { + CMSettings.System.putString(cr, CMSettings.System.FORWARD_LOOKUP_PROVIDER, + (String) newValue); + } else if (preference == mPeopleLookupProvider) { + CMSettings.System.putString(cr, CMSettings.System.PEOPLE_LOOKUP_PROVIDER, + (String) newValue); + } else if (preference == mReverseLookupProvider) { + CMSettings.System.putString(cr, CMSettings.System.REVERSE_LOOKUP_PROVIDER, + (String) newValue); + } + + return true; + } + + private void updateReverseLookupProviderList() { + Resources res = getResources(); + + String[] entries = res.getStringArray(R.array.reverse_lookup_provider_names); + String[] values = res.getStringArray(R.array.reverse_lookup_providers); + + if (isPackageInstalled(getString(R.string.cyngn_reverse_lookup_provider_package))) { + entries = Arrays.copyOf(entries, entries.length + 1); + values = Arrays.copyOf(values, values.length + 1); + + entries[entries.length - 1] = getString(R.string.cyngn_reverse_lookup_provider_name); + values[values.length - 1] = getString(R.string.cyngn_reverse_lookup_provider_value); + } + + mReverseLookupProvider.setEntries(entries); + mReverseLookupProvider.setEntryValues(values); + } + + private boolean isPackageInstalled(String pkg) { + try { + PackageInfo pi = getActivity().getPackageManager().getPackageInfo(pkg, 0); + return pi.applicationInfo.enabled; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private void restoreLookupProviderSwitches() { + final ContentResolver cr = getActivity().getContentResolver(); + mEnableForwardLookup.setChecked(CMSettings.System.getInt(cr, + CMSettings.System.ENABLE_FORWARD_LOOKUP, 1) != 0); + mEnablePeopleLookup.setChecked(CMSettings.System.getInt(cr, + CMSettings.System.ENABLE_PEOPLE_LOOKUP, 1) != 0); + mEnableReverseLookup.setChecked(CMSettings.System.getInt(cr, + CMSettings.System.ENABLE_REVERSE_LOOKUP, 1) != 0); + } + + private void restoreLookupProviders() { + restoreLookupProvider(mForwardLookupProvider, CMSettings.System.FORWARD_LOOKUP_PROVIDER); + restoreLookupProvider(mPeopleLookupProvider, CMSettings.System.PEOPLE_LOOKUP_PROVIDER); + restoreLookupProvider(mReverseLookupProvider, CMSettings.System.REVERSE_LOOKUP_PROVIDER); + } + + private void restoreLookupProvider(ListPreference pref, String key) { + final ContentResolver cr = getActivity().getContentResolver(); + String provider = CMSettings.System.getString(cr, key); + if (provider == null) { + pref.setValueIndex(0); + CMSettings.System.putString(cr, key, pref.getValue()); + } else { + pref.setValue(provider); + } + } +} |